Repository navigation
feat(graph): run Anonymizer's pipelines on a private graph kernel - #319
binaryaaron wants to merge 187 commits into
Conversation
Add the engine-side graph values and workflow modules (identity and reference values, workflow declarations, static and dynamic admission and substitution, composition and the metered compiler budget) together with the closed effect and artifact-shape values used by later layers. Introduce the lazy public anonymizer.graph facade over them. Tests cover static workflow analysis and the compiler reference, with the workflow_static corpus. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add the engine-side activation modules (activation values, topology and reducer), data-graph declarations and validation, canonical records, capability declarations and bounded plan preparation. Tests and references cover activation, data graphs and the compiler. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add request policies, values and receipts with a pure reducer, typed context and its admission, resource leases and cleanup facts, ingress envelopes and bounded content-free observations. Add the detection and Rewrite artifact types that requests carry. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
…nsport Add execution declarations and sealed results, bounded blocking-worker adapters, a single physical request driver, callback-ingress inspection, foreign transport entry with a client lease, and asynchronous initial context binding. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add execution admission, the invocation runtime and scheduling, dispatch, delegated slot borrowing, frames, map and context materialization, publication and provenance, cleanup, entry points and the executor, plus the NDD source frame. Tests cover effects, map, delegated, foreign and optional-tail conformance with their references. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add evidence admission and authentication, provenance reconciliation and release qualification, with the qualification and evidence suites, witnesses and the evidence model. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
… SDK Add request inspection and extensions (context providers and retained-result views) and lazy root-package exports including graph. Document the public graph SDK in docs/concepts/graph-sdk.md with mkdocs navigation, reference pages and a provider-free example, and add the public API and export-surface tests. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
NddAdapter.run_workflow gains optional source_frame, request_bridge and rewrite_binding parameters; the change is additive apart from removing the private _detect_missing_records. Add the version-qualified DataDesigner request entry, row partitioning and source positions, stage binding, the Rewrite evaluation binding, the data-designer==0.9.4 pin with a drift test, and the compat pytest marker. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add test_production_contracts.py, a provider-free compatibility oracle for the public facade covering Redact projection and failure, saved-result evaluation and DataDesigner retry. It lands before any route moves so each cutover is measured against it. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Admitted core, optional-latent and final stages replace the builtin detection controller. Add the whole-frame detection and Redact consumers, the shared harness (_opaque_redact, _frame_publication, harness retention), the NDD route module _route.py and the graph SDK _authoring_defaults.py. Redact evidence checking rejects empty templates, with matching config, replace.md and skill updates and the skill signature files. Validation-plugin and local-transform mediation and the frozen detection corpora are included. This commit also carries the logging hunks of replace/replace_runner.py. Part of the B10–B12 route cutover; the full suite is green again at B12. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
…acade Graph control owns Rewrite transitions for both DataDesigner schedules. The legacy repair loop and CombinedRewriteWorkflow.run are retired, and one source-bound invocation runs detection plus the semantic tail. The public Anonymizer facade is routed here, including the logging hunks of interface/anonymizer.py. Evaluation source-position fixes are included; evaluate() keeps its existing path. Part of the B10–B12 route cutover; the full suite is green again at B12. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Move Substitute map selection, generation and acceptance under graph control, with native map requests, compatibility entries and the Substitute lineage and compat suites. This closes the route cutover; the full suite is green from here. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Keep sensitive payloads out of library log output. This covers the logging change except interface/anonymizer.py and replace/replace_runner.py, whose logging hunks ride B11 and B10. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add docs/development/graph-sdk-cutover.md and the AGENTS.md section describing the graph SDK preview branch rules. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add tools/dogfood/ with a README, compare.py, dogfood_run.py and model and provider configs for comparing this branch against the released package. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-Authored-By: Claude <noreply@anthropic.com>
Replace the lazy attribute table with static imports and a sorted __all__ that binds the same names to the same objects. This drops the import_module/TYPE_CHECKING mirror of the export list. The pure-import test no longer asserts that importing anonymizer.graph avoids pandas; it keeps its two engine-component cases. The export pin is re-recorded for the new sorted __all__ order. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Importing anonymizer.graph now pulls in the engine eagerly. Defer the four edges that dragged in pandas and numpy: the harness frame types become TYPE_CHECKING imports with local imports at their few runtime uses, and rewrite artifacts import numpy and pandas inside the one method that needs them. import anonymizer.graph drops from roughly 580 ms and 735 modules to roughly 290 ms and 280 modules, with pandas no longer loaded. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…equest bridge Restore the upper-bounded DataDesigner range used on main instead of the exact 0.9.4 pin. The graph request bridge no longer compares the installed version; it checks the private DataDesigner hooks it relies on: - at bind time, before any work runs: DataDesigner._create_resource_provider, ModelRegistry's model_facade_factory parameter and ModelFacade.agenerate; - when DataDesigner first creates a provider or facade, before any request: the registry's facade factory and the facade client's HTTP fields, which are instance attributes and cannot be checked earlier. A missing hook raises AnonymizerWorkflowError naming the attribute, the supported range and the installed version. The bridge records the failure and re-raises it when the binding exits, so DataDesigner's per-row error handling cannot turn it into dropped records. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…oks lazily data-designer 0.9.5 classifies httpx.NetworkError and RemoteProtocolError (for example a ReadError) as connection errors, so its request executor now retries them. A bridge that admits one physical attempt per chain (detection) then saw a second attempt and rejected the whole invocation, where 0.9.4 kept the record's outcome unknown. For single-attempt chains the bridge now hands DataDesigner those errors as a SingleAttemptTransportError, which every supported release classifies as a non-retryable API error; errors 0.9.4 already retried (ConnectError, timeouts) are passed through unchanged. Private DataDesigner names are no longer imported at module scope, so importing anonymizer does not depend on them: - create_retry_transport and ParserException are resolved lazily and checked when the bridge binds; - the health-check skip switch and AsyncTaskScheduler's salvage_max_rounds, used only by the Substitute native map, are checked when the map plans its requests, before any work runs; - ensure_async_engine_loop is imported on first use. Each missing hook raises the standard capability error. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Every route runs DataDesigner through NddAdapter.run_workflow, which binds the source frame to DataDesigner's private dataset builder. A release without those hooks failed with a bare AttributeError message. The binding now checks, before any work runs, DataDesigner._create_dataset_builder and the artifact-storage module's BatchStage members and SDG_CONFIG_FILENAME. When DataDesigner creates the builder, before it runs, it checks the builder's _initialize_generators_and_graph, _write_builder_config and _data_designer_config, and the storage methods and paths it wraps. Each failure raises the standard capability error naming the attribute, the supported range and the installed version. A real DataDesigner always requires the hooks; a caller's stand-in designer without a dataset builder is still left unbound. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Builtin Redact, Annotate and Hash, and builtin detection or Substitute map generation reached from a caller override through super(), ran their DataDesigner calls on a library worker thread. In 0.4.0 they ran on the thread that called run() or preview(). Hand those bodies to the caller's thread like every other body, and run a handoff issued on the serving thread inline instead of raising. New placement tests pin 0.4.0 behaviour for builtin Redact, Annotate and Hash, for detectors that delegate to the builtin run(), and for Substitute under a caller replace runner, from the main thread, a non-main thread and inside a running event loop. The route-resolution rows keep pinning which route each runner shape takes and now pin that every route hands its body to the caller. The interrupt-drain test delivers a real SIGINT instead of simulating one in the kernel's completion wait, which the caller no longer reaches while a body runs. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…r pool Every synchronous body already returned to the caller's thread, but the kernel still started a one-worker pool per invocation whose thread only forwarded the body back to the caller, and the caller polled its queue every 10 ms. Submit delegated bodies straight to the caller's queue through a caller-thread submitter, wake the caller with a completion marker instead of the poll, and drop the per-invocation pool and the caller_callbacks switch, which every entry now sets. Standalone use outside a facade invocation keeps the worker submitter and BlockingPool. A Ctrl-C that lands after a body's result was set no longer turns into an InvalidStateError. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The patch replaced DataDesigner's scheduler wait process-wide so that a graph cancellation reached a DataDesigner run waiting on a library worker thread. Every DataDesigner wait now runs on the thread that called the public entry point, where Ctrl-C reaches DataDesigner's own handler as in 0.4.0, so the patch is no longer needed. Extend the subprocess Ctrl-C test to builtin Redact, Redact under a caller detector that delegates to the builtin run(), and Substitute under such a detector. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
run(), preview() and evaluate() run every synchronous body, builtin or supplied, on the thread that called them, at every depth, with a copy of the caller's context variables. Say so in the graph SDK concepts page and the cutover notes, correct the claim that callbacks get the caller's context rather than a copy, and state where a builtin reached through super() runs. Builtin detection now always receives the caller handoff, so its inline fallback is gone. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
A Ctrl-C that landed after the caller took a body from its queue, or just before it settled the body's future, left that future unresolved. The host then waited for it forever and the drain loop absorbed every further Ctrl-C, so the process only ended on SIGKILL. Track every queued body until it settles and, when an interrupt leaves serving, settle any that remain as failed before re-raising. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Caller code and every DataDesigner wait run on the caller's thread, but the per-invocation event-loop thread still runs internal bookkeeping and DataDesigner's engine threads still run Anonymizer column generators, as on main. Say so instead of claiming the loop thread runs no Anonymizer code, and fix a comment that still said a nested handoff raises. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
When a second Ctrl-C landed while the drain loop was serving, the interrupt handler asked the host loop to cancel even after that loop had closed, and the resulting RuntimeError replaced the KeyboardInterrupt. Share one guarded stop request between the handler and the drain loop. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
A second Ctrl-C landing inside the interrupt handler, before it settled the waiting bodies, could still leave one unsettled and the drain loop waiting forever. Settle first, then request the stop, and settle again on every drain pass so no interrupt window skips it. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
After an interrupt settles every waiting body, the drain loop could still take one of them from the queue and set its result again, which raised InvalidStateError in place of the KeyboardInterrupt. Skip bodies that are already settled, and test the case directly. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Until the SDK is published, anonymizer.graph and the engine modules behind it may change names, export order, signatures, defaults, exception constructors, pickles and import cost. The tests that pin them are refactor safety nets, re-recorded with the change they cover. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…d record boundary measurement Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…emetry fields Graph routes record failures under their operation names. Map "detect" and "substitute-map" to the entity-detection and replace-map-generation failure counts instead of the unknown-step count. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
DetectionScope is now built from the resolved label config the run uses, so the reported scope cannot drift from the engine's policy. The open-mode flag is replaced by allowed_labels (None when entity_labels is unset), matching the allowed/excluded naming used elsewhere. Detect.resolved_scope is removed and Detect.effective_labels is folded into the preflight check. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
|
I’d keep Codex spotted a catch with I’d also let an existing run-wide mapping win over another entity’s new proposal. Falling back because it matches a protected original makes sense; losing the mapping because another proposal picked the same name doesn’t. |
In run mode a substitute emitted for one key is never emitted for another key later in the run, placeholders are numbered across the run, and a key's established run-wide substitute wins over another key's new proposal. Record mode is unchanged. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…aceholder Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…ities Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…ts and report the resolved scope Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
|
@andreatnvidia Both points are right. #394 fixes them: in run mode a substitute is now unique across the whole run, placeholders are numbered run-wide, and an established mapping keeps its substitute when a later record proposes the same one. Record mode is unchanged. Thanks for the catch. |
…ue across the run Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Record one LlmReplaceWorkflow.fold measurement stage per generate_map_only call with per-label fallback counts (own, placeholder_taken, placeholder_protected) and the number of records that received a placeholder. Labels only; folded maps are unchanged. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The failed-attempt loop tested membership in the ordered target list once per attempt, which is quadratic in the number of rows. Keep the list for ordering and test membership against a set of the same targets. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The record-ID fallback tested each surviving ID against the source ID list, which is quadratic. Build the ID-to-position lookup first and use it for both the duplicate check and membership; the error is unchanged. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
A member withheld only by its group now refers to the group's causes tuple instead of its own copy, so stored entries per group drop from (G - C) * C to C. Every TargetOutcome keeps the same withheld_by contents and order. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Calls where every row has no entities return before the fold, so an active measurement session got no LlmReplaceWorkflow.fold record. Emit a zero record on that path so every call records one. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Add a measured all-passthrough call and a three-record case where two records propose one substitute for different names and a third reuses the resulting run-wide placeholder without a new fallback count. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…fallbacks Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
| with stage_timer("LlmReplaceWorkflow.fold", consistency=consistency, input_row_count=len(combined)) as fields: | ||
| maps = _fold_replacement_maps(combined[COL_REPLACEMENT_MAP].tolist(), consistency, requested, tally) | ||
| fields["entry_count"] = sum(len(m.get("replacements", [])) for m in maps) | ||
| fields["records_with_placeholder"] = tally.pop(("records_with_placeholder",), 0) | ||
| fallbacks: dict[str, dict[str, int]] = {} | ||
| for (label, step), n in sorted(tally.items()): | ||
| fallbacks.setdefault(label, {})[step] = n | ||
| fields["fallback_counts"] = fallbacks |
There was a problem hiding this comment.
Fold measurements cannot be imported
generate_map_only() now records LlmReplaceWorkflow.fold on every call, but StageMeasurement.stage does not accept that name. The strict model also rejects the new consistency, entry_count, records_with_placeholder, and fallback_counts fields.
Measured Substitute and non-combined Rewrite runs therefore produce files that read_measurement_snapshot() rejects, blocking completion seals and W&B imports. Update the measurement readers alongside this record and add a test that passes it through the strict reader.
There was a problem hiding this comment.
This still reproduces at 5b764cab, including when there are no entities and no model calls. The emitted fold record fails strict validation on the stage name and all four new fields. The reader schema needs to land with the producer change.
There was a problem hiding this comment.
Agreed—this confirms the finding, including the no-entity fast path. generate_map_only() emits LlmReplaceWorkflow.fold via record_stage() even when there are no entities or model calls; the strict StageMeasurement schema rejects that stage name and the additional fields (consistency, entry_count, records_with_placeholder, and fallback_counts). The reader schema and a strict-reader regression test need to land with the producer change.
There was a problem hiding this comment.
@andreatnvidia Fixed by #398, merged into this branch at cb37b27. The measurement reader and the W&B stage table accept the fold stage and its four fields, including on the no-entity path, and a test passes the record through the strict reader.
Replacement-map generation now records an LlmReplaceWorkflow.fold stage with consistency, entry_count, records_with_placeholder and fallback_counts. The strict ingress model and the W&B stage table row did not know that stage or its fields, so measured Substitute and non-combined Rewrite runs produced snapshots that could not be sealed or imported. Both now accept the stage with typed optional fields; the table row keeps the counts, with fallback_counts as compact sorted JSON under the table opt-in policy. Malformed fold records are still rejected. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
| ) | ||
| replace_elapsed = time.perf_counter() - started - sum(elapsed) # detection records its own stage | ||
| failures.extend(malformed) | ||
| if not detected: # no targets: the step never ran, but the detector still sees the (empty) input |
There was a problem hiding this comment.
At 5b764cab, calling Anonymizer.run() from a graph callback after cancelling the outer token hits this fallback even with nonempty input. Detection then runs with target=(), and resolving the returned positions raises IndexError. A custom detector also executes despite cancellation. The same guard appears in Rewrite. Could we restrict this fallback to genuinely empty graphs and handle cancelled invocations separately?
There was a problem hiding this comment.
@andreatnvidia Confirmed in Replace and Rewrite, and fixed in 7e83c01. After the outer token was cancelled the nested detect step was skipped, and the fallback meant for empty input then ran the detector outside the cancel check. The fallback now runs only when the graph has no targets (7015150), and a nested run() or preview() that the cancellation stops raises AnonymizerWorkflowError without calling the detector. The same rule now applies at every entry into the kernel: both Replace routes, evaluate(), which used to return unjudged rows after a cancel, and the staged detection and rewrite-evaluation runs (tests in 56389bc).
| current, failed = judge.run(current) | ||
| failures.extend(failed) | ||
| present = [c for c in judge.columns if c in current.columns] | ||
| return current[present].to_dict("records") if present else [{} for _ in range(len(current))] |
There was a problem hiding this comment.
A custom replace_runner.evaluate() returning one surviving row and one FailedRecord from two inputs now raises TypeError: invocation operation returned list of length 1, expected 2 entries. The same public Anonymizer.evaluate() call returns the partial result on 7605c13c. The graph keeps the original target count, but this publishes only the surviving rows. Keeping the previous path for custom evaluators would preserve compatibility; otherwise the outputs need explicit source alignment.
There was a problem hiding this comment.
@andreatnvidia Confirmed, and fixed in 82e4500. The judge step returned one entry per returned row instead of one per input row. It now maps returned rows back to their inputs, so a custom evaluate() that drops a row returns the partial result with its failed record, as on main. A test covers Replace and Rewrite.
… stage in strict ingress Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
A custom runner whose evaluate() returns fewer rows than it was given made evaluate() raise a TypeError from the graph kernel's length check. The judge body now maps the returned rows back to their targets, so evaluate() returns the runner's partial frame and failed records again; a target with no returned row reads as judge unavailable. Fixes the #319 review comment on src/anonymizer/interface/anonymizer.py:185 (custom evaluate() returning 1 of 2 rows plus a FailedRecord); the result now matches main. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The Replace and Rewrite detect paths and evaluate() each called their whole-frame step on an empty input when the graph had no targets, with two different predicates. One helper now does this when the graph has no targets. A test covers the detector seeing an empty input in both modes. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
An Anonymizer.run(), preview() or evaluate() called from a graph callback after the token was cancelled behaved differently per path: the detect path called the detector anyway and raised an IndexError, the local Replace route returned rows without replacements, the Substitute route returned no rows, and evaluate() returned unjudged rows. Every entry into the graph kernel now raises AnonymizerWorkflowError when the result shows a target stopped by the cancellation, before any detector or judge call. Stage sequences raise the same error instead of returning after skipped stages. Fixes the #319 review comment on src/anonymizer/engine/replace/route.py:216 (nested run after a cancel reaching the empty-graph fallback; the same guard was in Rewrite). Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
Move the cancel check and the empty-input detector call next to per_target in the engine backend module. run_stages now returns its result and the detection and rewrite-evaluate callers raise on cancel. A test keeps graph_core from importing other anonymizer modules; the one allowed exception is the existing measurement timer import in execution.py. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…evaluation The detection run path and rewrite evaluate each wrap their staged run in a cancel check, but no test failed without it. Drive both inside a cancelled frame, before the first stage and between stages, and assert the workflow error and that no later stage ran. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
This PR is the long-lived base of the Graph SDK feature line. It is not a merge candidate for
main. Later steps land on this branch as merge commits, without rewriting history, andmainis merged in as it moves. The branch containsmainat7605c13c.Against
main, Anonymizer's pipelines run on a small private graph kernel, behind theAnonymizerAPI and config.anonymizer.graphis provisional: its names, signatures and defaults may change before it is published. The compatibility contract is theAnonymizerAPI and config of 0.4.0; this branch only adds to them:Substitute(consistency=...), declared detector facts onDetect(detector_labels,detector_labels_fixed,detector_offset_unit),result.outcome(row)andresult.detection_scope.What changes against
main118 files, +10,269 / -1,442:
src/35 files, +4,186 / -874;tests/55 files, +4,437 / -491;docs/+88 / -10;skills/+62 / -54. Non-blank, non-comment, non-docstring lines undersrc/: +2,438. The branch has 180 commits, 36 of them merges.9a8369cb, +100,340 / -1,237) this PR carriedengine/graph_sdk,engine/harness, their adapters and their test suite. All of it is removed, so none of it appears in the diff againstmain.src/anonymizer/engine/graph_core/(1,593 lines in six files), with five modules:kernel.py(419): kind registry, static plan, the executor over its state, cancel token;authoring.py(379): operations (per target or phase-scoped), workflows, graph values;execution.py(697):prepareandprotect, on the caller's thread, and the request forecast;observe.py(44): the observed-request channel (below);stages.py(45): runs a fixed sequence of stages as one invocation. Detection and the rewrite judges use it.anonymizer.graph(66 lines) re-exports exactly 24 names from the kernel:Assessment,CancelToken,ContextScope,ContextSource,ContextView,Datum,Dependency,GraphRejection,GraphScope,InspectionSummary,Limits,Mention,Operation,OutputGroup,PreparedProtection,ProtectionGraph,ProtectionResult,Reason,Requests,TargetOutcome,Workflow,prepare,protect,register_kind.tests/graph/test_public_api.pypins the list and checks that importing the module loads only the kernel.engine/replace/route.py). Redact, Annotate and Hash run one target per row. Substitute's replacement map is a phase-scoped step of the same per-row workflow, so a row DataDesigner drops is a failed target in the same record as every other row.protect()call runsdetect, thensubstitute-map(Substitute only),replaceandverify-patches. A row the detector drops fails atdetectand blocks its later steps. A caller-supplied runner that overridesrunkeeps the previous path.engine/ndd/backend.py).DataDesignerBackendruns a DataDesigner workflow and returns the surviving frame, each surviving row's input position and the observed request count. At this head only Substitute's map step uses it; detection, Rewrite and the judges callNddAdapter.run_workflow()directly.NddAdapter.run_workflow()is still the only caller ofDataDesigner.create()andpreview().engine/graph_core/observe.py). Code inside a step callsreport_requests(n).ProtectionResult.summary()shows the total as exact only when every step that can make requests reported.report_requestsis not exported.docs/concepts/graph-sdk.md, a provider-free example inexamples/graph_sdk/, the spec programs intests/spec_programs/, and a dogfood kit intools/dogfood/that uses only the publicanonymizerAPI.Since #345-#347 folded in, Rewrite runs as one invocation with detection as its first step, and
evaluate()runs its judges as one invocation. Inside those invocations, detection's own stages (detect,tag-latent,finalize) and the rewrite judges still run throughrun_stages.Reading order
docs/concepts/graph-sdk.md(61 lines): whatanonymizer.graphdeclares, prepares, protects and reports.src/anonymizer/engine/graph_core/:kernel.py,authoring.py,execution.py, thenobserve.pyandstages.py.src/anonymizer/engine/replace/route.py(375 lines): the Replace pipeline. Thenengine/ndd/backend.pyand the facade ininterface/anonymizer.py.tests/spec_programs/: numbered programs, each pinning one kernel behaviour.run_allruns them all.Folded step history
Each step below was a draft PR stacked on this one. This branch was fast-forwarded to
44924e2eand then, step by step, to13fb7470, which contains all of them unchanged. Each is folded into this PR; closed, description kept as the record.f736a427): Redact, Annotate, Hash and Substitute run on the private kernel, and the graph SDK code only those routes used is deleted.b68adb88): Rewrite runs 0.4.0's stages on the kernel, and the graph SDK's own Rewrite path is deleted.0cea5923): detection runs on the kernel, the old graph SDK and its tests are removed, andanonymizer.graphexports the kernel's 23 names.e8ea6481): an operation can run once over every target in aprotect()call and still record one outcome per target.ba93da53): Substitute runs as one invocation, with its replacement map as a step of the per-row workflow. It also carries the kernel executor rewrite from refactor(graph): simplify the kernel executor #337.78815482): in Replace mode, one invocation runs detection and replacement.44924e2e): steps report the requests they make, and Substitute's map step runs through a named execution backend.16ce9a80): Rewrite as a per-target operation with a covering verifier, andneeds_human_reviewprojected from the execution result.f0980f6c): detection as the first step of the Rewrite invocation.13fb7470):evaluate()as one invocation of the judge bodies and covering verifiers.Since then, PRs into this branch merge with merge commits, and
mainwas merged in five times, most recently at7605c13c:phase_scoped.main's fix(rewrite): keep every repair iteration's output on the staged path #354 fix, at this branch's own repair join).How to test
Please test at this PR's head and name the SHA you ran.
CI's Test job runs only on PRs to
main, so this PR is where the full suite runs on GitHub.Known gaps
docs/development/graph-sdk-cutover.md(the M0 stage plan, requirement map and route inventory) is removed on this branch: every path it named is gone and every route it listed has moved to the kernel. It stays in history at44924e2e.skills/anonymizer/differs frommainin 6 files (+62 / -54).require-nvskills-cipasses at this head; if the skill changes again it needs a/nvskills-cicomment, and a signature run may add a bot commit to this branch.engine/graph_sdk/_execution_publication.pyand_workflow_admission.py, which no longer exist.mainare described in the steps' PRs: trace-column dtypes in refactor(replace): run Substitute as one invocation #336, and measurement-stage and custom-adapter notes under "Known differences" in refactor(replace): run detection and replacement as one invocation #342 and feat(graph): count observed requests and name the DataDesigner backend #343.mainis decided when it lands. This repo merges by squash only.docs/concepts/graph-sdk.md:prepare(request_budget=...)checks the planned request count and does not enforce a ceiling on actual requests while the workflow runs. The other four (checks on mentions produced by a step,content()after a failed text producer, a missing leakage number in the final checks,protect()rechecking the prepared combination) are fixed on this branch.result.detection_scope(feat(detect): validate declared detector facts and report the resolved scope #385); whether the labels should also be logged is an open question.🤖 Generated with Claude Code