Skip to content

let a graph row assert the handoff target (ADR-0032) - #135

Merged
kikashy merged 3 commits into
mainfrom
graph-handoff
Aug 24, 2026
Merged

let a graph row assert the handoff target (ADR-0032)#135
kikashy merged 3 commits into
mainfrom
graph-handoff

Conversation

@kikashy

@kikashy kikashy commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #128.

ADR-0032 discharges ADR-0025's graph-surface deferral, both carriers under one semantics: expectedHandoffTarget asserts the composite's reported target (the result node's own — the value a target-only pack edit reaches while every disposition byte stays identical) and expectedNodeHandoffTargets asserts named nodes' (each such node also named in expectedNodes). Version-gated by graphMatrixVersion "2" with the pack matrix's machinery built for this surface: silence reads as version 1, a version-2 member under version 1 is refused with the version it would take, and ADR-0025's load-time rot-pin inverts into exactly that refusal.

ADR-0025's semantics extended whole: decoded-value comparison by the one comparator (SameHandoffTarget, exported so two surfaces cannot disagree), null as an assertion, capped renderings as pairs set before the comparisons run, "unavailable" exactly where a run was refused under an expected composite. Deliberately not ported: the render-once pass, the digest-binding guard, and the dedicated 4 MiB counter — both renderings are minted at the comparison from values this run reported, and rows are charged whole by the existing report budget.

Tests: the defect-class differential (a target-only pack edit invisible without the assertion, caught with it — composite and node both), reported-target semantics (null/named on escalating and non-escalating rows), unavailable on refusal, loader refusals (version gate, case-fold, beside-error, unnamed node, wrong shapes), CLI and MCP passthrough with exit-code movement. Six mutations each fail a test (comparator neutered, version gate removed, unnamed-node check removed, pair dropped, node comparison skipped, unavailable dropped).

Material-decision impact: public-surface; review: round to be posted on this PR

🤖 Generated with Claude Code

kikashy and others added 3 commits August 24, 2026 09:53
ADR-0025's defect class, closed on the last surface that had it: an edit
reaching only escalation.target.name left every disposition byte identical,
and a graph suite stayed green over it.

- Under graphMatrixVersion "2", expectedHandoffTarget asserts the
  composite's reported target (the result node's own) and
  expectedNodeHandoffTargets asserts named nodes' — each such node also named
  in expectedNodes, as a pack row asserts beside its own disposition.
- ADR-0025's semantics whole: decoded-value comparison by the one comparator
  (SameHandoffTarget, exported so two surfaces cannot disagree), null as an
  assertion, capped renderings as expectedHandoffTarget/actualHandoffTarget
  pairs set before the comparisons run, "unavailable" exactly where a run
  was refused under an expected composite.
- The version machinery is the pack matrix's, built for this surface:
  silence reads as version 1, and a version-2 member under version 1 is
  refused with the version it would take. ADR-0025's load-time rot-pin on
  this surface inverts into exactly that refusal.
- Not ported, deliberately: the render-once pass, the digest-binding guard,
  and the dedicated 4 MiB counter — both renderings are minted at the
  comparison from values this run reported, and rows are already charged
  whole by the report budget.
- ADR-0025's graph-surface deferral is discharged via the index annotation;
  its text is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…round tests (review round 1)

- Mismatch details say reported, not configured: on a non-escalating
  disposition the configured target can match while the reported target is
  correctly null (round-1 finding 7).
- The version gate's strictness is explicit and pinned: wrong-typed
  graphMatrixVersion stays a shape refusal, null and empty are refused
  rather than silently read as the default, the supported list is literal
  and cumulative with a fail-closed rank guard, and a hygiene test holds
  every introduction-map version to a place in the list (findings 2, 4).
- An explicit-null expectedNodeHandoffTargets is refused as the shape defect
  it is, instead of silently erasing the assertion (finding 3).
- The pair invariant is narrowed honestly and pinned: well-formed assertions
  about runs this walk performed; row-defect mismatches report the defect
  and no pair; a classless graph-layer refusal degrades to unavailable told
  by the detail, not by an error class (findings 5, 6).
- New tests: version-gate contract (code/exit/message), all assertions
  coexisting with ADR-0031 traces on one comparison, disposition mismatch
  retaining its pair, classless refusal, no-pair defect paths, and the
  rendering budget differential.
- ADR: the per-assertion rendering cost is analyzed rather than waved at —
  the graph walk re-reads and re-admits every node's pack per row by
  design, so one canonicalization of one member of those bytes per asserted
  row is a constant factor on existing work, not the amplification class
  ADR-0025's render-once pass exists to prevent (finding 1, partially
  declined: the proposed render-once port would reintroduce the cross-row
  handle whose only honest form needs the digest guard this surface has no
  use for).
- Changelog and ADR narrow the compatibility claim to what is true and
  declare the three gate changes; the 0025 annotation uses the
  partial-supersession phrasing; the comparator doc names both carriers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
@kikashy

kikashy commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1 — disposition

