Skip to content

chore(graph-sdk): run Rewrite as a per-target operation with a covering verifier - #345

Merged
binaryaaron merged 6 commits into
aagonzales/graph-sdk/feat/mainfrom
aagonzales/chore/graph-sdk-rewrite-lowered
Oct 30, 2026
Merged

binaryaaron merged 6 commits into
aagonzales/graph-sdk/feat/mainfrom
aagonzales/chore/graph-sdk-rewrite-lowered

Conversation

@binaryaaron

@binaryaaron binaryaaron commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Rewrite mode now runs as one graph invocation, and the record decides each row's release. An invocation-scoped rewrite step runs today's Rewrite pipeline once over all entity rows. A model-free assess-final verifier then turns each row's final evaluation into an assessment. needs_human_review is projected from the record. Output columns, row sets and DataDesigner calls match the base, and no public name is added.

This branch stacks on #319 and lands on #319's branch by fast-forward, without rewriting history.

What changes

  • The rewrite operation (engine/rewrite/rewrite_workflow.py). _rewrite_lowered makes one target per entity row; rows with no entities stay outside the graph and take the passthrough defaults as before. The step runs the existing stages as plain calls:

    • staged: replacement map, pipeline, then the bounded evaluate-repair loop;
    • combined: the one rewrite-combined workflow.

    The step fans out per target as text plus a rewrite-metrics value. The rewrite-metrics kind is registered by name only, not exported. The fan-out is keyed on each row's input position, so a frame that repair has reordered still maps each row to its own target. Each row's entry falls into one of three cases:

    • a row whose extracted rewrite is not text fails its target with RewriteUnavailable, and its output row stays with null text;
    • a row the DataDesigner run dropped fails as missing_output;
    • any other error from the step reaches the caller as before.
  • The verifier. assess-final reads the final candidate's metrics and calls no model. A target is met when the privacy checks pass: no high-sensitivity leak, the replacement was ready, and leakage is within flag_leakage_above. The last evaluation in the repair loop already scored the final candidate, so the DataDesigner calls are exactly the base's.

  • The review flag. needs_human_review is assigned in one place, per row: true when the target is not met, or when utility is below flag_utility_below. Protection and the human-review flag stay separate decisions, so a row flagged only for low utility is protected and still flagged. The values and dtypes equal the base's on both paths. On the combined path, the finalizer still computes its own value inside the DataDesigner workflow, and that value agreed in every probed case.

  • The combined workflow (combined_rewrite_workflow.py). It runs through the same lowering. Its DataDesigner column config is unchanged.

  • Spec program SP-16. It has thirteen checks:

    • a bounded loop inside an invocation-scoped operation, with a covering verifier that sees only the last pass;
    • failed targets and request counts;
    • Rewrite's verifier and review-flag boundaries;
    • error re-raise and the review flag's dtype;
    • fan-out with a reordered frame and with a dropped middle row;
    • source positions with passthrough rows;
    • the replacement-readiness default and its coercion.

evaluate(), detection, the Replace routes and the kernel are unchanged. Detection in Rewrite mode still runs before the graph, and moves into it with the evaluate() change.

Net non-blank, non-comment, non-docstring code lines under src/: +69 (rewrite_workflow.py +76, combined_rewrite_workflow.py −7).

Known differences

  • A row whose leakage score is missing while a leakage limit is set now fails only that row's target, and the row is flagged. Before, the staged path raised TypeError for the whole run.
  • When the input frame already carries a needs_human_review column, the projected column keeps that column's dtype; before, the staged path replaced it with bool.

Checks

  • make check passes.
  • make test: 1,593 passed, with test_wandb_snapshot_rejects_fifo_socket_and_device deselected as on the base.
  • uv run --group dev python -m tests.spec_programs.run_all: 97 pass, 8 retired, 0 fail.
  • A probe through Anonymizer.run on both Rewrite paths ran 176 assertions:
    • needs_human_review equals "not met, or utility below the floor" on every row;
    • a row with an empty rewrite completes with null text and is flagged;
    • a dropped row is reported as a failed record;
    • after a partial repair every row keeps its own text and metrics.
  • Ten probe scenarios produce frames equal to the base, columns and dtypes included.
  • Mutation testing of the new code: every mutant is caught except three with no observable difference, one most likely without one, and one that cannot be reached without changing the code.
  • Parity against main (f294dc1e) on pandas 2 and 3, over all cases: no blocking difference. Against the base branch, case by case: no blocking difference, and the only informational differences are trace-column order at the level main shows against itself.
  • Executed lines per case against main: median 1.23×, at most 1.26× on cases whose status matches main (base: 1.23× and 1.26×).
  • No new imports of private names across packages, no new upward imports, and no new import cycles.

🤖 Generated with Claude Code

binaryaaron and others added 6 commits October 28, 2026 20:34
One protect() call now runs the rewrite over the entity rows of the
detection frame. The invocation-scoped `rewrite` operation wraps the
existing frame stages (or the combined workflow) and fans out text plus
a metrics value per target, keyed on the row order. The model-free
`assess-final` verifier turns the final evaluation into the privacy
assessment. needs_human_review is assigned from the record: not met, or
utility below the flag. A row whose rewrite is not text fails as
RewriteUnavailable, stays in the output with null text and does not
abort the run.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
E16.1 attributes the verifier's assessment to the loop's last value,
E16.2 shows a target the body fails stays out of the protected set while
the run completes, and E16.3 reports the observed request count exactly
under an opaque operation and a verifier that declares none.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The record-derived column overwrote the combined path's Arrow bool column
with a NumPy bool one. Build the column in the dtype the frame already
holds, falling back to bool for the staged path, so the output frames
match the pre-change ones dtype for dtype.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Add E16.4-E16.9: the final verifier's leakage rules and thresholds, the
needs_human_review projection and its dtype, error propagation out of the
rewrite body, source positions with passthrough rows, and the replacement
readiness default.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Add E16.10 (rows returned in partial-repair order keep their own text and
metrics, output in input order) and E16.11 (numpy and pyarrow booleans for
replacement readiness are coerced like a Python bool).

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…on order in SP-16

Add E16.12 (a row dropped by the backend leaves every surviving target its
own metrics and review flag) and E16.13 (a target that is not met with no
utility score is flagged without raising).

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Base automatically changed from aagonzales/chore/graph-sdk-execution-backend to aagonzales/graph-sdk/feat/main October 29, 2026 21:15
@binaryaaron
binaryaaron merged commit 16ce9a8 into aagonzales/graph-sdk/feat/main Oct 30, 2026
10 checks passed
@binaryaaron
binaryaaron deleted the aagonzales/chore/graph-sdk-rewrite-lowered branch October 30, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant