fix(consolidation): apply sanitize_text to the _DedupDecision merge write path - #3144
Conversation
…rite path The dedup merge path passed the LLM's synthesized text straight to the fold UPDATE with only .strip() applied, so control characters and lone surrogates reached SQL unscrubbed. _CreateAction and _UpdateAction already scrub their text via a sanitize_llm_output field_validator; the merge path did not. Character-safety only (control chars + surrogates), matching the existing create/update behaviour. Adds a regression test alongside the existing fold test; that test is left untouched.
|
Heads-up on the red check, since it doesn't come from this PR.
On test evidence — |
nicoloboschi
left a comment
There was a problem hiding this comment.
Approve. Correct, minimal fix at the single _dedup_adjudicate chokepoint — covers both reconcile-create and reconcile-update fold paths. Fallback (or best_text) and .strip() semantics preserved; test asserts the char is removed, not just that the text was dropped. Character-safety only, consistent with the declined-content-sanitizer decision on #2544/#2547.
Follows up #2544.
@nicoloboschi on that issue:
This is that fix, scoped to the merge path.
Character-safety only
sanitize_llm_outputis an alias ofsanitize_text(llm_wrapper.py), which strips ASCII control characters and lone UTF-16 surrogates — the things that breakjson.loadsand the Postgres UTF-8 encode. This is not the declined content sanitizer; no content filtering is added here.The asymmetry
_CreateActionand_UpdateActioneach scrubtextthrough asanitize_llm_outputfield validator._DedupDecisionhas only_normalize_action, so the merge path handed the LLM's synthesized text to the foldUPDATEwith just.strip()applied.The
best_textfallback is preserved: a decision whose text sanitizes away to empty still folds into the twin's existing text rather than blanking the row.Test
Adds
test_dedup_llm_merge_sanitizes_text_before_writenext to the existingtest_dedup_llm_merge_folds_into_twin, which is left untouched. It injects a NUL into the merge decision and asserts the foldUPDATEargument is clean.Verification
assert "\x00" not in args[1], and the other 41 tests in the file pass either way — so it detects this gap and nothing else.tests/test_consolidation_dedup.py— 42 passed.tests/test_consolidation*.py— 114 passed, vs 113 on unmodifiedmain; the delta is exactly this test. Both runs report the same 83 setup errors, which are environmental: this checkout lacks the optionalsentence-transformersextra.tests/test_llm_wrapper.py— 47 passed.ruff checkclean,ruff format --checkclean,ty check hindsight_api/reports no diagnostics inconsolidator.py.