Skip to content

Amendment 19: typed opaque artifacts on the graph interface - #911

Merged
MaxGhenis merged 35 commits into
mainfrom
amend-typed-artifacts
Sep 12, 2026
Merged

Amendment 19: typed opaque artifacts on the graph interface#911
MaxGhenis merged 35 commits into
mainfrom
amend-typed-artifacts

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

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_outputs with validation, the artifact-edge arm of compile_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) and KernelContext.artifacts, placed before tolerances so amendment 17's "numerics rides at the end" stays literally true (both assertions are now enforced in test_graph_kernel_contract.py).
  • artifact_edges.py (new), keys.py (opaque_artifact_key, a typed_artifacts term in node_key only when a node declares an input), serialize.py, view.py, manifest.py (typed artifact edges are authenticated on load; a corrupt typed manifest loads as StoreCorruptError), 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.
  • The charter entry, docs/graph-interface.lock re-recorded for both files, a changelog.d fragment, sorted graph/__init__.py exports; the acceptance suite's B2 KernelContext field-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 and origin/main's were resolved twice from one process image (shadowed through PYTHONPATH) 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_graph refuses the declaration before any node executes. A gate whose kernel raises becomes a fail verdict and the run continues (amendment 7), so its synthesized result carries no artifacts — modelling an unproduced output (a blocked_by regime 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.py and lazy populations; the store's Frame-metadata format; chunked source hashing; SourceBytesCodec; the per-coordinate _write_node refactor.

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 --check 42156/42156 and 41/41; tools/ci_test_groups.py --verify clean; ruff check clean. 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 in artifact_edges.py getting tests. Lane report: PROGRESS.md (root journal) and the worktree's out.md.

🤖 Generated with Claude Code

MaxGhenis and others added 30 commits September 11, 2026 19:48
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>
MaxGhenis and others added 4 commits September 11, 2026 23:42
…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>
@MaxGhenis
MaxGhenis merged commit 0c3f4f6 into main Sep 12, 2026
23 checks passed
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