Amendment 19: typed opaque artifacts on the graph interface - #911
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contract tests for ArtifactType/ArtifactOutput/ArtifactInput, the two new Node fields and their elision from the canonical projection, ArtifactValue, KernelContext.artifacts, the compile-time artifact edge, opaque_artifact_key and the consumer key term, and the one-screen view. Red: the interface does not exist yet (4 collection errors). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ArtifactType, ArtifactOutput and ArtifactInput in decl.py; ArtifactValue and KernelContext.artifacts in kernel.py. compile_graph resolves every declared artifact edge (unknown producer, undeclared output, mismatched type, and self-dependence are compile errors) and makes the producer a predecessor, so an artifact cycle is refused by the existing depth computation. The declarations are normative but elided from the canonical projection when empty, so a node that declares no artifacts keeps its pre-amendment key; a consumer key gains a typed_artifacts term naming the producer's opaque_artifact_key, which keys.py now exposes under the domain the executor already used for undeclared opaque outputs. Declarations round-trip through graph JSON and appear in the one-screen view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified delivery to the consumer, the required declared output, memoized edges, the cached typed contract, manifest schema-3 provenance, the numeric scope a typed edge may not launder, and the artifact payload in the input context digest. Red: 7 of 8 fail against the executor as it stands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New artifact_edges.py derives a producer's NumericScope, refuses an edge that would launder it (a platform_bitwise or tolerance_bound payload needs a consumer of the same class), renders the portable descriptor a receipt records, and rebuilds a verified ArtifactValue from one. run_graph resolves every node's typed contract once, loads each declared input's bytes from the store after checking the producer receipt's identity, and hands the consumer an immutable ArtifactValue per alias. The payload and its provenance enter the input context digest, so B4's mutation check covers artifacts. A kernel that omits a declared output is rejected (a cached record that lacks one is a miss). The cache record carries the typed contract at schema 2 and a graph declaring no artifacts keeps schema 1 and its store hits; a cached contract that disagrees with the graph is corruption. NodeReceipt carries the same provenance, the run manifest serializes it at schema 3, and _validate_typed_ancestry authenticates every edge on load, including that a release's gate ancestry covers gates reached only through bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records what the amendment adds, which acceptance items it touches, and the measured node-key answer: the two new fields are elided from the canonical projection when empty, so no node key moves for a graph that declares no artifacts (the acceptance suite is green with no re-pin and the 20 node keys of the three toy graphs are byte-identical before and after). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t 19) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A gate whose kernel raises becomes a `fail` verdict and the run continues (amendment 7), so its synthesized result carries no artifacts; a declared typed output would turn that verdict into an aborted run instead. Amendment 19 carries no regime for an output a node was unable to produce, so the executor refuses the declaration at run start rather than modelling it, and amendment 7 stays literally true for every legal node shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Charter F2 cannot be routed around by amendment 19: an artifact edge joins the same predecessor set as a cell edge, so _release_tier walks it and the manifest's typed ancestry check refuses a release whose gate_ancestry omits a gate reached only through bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 node keys across the four toy acceptance graphs, the 41-node UK spine and the 8-node US post-transfer graph, computed against origin/main's sources and against the amendment's, are byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Direct coverage of _validate_typed_ancestry: a recorded input must resolve to a present producer whose output descriptor is byte-identical and whose stored opaque identity matches, and a release may not omit a gate that reaches it only through a byte edge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The producer lives in the CREATE version and the consumer in the version a FILTER opens; the byte edge crosses the boundary, the producer stays a predecessor, and auto/require/forbid agree on the consumer's key and its recorded typed provenance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Error require_compatible_scope raises NodeRejectedError, which is a GraphRuntimeError and so escaped RunManifest.load's (TypeError, ValueError) conversion: a manifest whose recorded edge laundered its producer's numeric scope surfaced as a node rejection instead of the store corruption it is. Inside manifest validation that refusal is malformed provenance, so it is converted to ValueError at the call site and the loader's contract holds. Found by the adversarial review of this extraction; reproduced before the fix, and now covered end to end through RunManifest.load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It is the artifact's store identity, derived from the producing node's key and the output name, not a hash of the payload. The store validates the bytes filed under it against their own recorded SHA-256 on every load (charter E1) and the executor checks the key against the producer's receipt, so the bytes are verified — but calling the key a "content identity" claimed a mechanism the code does not have. Found by the adversarial review of this extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…measured Two review findings. _validate_typed_ancestry evaluated set(receipt["gate_ancestry"]) for every release node, including in a manifest with no typed artifacts at all, so a malformed scalar raised a bare TypeError at construction where it used to reach RunManifest.tier's precise "invalid gate ancestry" diagnostic. The set is now built only when a byte edge actually reached a gate, so a manifest declaring no artifact edges behaves exactly as it did before. The amendment's node-key measurement said "the four toy acceptance graphs", but _toy.py defines three builders and the measurement used four instances of them; all six graphs are now named with their node counts, so the measurement can be re-run. The F2 sentence now separates the two mechanisms that hold it — the executor's tier derivation over predecessors, and the loader's check, which binds only on a foreign manifest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_validate_result raised StoreMiss for a restored result lacking a declared artifact, but it runs outside run_graph's try/except StoreMiss, so that spelling could never produce the fallback it named — it would have aborted an auto run with a require-shaped failure. The miss decision belongs to _require_record_shape, which runs inside the fallback; by the time a restored result reaches _validate_result the record has been accepted, so a still-absent artifact is StoreCorrupt. Covered by a test that drops the producer's stored artifact entry: require reports the miss, auto re-executes the kernel at the same node key and rewrites the bytes at the same artifact identity. Found by the adversarial review of this extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A type name or version, and the producer's output name, move both ends of the edge; the consumer-local alias moves only the consumer, because the producer never sees it. The CREATE node above them never moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The producer has its own kernel ref, so re-hashing only its implementation isolates the edge: the consumer reads none of its cells and its key moves anyway, because the producer's node key is inside the consumer's typed_artifacts term. Without the declaration the same edit leaves the consumer's key alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… to run Two review findings. The charter and opaque_artifact_key's docstring claimed that declaring a type "gives an existing output the identity it always had". The derivation is indeed preserved — same domain, same formula as the executor's private helper on main, verified byte-for-byte — but artifact_outputs is normative, so declaring a type moves the producing node's key and the output's identity with it. The branch's own key test already asserted that. Both texts now say what is true, and the key test pins the consequence. run_graph also read every declared input's payload before deciding whether the consumer was a cache hit, so a warm replay read a fitted model once per consumer for nothing, on top of the producer's own restore. The producer receipt and the descriptor are authenticated from identities alone, which needs no bytes; the payload is now read only on the path that runs a kernel. Every check and every error type is where it was, and a test asserts that a fully-hit run performs no consumer-side payload read while still catching a tampered producer identity. Found by the adversarial review of this extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The node-key answer is re-measured by resolving microcosm.graph twice from one script — once from this branch, once from origin/main — so only the graph-kernel code varies. All 74 node keys and all 74 canonical projections across the six named graphs are byte-identical. Also records why the two local test_release_target_parity failures are not this lane's and not CI's: they are guarded on a pinned feed outside the repo whose July vintage predates #855's label requirement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the F1 worker-identity report the file still held from an earlier lane. Covers the per-file diff, the measured node-key answer, every command with its direct exit code, and the two non-zero exits that are pre-existing rather than this lane's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An adversarial audit measured artifact_edges.py with a line trace: eleven non-docstring statements never ran in the whole graph suite, all of them on the surface that parses provenance written elsewhere. The manifest says it "authenticates every edge on load"; these are the arms that make that true. Three tests through the public NodeReceipt/RunManifest surface now drive value_from_descriptor's four refusals, scope_from_payload's malformed-scope and malformed-tolerance refusals and its tolerance reconstruction, and require_compatible_scope's platform-plus-tolerance refusal — which numeric_scope cannot build and only a foreign manifest can carry. The trace now reports zero unexercised statements in the module. Also names, in the two places a reader would otherwise wonder, why the consumer-side receipt comparison in run_graph has no test: generated receipts cannot reach it, because the producer's own record-shape check or _write_node settled the identity first. The guard stands as the consumer's own check should those paths ever diverge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent reconstructions of the tree at that commit run its own test file against its own sources and get 8 failed, 63 passed. The direction is conservative — the commit was redder than its message claims — but the tally was wrong in the message and repeated here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The charter's freeze is a human gate: nothing in the repo compares the lock with the two files, which is how the integration branch changed both without an amendment. graph-interface-lock-test (8bd6e05) already adds the test; this branch passes it as it stands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y true Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # PROGRESS.md
…t only the ends The charter entry and the lane report said keys move only for a node that declares an artifact edge. By A3 every descendant of either end also moves — the consumer's cell readers and any structural node whose base version contains the producer or consumer, since its key binds every member's key. No graph on main declares an edge, so the measured 74 unchanged keys stand; the wording now says what the measurement shows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When a byte edge reaches a gate, _validate_typed_ancestry built a set of the release's gate_ancestry before tier validated it, so a foreign manifest carrying a non-sequence there raised a bare TypeError instead of tier's diagnostic naming the node and value. The typed check now applies the same shape rule first and raises the same message; a regression carries the case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PROGRESS.md and out.md said nothing was pushed, that a whole-workspace run was still going, and that the interface lock had no automated enforcement with the lock-test branch unmerged. Each is dated in place: the branch is PR #911, the run was superseded by CI, and #910 merged the enforcement test on 2026-09-11. Root journals are history, not state (CLAUDE.md). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The round-2 note sat only at the top of section 8; the header and section 9 still said nothing was pushed and no PR existed. Both now carry the same dated note beside the claim. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Amendment 19 to the graph interface charter (
docs/graph-acceptance.md, "Interface freeze"): typed opaque artifacts. Extracted from the #893 integration branch, where it had been applied to the two frozen files without an amendment, and landed the charter's way — as amendment 17 was (cdbf71888+80b63ba14).What it adds
decl.py:ArtifactType,ArtifactOutput,ArtifactInput;Node.artifact_inputs/Node.artifact_outputswith validation, the artifact-edge arm ofcompile_graph, and elision of both fields from the canonical projection when empty.kernel.py:ArtifactValue(immutable bytes with type, content key, producer key and numeric scope) andKernelContext.artifacts, placed beforetolerancesso amendment 17's "numerics rides at the end" stays literally true (both assertions are now enforced intest_graph_kernel_contract.py).artifact_edges.py(new),keys.py(opaque_artifact_key, atyped_artifactsterm innode_keyonly when a node declares an input),serialize.py,view.py,manifest.py(typed artifact edges are authenticated on load; a corrupt typed manifest loads asStoreCorruptError),executor.py(the executor honours declared edges across population versions under every resume policy; descendant-exact invalidation reaches through a byte edge), and the graph explorer shows a node's typed artifact provenance.docs/graph-interface.lockre-recorded for both files, achangelog.dfragment, sortedgraph/__init__.pyexports; the acceptance suite's B2KernelContextfield-set change is its own commit (a2b6dfb0b).Node keys
No node key moves for any graph that declares no artifact edge — and there are none on
main. Keys move only for the two ends of a declared edge. Measured, not asserted: the branch's graph package andorigin/main's were resolved twice from one process image (shadowed throughPYTHONPATH) over both country graphs and the three toy acceptance graphs.Interim ruling (yours to overturn)
A gate kernel may not declare a typed artifact output;
run_graphrefuses the declaration before any node executes. A gate whose kernel raises becomes afailverdict and the run continues (amendment 7), so its synthesized result carries no artifacts — modelling an unproduced output (ablocked_byregime for consumers) would be a further amendment and interacts with the execution-state work deliberately left on the integration branch.Deliberately out (each its own lane)
SeedSource.KEYED/keyed_uniform(Amendment 20, next);availability.py/ execution state / manifest schema 4;attachments.pyand lazy populations; the store's Frame-metadata format; chunked source hashing;SourceBytesCodec; the per-coordinate_write_noderefactor.Verification (direct exit codes)
Red-first at each step (declaration contracts 4 collection errors → green; executor contracts 8 of 8 failing → green). Whole graph package green including every
test_acceptance_*and the new interface-lock test after merging main (post-#909, #910);tools/spec_engine_coverage.py --check42156/42156 and 41/41;tools/ci_test_groups.py --verifyclean;ruff checkclean. An adversarial audit (five dimensions, refute-by-default verifiers) surfaced one surviving finding — a red-commit count of 7 that was 8 — corrected in the journal; two refuted findings still led to eleven previously unexecuted statements inartifact_edges.pygetting tests. Lane report:PROGRESS.md(root journal) and the worktree'sout.md.🤖 Generated with Claude Code