Reviewer: Codex (codex-cli 0.145.0, OpenAI) · Reviewed SHA: f7d0965dac7bc4ca956ef4a868b36946cdfab583 · Fix commit: 32da4f2 (covered by the dispositioned-finding exception; the follow-up commit is the status flip only)

# Severity Finding Disposition
1 HIGH Per-row rendering reintroduces ADR-0025's rejected work amplification (10k rows × 1 MiB target ≈ 10 GiB of repeated canonicalize+hash). Partially declined, with the analysis now in the ADR. The premise transfers from the wrong surface: ADR-0025's render-once pass matters because the pack walk loads each pack once per run, so per-row rendering would have changed the run's asymptotics. The graph walk reads and re-admits every node's pack per row by design (readNodePack inside Evaluate, per row) — the cited pathological run already performs ≥ pack-bytes of read+decode+admission per row, and one canonicalization of one member of those same bytes per asserted row is a bounded constant factor on that existing cost, not a new class. The proposed fix would also reintroduce the cross-row rendering handle whose only honest form needs the digest guard — machinery this surface has no use for, as the ADR records. Accepted: the ADR was silent on this cost; constraint 4 now carries the analysis.
2 HIGH The old-document compatibility claim is false (wrong-type reclassified VERSION/2; null and "" newly refused; message change). Accepted, fixed. Wrong-typed graphMatrixVersion is classified SHAPE/exit 1 as the strict decoder classified it; null and empty are now deliberately refused (neither is silence — the looseness this loader has been shedding) and declared in the changelog and ADR rather than smuggled; the byte claim is narrowed to omitted-or-supported-version documents. The gate's full contract (code, exit, sentence) is pinned per input in TestGraphMatrixVersionGateContract, and the reclassification is mutation-verified.
3 MEDIUM expectedNodeHandoffTargets: null silently accepted and erased. Accepted, fixed. A present member must be an object; explicit null is refused as SHAPE with a sentence saying the literal null is not an absent member. {"n":null} stays valid. expectedNodes' long-standing null tolerance predates this member and is deliberately unchanged — its own decision if ever. Mutation-verified.
4 MEDIUM Version machinery fails open on the next bump (supported list derived from consts; rank -1 admits). Accepted, fixed. The supported list is literal and cumulative; the rank check fails closed on an unrankable introduction; a hygiene test holds every introduction-map version to a place in the list. The fail-closed guard itself cannot be discriminated while the list is complete — it is the belt to the hygiene test's suspenders, labeled here as such.
5 MEDIUM ActualErrorClass does not universally distinguish "unavailable" (classless graph-layer refusals). Accepted, fixed. ADR and result docs now say: any unexpected refusal degrades to "unavailable"; a §8.4-classed refusal sets the class beside it; a classless refusal is told by the detail. TestClasslessRefusalReportsTheTargetUnavailable exercises a fact-conflict refusal end to end; mutation-verified (restricting the pair to classed refusals fails it).
6 MEDIUM Pair-exactly-when-asserted has wire-reachable holes (row-defect disposition; unknown node). Accepted, narrowed rather than half-rendered. The invariant is now stated as: a well-formed assertion about a run this walk performed; a row-defect mismatch reports the defect in the detail and no pair. Both paths pinned in TestRowDefectMismatchCarriesNoPair.
7 MEDIUM "configured handoff target differs" is the wrong premise on non-handoff dispositions. Accepted, fixed. Both details say "reported handoff target"; tests updated; the RowCase doc states the reported-when-requested rule.
8 LOW Changelog conflates closed-input and MINOR rules; index annotation wording. Accepted, fixed. The changelog states the two rules separately; the 0025 annotation uses the partial-supersession phrasing with the deferral discharge.
9 LOW Exported comparator doc names only the pack-matrix bound. Accepted, fixed. The doc names both carriers' byte bounds generically.

From the surviving-mutations list, also added: all-assertions-coexist (headline + node + ADR-0031 traces on one comparison), disposition-mismatch-retains-pair, executed node-null match, and the rendering budget differential (bare-exact boundary, ADR-0031's argument applied to this member; mutation-verified). Not added: a pre-PR byte golden (the no-member substring assertions on unasserted rows pin the same fact) and a render-count test (per finding 1's disposition, the property to hold is cost class, which the ADR now states and source structure enforces — there is no separate render pass to count).

Reviewer's sandbox could not run go test; the full gate ran author-side at 32da4f2: gofmt/vet clean, full suite green, corpus 20/20. Round-1 fix mutations: 4/4 caught (map-null check, wrong-type reclassification, classless-unavailable restriction, budget charging), on top of the six pre-review mutations in the PR body.

ADR-0032 flipped to accepted.

@kikashy
kikashy merged commit 73a7780 into main Aug 24, 2026
6 checks passed
@github-actions

Copy link
Copy Markdown

🎉 Thank you for your contribution to Judgment Pack!

If you’d like another task, choose a path that interests you:

@kikashy kikashy mentioned this pull request Aug 24, 2026
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.

Let a graph row assert the handoff target, completing ADR-0025 on the graph surface

1 participant