Repository navigation
fix(rewrite): flag incomplete disposition coverage and verbatim-retained entities - #353
binaryaaron wants to merge 8 commits into
Conversation
…ned entities Add a deterministic check module for the rewrite pipeline: - disposition_coverage lists detected and latent entities that the sensitivity disposition never mentions. Rows with gaps are flagged for human review on both the staged and combined paths; the gaps are kept in the _disposition_missing_entities trace column. - retained_protected lists entities marked for protection whose original value still appears verbatim in the rewrite. A new evaluate column, _retained_protected_entities, is computed on every evaluated candidate; a non-empty list triggers repair under every risk tolerance, names each entity and its method in the repair prompt, and flags the row for review if it survives the loop. Rows with nothing retained get the same repair prompt as before. Logs carry counts only. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Unit tests for normalization and boundary matching, repair trigger and prompt text, a replay of recorded dispositions, and staged and combined runs through the real NddAdapter with deterministic stub columns. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…ting 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Set the flag in place with .loc instead of rebuilding the column, so the combined path keeps bool[pyarrow] rather than falling back to numpy bool. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
… flags Assert that the combined graph's repair column reads the per-iteration retained column, so a lost mapping entry fails a test. Add a mixed-frame test where rows are repaired, so failing rows are concatenated after passing rows on the staged path, and check the review flags and coverage gaps land on the right record ids on both paths. Remove internal vocabulary from test comments and rename the recorded disposition fixture to a neutral name. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
When every row's retained-entity list is empty at the first evaluation, the column comes back as an Arrow list of nulls; a later evaluation returns lists of strings. Concatenating the passing and repaired rows turns the column into nested numpy arrays, and seeding the next repair call JSON-stringified them with default=str, so the repair prompt could not parse the cell and dropped the [RETAINED] line from the second repair onwards. Normalize the column with the other evaluation payloads, and accept pyarrow scalars in normalize_payload. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
[Medium impact] The PR appears safe to merge; the previous overlap issue is fixed and no new blocking issues were found. SummaryAdds deterministic checks for missing protection decisions and protected values left in rewritten text.
|
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
|
Some of these changes are not necessary with the sensitivity disposition refactor (#333). That PR streamlines the sens disp prompt output so we can handle lots of entities (i.e. require fewer tokens) and there's a check that all entities are covered. As for "retained_protected returns the disposition entries whose method is not leave_as_is and whose value still appears in the rewritten text. ", I"m not sure why we would want this. If the value is still in the rewritten text, it's the job of the Privacy QA to pick that up. |
|
Closing this in favour of #333. Summary of what I checked before deciding, from ten recorded rewrite runs at the default Coverage. Agreed, #333 makes this half redundant. #333 rebuilds each disposition entry's identity from the supplied inventory by ID and raises when the returned count is short ( Retained values. The privacy QA does detect them. In the recorded runs, 5 rows kept a value the disposition had marked for protection (
That is a threshold-policy question rather than a detection one, so a separate text matcher is the wrong tool for it. I have opened #358 to track it: should a "yes" from the privacy QA on an item the disposition marked for protection trigger repair regardless of #354 does not use any of this PR's code. It is now rebased onto |
Rewrite mode now flags a record for human review when its sensitivity disposition leaves a detected entity uncovered, or when a protected entity's original value survives verbatim in the rewritten text. Until now both failures were silent. With a small
max_tokens, the disposition can cover only part of a record's entities: the rewrite prompt then handles only the listed ones, the privacy QA does not ask about the rest, and real names reach the output withleakage_massnear 0 andneeds_human_reviewFalse.What changes
A new pure module,
engine/rewrite/retention.py, makes no model calls and provides two checks.disposition_coveragereturns the detected entities (keyed on value) and latent entities (keyed on value and label) that have no disposition entry. A non-empty result setsneeds_human_reviewand is recorded in the trace column_disposition_missing_entities. It does not trigger repair, because a repair cannot supply a missing disposition.retained_protectedreturns the disposition entries whose method is notleave_as_isand whose value still appears in the rewritten text. Matching works as follows:casefold;(?<!\w)/(?!\w)rather than\bso values like "B.S." and "(555) 010-0142" match;leave_as_isspan is exempt, so a kept "Florida State University" does not count as a retained "Florida". A protected "Florida State University Foundation" extending beyond that kept span is still reported.The result is recorded in the trace column
_retained_protected_entities. It triggers repair under every risk tolerance preset (max_repair_iterations=0still disables repair). The repair prompt gains one line per retained entity naming its value and method; for rows with nothing retained, the prompt is byte-identical to before. If the value is still there after repair, the record is flagged for review.Both Rewrite paths are wired: the staged workflow and
Rewrite(use_combined_graph=True). Common-word values such as "History" can match by chance;docs/concepts/rewrite.md("Working with flagged records") names the two trace columns and this false-positive class.No public column, signature or default changes. The two new columns are internal and appear only in
trace_dataframe. Log lines carry counts only.Relation to #333
#333 adds a coverage verifier at the disposition step. This change touches none of #333's files and reads only
entity_value,entity_labelandprotection_method_suggestion. Once both land, the two coverage signals overlap, and one should become the single source.Checks
make checkpasses.PYTEST_ADDOPTS=--basetemp=/tmp/af353-all make test: 1,655 passed at72a5dba5.tests/engine/test_retention.py: 72 passed. Three new regressions for overlapping names, case/whitespace variants, and multiple occurrences fail before the fix and pass after it.tests/engineon pandas 3.0.6.tests/engine/test_retention.pycover, on both paths:max_repair_iterations=0, but the record is still flagged;🤖 Generated with Claude Code
For reviewers
tests/engine/test_retention.py,tests/engine/test_rewrite_workflow.py,tests/engine/test_combined_rewrite_workflow.py.leave_as_isentries are never flagged; a protected value inside a kept longer value ("Florida" in "Florida State University") is not flagged; a retained value triggers repair under every risk preset; both Rewrite paths (use_combined_graphon and off) carry the two trace columns.