Repository navigation
feat(spec): accepted-plan compiler — composition half of MS-01 (R12) - #351
Merged
Merged
Conversation
…ciliation R12) Turns a plan an EXTERNAL agent authored — after the server re-read its providers (R10) and resolved its programs (R11) — into the deterministic structure the V-next settlement compiler encodes. PCC does no decomposition: the plan's shape is the caller's; this only decides whether it can be funded exactly and in which canonical form. Guarantees, each fail-closed with a typed violation: - ONE V-NEXT JOB PER OPERATOR. PolicyIdentity has exactly one operator, who signs the JobPolicy; payees who are not the operator never sign, so a multi-operator plan compiles to one job per operator instead of binding one operator's signature over another's terms. - CANONICAL UNIT ORDER. Unit order is money-significant (unitsRoot folds ids in array order). Topological sort, ties by nodeId: the order a planner listed nodes in cannot move a unit (permutation tests). - EXACT BIGINT ECONOMICS. f = floor(g*feeBps/10000), n = g - f, payouts conserve exactly per unit (R34 ===); no JS Number on the money path. - BOUNDED AUTHORITY. Sum of g must fit the server-issued reservation (R13) in its currency, for this request. - ASSURANCE. Non-zero tiers must pass the injected program gate (evidence's assertAcceptedProgramForTier, #349); no gate -> refused; two different programs -> refused under v3 (never collapsed). - ONE ALGORITHM. The v3 composition root is derived from the same input the units come from and echoed into every unit. - FROZEN-ABI LIMITS. <=16 units/job, 5 <= g <= 2^128-1, feeBps <= 1000, n > 0, operator != payer, no zero addresses. Not done here, by design: ABI encoding (escrow's R14 compiler in @pcc/contracts), reservation issue/consume (R13 store, schema pending an operator decision), provider re-read (R10), the HTTP route (R9, needs R28). Tests: 26 new; full @pcc/spec suite 823/823 (39 files) including them; four mutation checks (reservation cap, missing gate, tie-break, fee floor) each turn exactly one test red. tsc --noEmit clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…+astra review of #351) Cross-family review (sol gpt-5.6 + astra gpt-6, both DO-NOT-SHIP) found the v3 compositionRoot commits the PLAN but not several settlement terms: two compiled deals differing only in a node's tier, the fee, or the signing operator shared one root; and caller-supplied currencyDecimals could make the committed cost disagree with the gross actually pulled (g=10^7 at 6 vs 18 decimals). Both confirmed mapping, economics, authority and test polarity sound. Fixes: - acceptedDealDigest: sha256 over the canonical JSON of the WHOLE compiled deal (every unit field incl. tier/fee/amounts/payees/order, signing operators, reservation, currency + decimals, both roots). This is the digest the reservation consume seals (MUST-CLOSE 8). The docs now say plainly what compositionRoot does and does not bind. - Decimals are server-resolved from SETTLEMENT_TOKEN_DECIMALS (USDC: 6); the input no longer takes decimals; unknown currency -> refused. - A non-zero tier must carry a program hash, checked here independently of the injected gate (sol: a permissive gate approving null slipped through). - Rejection diagnostics are sorted (permutation-stable). Tests: +8 (each reviewer counterexample is now a regression test, incl. sol's isolated tier1-vs-tier2 case); full @pcc/spec 831/831; tsc clean. Mutation checks: dropping program-required, all tier fields from the digest, the decimals table, or the sort each turns one test red. (A first tier test did NOT isolate tier -- a co-varying program field hid the mutation; replaced by the isolated case.) pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…med input (astra confirmation of #351) Astra's confirmation review of fb68b1d returned SHIP-WITH-FIXES: findings 1-3 (unbound settlement terms, caller decimals, null program on a non-zero tier) CLOSED; finding 4 NOT CLOSED, plus one robustness gap. - Duplicates: the second copy of a node id used to be skipped before validation, so [bad x, good x] reported duplicate-node + gross-out-of-range while [good x, bad x] reported only duplicate-node. Every occurrence is now validated, duplicate-node is emitted once per id, and a malformed node set stops before any stage that assumes unique, well-formed nodes. - Malformed runtime input: a non-string operator was flagged and then hit .toLowerCase() before the rejection (TypeError). Pattern checks now demand a real string first (RegExp.test coerced ["plan-1"] to "plan-1", so an array planId, nodeId or tierKey used to COMPILE), and diagnostics use a String() that cannot throw. Tests: 3 new + 1 extended; each fails against fb68b1d (the malformed-input test reproduces astra's TypeError). Full @pcc/spec 834/834 (39 files); tsc --noEmit clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
This was referenced Sep 24, 2026
…<-> R15)
pcc-economics (#2252) asked where compileAcceptedPlan meets
compileEconomics. Composition adopts economics' own output shape
(CompiledEconomics.units[] {unitRef, gross, fee, net, payouts[]} plus
economicTermsHash and rightsTermsHash, feat/economics-agreement-compiler
@ 08935a5), injected as CompileDeps.splitNet. unitRef is the composition
node id.
The compiler calls the splitter once, with every unit in canonical plan
order ({nodeId, operator, payoutAddress, g, f, n}, bigint), and holds the
answer to its own numbers, refusing and never repairing:
- the unit set must be exactly the plan's (a missing, extra or duplicate
unit is economics-malformed);
- gross/fee/net must equal floor(g*feeBps/10000) and n = g - f
(economics-mismatch, one per field per unit);
- each unit has 1-16 legs to non-zero addresses with positive integer
base-unit amounts (invalid-payout-leg / too-many-payout-legs); the 256
legs-per-job cap is implied by 16 units x 16 legs;
- legs conserve EXACTLY per unit, with every unit reported
(payout-sum-mismatch);
- a refusal, or a malformed result, is typed and never a throw.
Both terms hashes are sealed in acceptedDealDigest (null when no agreement
applies). Without a splitter, behavior is unchanged: one leg, and the
payout address receives n.
Tests: 8 new (45 in the file); full @pcc/spec 842/842; tsc clean.
8 mutation checks (fee cross-check, conservation, unit set, leg cap,
zero-address leg, zero-amount leg, hash sealing, duplicate unitRef) each
caught.
pcc-composition 8a0f4de0, goal pcc-reconciliation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…hot (astra review of af425ed) Astra's review of the split hook: DO-NOT-SHIP. The checks were sound for plain data, but the runtime boundary could be bypassed: - a `recipient` getter could grow the payouts array after its length was checked, so a 17th leg was consumed past the 16-leg cap (and a job could exceed 256 legs); - a hash getter could return a valid digest to the check and another value to the final read; - throwing getters or proxies anywhere in the answer threw out of the compiler instead of producing a typed violation. snapshotSplit() now copies the answer into plain data, reading every property exactly once and every array's length once (a lying proxy length is refused), inside one catch boundary: a throw while reading is economics-malformed. Only the copy is validated and used. The splitter call itself stays outside the boundary: a throwing splitter is a server fault and propagates, rather than being read as a refusal. Amounts are bounded to 78 digits (a uint256 has 78). Not changed: the reviewer's claim that BASE_UNITS' `$` admits "1\n" holds for PCRE/Python, not for a JS regex without the m flag. A test now pins that "1\n" is refused. Tests: 52 in the file (7 new or rewritten). The bad-hash test now uses an otherwise complete answer (it used to fail on the unit set first). New cases: array growth, hash read-once, throwing getters and proxies, a throwing splitter, rights-hash-only / recipient-only / amount-only / leg-order-only digest changes, unit-order invariance, net-only drift, unit substitution, per-unit errors that cancel globally, non-canonical and 79-digit amounts, and the 16-legs and 16x16=256 boundaries. 4 fail against af425ed. Full @pcc/spec suite green; tsc clean. 12 mutation checks each caught (adds: length re-read mid-loop, hash read twice, no catch boundary, no amount length bound). pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…er's input (astra confirmation of 4479b79) Astra's confirmation of the economics-answer snapshot: all six findings CLOSED (and it withdrew the "$ admits \n" claim: correct for JS). One new escape: an answer getter, or the program gate, runs AFTER the input is validated and can mutate a shared reference to the CALLER's input (e.g. input.feeRecipient). Unit construction then re-read it, and the digest sealed an unvalidated value; an input getter defined late could also throw outside any boundary. snapshotInput() now copies the whole input (plan fields, nodes with their evidence requirements, edges, reservation) into compiler-owned frozen plain data. It reads every property and every array length exactly once, before anything else runs. Validation, the injected gate and splitter, unit construction and the digest see only that copy. A throw while reading, or a lying length, is invalid-plan-field "input". Lists are bounded (1024 nodes, 4096 edges, 64 evidence requirements per node) for resource safety. Tests: 58 in the file (6 new or strengthened): answer getter mutating the caller's feeRecipient; late-throwing input getter; input getter read once; a gate mutating a node after validation; throwing, lying and over-bound input; empty and single-leg payouts; sparse legs; revoked proxies; lying payout/unit lengths; throwing leg getters; the 78/79-digit boundary; unequal units for the unit-order check; mismatch assertions naming node and field. The input-mutation tests fail against 4479b79. 16 mutation checks each caught. Full @pcc/spec suite green; tsc clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…ers read defensively (astra round 3 on ce18cf4) Astra round 3: the input-snapshot fix held for ordinary object-shaped input, but the general guarantee was open: - a callable object (Object.assign(() => {}, fields)) skipped copying, so a splitter could later rename a "validated" reservation that the digest then sealed; - a non-array container (e.g. a revocable proxy) was kept by reference and could throw after the snapshot; - the catch classified the thrown value with instanceof, which a hostile value (a revoked proxy) makes throw; - the program gate's returned answer was read without protection. Now: - Every structurally invalid value (a function, a non-array where a list belongs, an object where a primitive belongs) becomes NOT_DATA, a frozen compiler-owned sentinel that fails every check. Primitives are copied. Nothing in the snapshot is a caller reference. - A bound is recognized by the identity of a token snapshotInput owns; the catch never inspects the thrown value. - readGateResult() reads the gate's answer once and defensively. A malformed answer is program-gate-refused "malformed-gate-result"; the gate itself throwing stays a server fault. - The caps are documented as product limits (not ABI) and are enforced before any element is read. Tests: 65 in the file (7 new, 2 strengthened). New: a deep read-counting proxy proves every input property (fields, nested evidence, edges, reservation, lengths, indices) is read at most once and that the result equals the plain compile; callable reservation and node; delayed proxy revocation; a hostile thrown value; malformed gate answers; each cap at its limit and one above with zero elements read; a splitter mutating the caller's reservation and nested evidence. The input-mutation tests now compare the WHOLE plan and digest to an untouched baseline. The 4 escape tests fail against ce18cf4. 20 mutation checks each caught. Full @pcc/spec suite green; tsc clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…nswer's scalars (astra round 4 on 0571c00) Astra round 4: findings (1)-(4) of round 3 are CLOSED. One blocking path remained: deps.assertProgramForTier and deps.splitNet were each read twice (presence check, then call), so a callback could swap a dependency between the reads, e.g. a gate redefining deps.splitNet as a throwing getter, and cause a compiler-generated exception. Also, the splitter snapshot copied raw scalar slots, so an object-valued refusal code had its own toString invoked by show(). - Both callback slots are read ONCE, before any input, and used from those captures throughout. A slot that is present but is not a function fails closed: a non-function gate is program-gate-missing; a non-function splitter is economics-malformed "dependency", never a silent single-leg default when economics was wired. - Every scalar in the splitter snapshot passes through leaf(), so no caller object or function survives it; a refusal code renders as "<object>" without calling the caller's conversion. - Noted: diagnostics for malformed JSON values now show "<object>" rather than String(value) (e.g. nodeId ["x"]). Acceptance is unchanged; only the message is. Tests: 70 in the file (5 new; whole-plan equality tests now assert baseline success first). New cases: a gate swapping deps.splitNet; dependency getters read exactly once; non-function dependency slots; an object-valued refusal code; the edges and evidence caps refusing with zero element reads. The 4 dependency/code tests fail against 0571c00. 23 mutation checks each caught. Full @pcc/spec suite green; tsc clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…oxy equality (astra round 5: SHIP) Astra's round-5 confirmation of 2c6324d returned SHIP: the dependency path is CLOSED, and no validation/use mismatch, retained caller reference or escaping exception remains beyond the stated server faults. This adds its one nonblocking suggestion. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
7 of 8 tasks
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…tra round 3 on 27a23c6) Astra round 3 on #355: the sparse-tier finding is CLOSED, but the broader getter/proxy/callback class was open, the same class closed in the compiler (#351): - Claims were retained and re-read after validation. A tierKey getter passed as tier0 and was returned as tier3. A loader could re-point two claims to one node id after the duplicate check. - Live rows were retained across callbacks. A csdForType callback could change the row's kernelId after the kernel was selected, mixing k-2's identity with k-1's operator. - Pricing was re-read. A pricing getter could price the gross at $6.50 and the digest at $7.25; a baseCost getter could bypass the minimum check. - Throwing getters on claims and rows escaped the service. A tier list's length was re-read. Now every claim, capability row (with a pricing snapshot and a fixed-length tier copy), kernel row and loader answer is read EXACTLY ONCE into owned plain data. Validation, comparison, the digest (capPrice runs on the same pricing snapshot) and the output use only those copies. The dependencies and the tenant option are captured once, before any claim. A loader or csdForType call that THROWS still propagates (an outage must not read as "not found"). A failure to READ data is a typed verdict: - invalid-claim "unreadable-claim" - a row that cannot be attributed is missing - an unreadable loader answer is unavailable "malformed-live-row" A dependency that is not a function throws TypeError (a wiring fault). Tests: 42 (9 new): tierKey getter, a loader re-pointing node ids, a csdForType callback mutating the row, a pricing getter, a baseCost getter, throwing claim/row getters, lying tier length, unreadable loader answers, an unreadable claims list, non-function deps. All 9 fail against 27a23c6. Mutation checks: 5 new snapshot mutations all caught. Of the 13 prior ones, 12 are caught; "unattributable rows accepted" survives because the guard is redundant with the requested-id filter (claims carry only validated ids), so it is kept as defense in depth. Scoped tsc clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
… outcomes carry verdicts and a submission digest The pattern that closed #351 and #355 under cross-family review, now applied to the seam (#356) before its confirmation round: - The submission is copied once (snapshotSubmission: requestId, reservationId, each node's fields, edges; lengths read once, lists capped, non-primitives become an owned sentinel). The program cross-check, R10 and the compile use only that copy. A callback that mutates the caller's submission changes nothing. - The reservation record is read once. A malformed record (non-numeric expiresAt, or minTier outside 0..3) is a typed refusal, "malformed-reservation". - Dependencies and policy are read once, before any input. An unreadable or non-function dependency is a defined wiring fault (TypeError). Each dependency is called with deps as its receiver, including the gate the compiler calls. The principal and tenant are read once. - Every outcome after the snapshot carries `submissionDigest`, a type-tagged sha256 of the submission exactly as evaluated, so a read model can prove the submission it shows belongs to this outcome. It also carries `verdicts`, all of R10's verdicts whenever R10 ran. That field moves down from #357, where it belongs to the seam. Tests: 30 in external-plan-trace.test.ts (7 new). 17 mutation checks each caught: 7 new (fields read twice, compile edges taken from the caller, reservation read twice, receiver dropped, unguarded wiring, unguarded submission read, digest ignoring edges) plus the 10 earlier seam mutations re-anchored. Scoped tsc clean. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…omics #2755) Economics asked (#2755, #360 @bad29552) to bind agreementHash, not only the two terms hashes. Those do not cover the agreement's envelope (asOf, version, deadline), so sealing only them would leave asOf mutable after acceptance. agreementHash covers both terms hashes and the envelope. - NetSplitResult ok now REQUIRES agreementHash. It is now structurally identical to economics' PlanSplitResult. - snapshotSplit reads it exactly once, with the other scalars. A missing, malformed or non-string value is economics-malformed "agreement-hash"; a throwing getter is "result". It is never a throw. - CompiledAcceptedPlan.agreementHash (lowercased) is null when no agreement split the payouts. It is part of the acceptedDealDigest preimage, and the domain moves to PCC:accepted-deal:v2 because the preimage schema changed. Nothing is persisted under v1: the R13 store does not exist yet, and VCR carries the digest as opaque bytes. Tests: 70 -> 73, spec suite 870/870, and the test file typechecks. Six mutation checks were all killed: the hash left out of the preimage, not digest-checked, re-read from the answer, not lowercased, dropped by compile, and swapped for the economic hash. This changes #351 after its round-5 SHIP, so it goes back to cross-family review (board rule 1). pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
… be verified end to end (N19) The oracle reads a funded tier's evidence two ways. One is event presence under the CSD's ladder (#383). The other is a registered verifier per listed primitive, and a stub verifier fails closed. A tier whose primitives nobody can verify was still fundable here, as long as its committed program fit. That sold assurance the oracle cannot check, and the tier evidence no stage enforces weakened the promise without anyone seeing it. assertAcceptedProgramForTier now takes the funded CSD's whole evidence map in place of one tier. It refuses with tier-not-eligible, and the reasons, unless tiers 0..T are eligible under computeCsdEligibility's oracle-enforcing mode (requireImplementedVerifier). A funded tier absent from the CSD is tier-not-in-csd. The program check runs against the CSD's own tier. The primitive index is injectable, for tests and for when verifiers go live. As authored, document-print-and-mail is eligible only at tier 0: - tiers 1-3 lack ident.registered_key, which receipt.kernel_signed and machine.execution_log depend on; - tier 2 has no Family-G primitive; - six of its verifiers are stubs. So the tier2 program (0xd229c8da) stays pinned but cannot be funded until the CSD and its verifiers are fixed. This is N19's "or those tiers made unfundable", derived instead of hand-listed. API change for composition (#351/#356): pass evidence: csd.evidence in place of tier: csd.evidence[tierKey]. Tests: committed-program.test.ts 31/31, including 8 new eligibility cases. The existing program-leg negatives now fund a fixed CSD with live verifiers. Spec suite green; tsc clean. 7 mutants killed: leg removed, report-only, reasons scope, off-by-one, tier-in-CSD, missing-tier reason, caller index. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…25): canonicalPlan + planHash
Before this, the accepted deal sealed money and assurance, but not WHICH
document gets printed (MC 10, VCR #2300 and #2332). Now:
- plan-json.ts: a node's `inputs` and `constraints` are the caller's
plain-JSON content. It says WHAT runs, never who is paid. The copy is
exactly JSON: no undefined, NaN, bigint, function, symbol, holes or
non-plain objects, and no "__proto__" key. It is bounded in depth, keys,
array length, string and key length, value count and canonical size. It
reads every property and length once into owned frozen data, and a read
that throws is a refusal.
- canonical-plan.ts: VCR's per-node execution contract.
- Content: schema, plan and node id, capability (type, id, CSD, digest),
operator, payTo, the exact amount in base units, job, assurance
(tier, program, evidence), inputs and constraints. Addresses and hashes
are lowercased.
- planHash = "sha256:" + hex(sha256(canonicalize(plan))), byte-exact
against VCR's crossrepo-accepted-bundle-v1, including its one-byte
mutation.
- Compiler:
- Execution JSON is copied in the read-once snapshot. Bad JSON becomes an
unforgeable marker and is reported as invalid-execution-json for EVERY
bad field, independent of node order.
- Each NodeUnitBinding carries its canonicalPlan and planHash.
- The acceptedDealDigest preimage (still v2, not yet reviewed) seals the
carried planHash AND a hash recomputed from the carried content, so the
existing recompute-and-compare catches either being tampered with.
- execution-contract.vectors.json: a golden vector for VCR (the deal, the
canonicalPlans, planHashes, digest and 3 mutations). Regenerate only
deliberately, with PCC_WRITE_VECTORS=1.
Tests: 13 new, spec 883/883, tests typecheck. 25 mutation checks, all
killed: 10 on the compiler, 13 on the JSON copier, 2 on the hash. The last
two survivors led to a read-once test on the inputs property itself and
the removal of two dead undefined checks.
pcc-composition 8a0f4de0, goal pcc-reconciliation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…nto the sealed deal (N25) An agent's plan may now say WHAT each node runs on: `inputs` (e.g. the document hash and page count) and `constraints` (e.g. a deadline), as plain JSON. That is content, never authority. The compiler (#351, merged in) seals it through each node's canonicalPlan and planHash. - snapshotSubmission reads each node's inputs and constraints ONCE through @pcc/spec's copyPlanJson. The result is absent, an owned frozen copy, or the refusal reason. - submissionDigest v2 encodes execution JSON by its canonical form. Absent, {} and invalid all differ, and invalid evaluates to its reason, just as a non-primitive evaluates to NOT_DATA. - Invalid execution JSON is refused as { stage: "submission", reason: "invalid-execution-json", fields }. It names EVERY bad field in (nodeId, field) order, and it comes before any reservation read. - The compiler receives the owned copies; absent means {}. Tests: 5 new in the trace, 35/35 there and R10 47/47; the files typecheck. 8 mutation checks, all killed: not refused, dropped before the compiler, the digest ignoring it, a double read, unsorted problems, a reservation read before the refusal, absent colliding with {}, and constraints dropped. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…hows each unit's sealed execution (N25) This merges #356, which brings #351's agreementHash seal and the per-node execution contract. PlanPresentation's owned plan copy now includes every field the deal digest commits to, so its re-derived digest matches: - agreementHash; - each binding's planHash; - a field-by-field copy of its canonicalPlan, with inputs and constraints read once through copyPlanJson. Non-JSON is invalid (malformed-outcome) even inside a resealed plan. Additionally: - The binding check requires each canonicalPlan to name its own plan, node, job and unit (plan-binding). - Compiled and sealed nodes show `execution: {planHash, inputs, constraints}`, taken only from the intact, bound plan, never from the proposal. The deal shows agreementHash. - The seam's invalid-execution-json refusal renders as refused, with one code per bad field ("<field>:<reason>@<nodeId>"). - The trace test's tail conflict (both sides appended a describe block) is resolved by keeping both. Tests: trace 53/53 (4 new) and the files typecheck. 9 mutation checks, all killed: planHash or agreementHash not copied, the contract not bound (and only its node unbound), execution not shown, exec-JSON codes not rendered, execution JSON not validated, and the deal hiding agreementHash. That last one survived until a resealed-agreement test was added. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…act) into feat/agent-plans-route
…omics #3025, option b) Economics chose option (b), royalties ON TOP. With an agreement, a unit's gross is the agreement's unit gross, and the operator's own live quote (R10) is at most that gross. Economics holds the operator's legs to at least quote - floor(quote*feeBps/1e4) (OPERATOR_BELOW_QUOTE, #360 @c28bd34e) and needs the quote to do it. - AcceptedPlanNode.quoteBaseUnits (optional) is copied by the read-once snapshot. It must be a positive bigint no greater than the gross, else invalid-node-field "quoteBaseUnits". Absent means the quote IS the gross, as when there is no agreement. - SplitUnitInput.quote now matches economics' PlanSplitUnit. - Nothing new is sealed. The live quote is committed through matchedCapabilityDigest, and the grossed-up price through g, f, n and the payout legs. The golden vector and the digest are unchanged. Tests: 3 new, compiler 76/76, spec 886/886. 4 mutation checks, all killed: the quote not passed, not validated, above the gross, not snapshotted. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…ntation shows the economics refusal stage This merges #356's R15 binding, which also brings #351's quote to the splitter. Two presentation changes: - The refusal whitelist gains the seam's new "economics" stage. A missing unit gross, a gross below the quote, and an agreement refusal render as refused, Layer C, never a re-quote. The node is named, and economics' code is shown as a code. - The `execution` doc now says its inputs are the CALLER's content, sealed as what was agreed, not facts PCC verified: render them as data. This is the doc nuance noted after the self-review of #357. The trace's tail conflict (both sides appended a describe block) is resolved by keeping both. Tests: trace 58/58 (1 new) and the files typecheck. 2 mutation checks, both killed: the stage not whitelisted, the code not shown. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…ing) into feat/agent-plans-route
…ptedDealDigest hashes (amendment #3231) R13 stores the sealed deal as the digest's own preimage, so sha256(stored bytes) == consumed_deal_digest holds literally (steward condition (a), #3235). acceptedDealDigest is now sha256 of acceptedDealPreimage(plan). Behaviour and digest are unchanged; the golden vector still matches. Tests: 1 new (the digest is sha256 of the preimage's UTF-8 bytes; the preimage carries the settlement terms and each planHash). Spec 887/887. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…order, operator floor) An adversarial self-review of 9be1e8b (SHIP-WITH-FIXES) found one HIGH and several LOW gaps. Each fix has a test that fails without it. - plan-json, number policy D5 (HIGH, finding 1): - An integer outside +-(2^53-1) is refused as "unsafe-integer". VCR, the oracle and #359 refuse to hash one (the shared vector's policyD5), so sealing one sealed a planHash nobody could recompute. - A larger value must be sent as a decimal string. - plan-json: - Keys are visited in sorted order, so which refusal an input gets no longer depends on key insertion order (8). - The canonical-size bound is checked AS the copy grows, on a sound lower bound (string and key lengths), so an oversized input is refused before the rest is read (7). - The header now says symbol-keyed and non-enumerable properties are ignored, as JSON.stringify ignores them (10). - Compiler, evidence requirements (2, 5): - Each requirement must be an object with printable-ASCII ids and an integer tier 0..3. A number, boolean, bigint or symbol id is refused and never throws. - They are sealed sorted by (evidenceTypeId, tier, requirementId), so the order they are listed in cannot change the planHash or the deal. - Compiler, quotes and the operator floor (4): - A quote that differs from the gross is refused without an agreement (quote-without-agreement). - With an agreement, the legs to the node's payout address must carry at least the quote's net (operator-below-quote). Addresses are compared case-insensitively. - Compiler, diagnostics and the preimage (3b, 6): - The too-many-payout-legs count is clamped, so a lying length cannot put an unsafe integer into the diagnostics. - The preimage seals the carried planHash verbatim. It was case-folded; VCR compares planHash byte-exactly. - A test-file type error from 9be1e8b is fixed: a SplitUnitInput literal lacked quote. - execution-contract.vectors.json gains two sections. Every existing value is unchanged. - refusals: the D5 case. - edgeCases (F7): Unicode keys (UTF-16 code-unit order), JavaScript's shortest number forms and -0, pinned with the canonical text. Tests: spec 898/898, and the test files typecheck. 21 mutation checks, all killed, each by a failing test. Three first-pass gaps got tests before the run: the key-size floor, the tier tie-break, and case-insensitive payout-address matching. Findings 9 (cross-language string hazards) and 11 (planHash is unique only through planId) are INFO and are noted in the PR, not changed here. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…mc9-child-reservations pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…bounded before it is read (astra round 6) astra's round-6 review of 9d7686a returned SHIP-WITH-FIXES. Each finding: F (HIGH): operator-below-quote was bypassable by omission. The splitter received quoteBaseUnits ?? gross, but the floor check skipped an omitted quote. One helper, quoteOf, now feeds both paths, so an omitted quote is the gross everywhere. Under royalties on top, an omitted quote therefore leaves no room for a royalty. The royalty fixtures now carry an explicit quote below the gross, sized so the operator's share meets its floor exactly. B (MEDIUM): the key bound ran after Object.keys had read every key's descriptor, and keys were sorted before their lengths were checked. - The key list is now read once with Reflect.ownKeys, and its count of own string keys (enumerable or not) is checked before any descriptor read. - Key checks (reserved, too long, ill-formed) run before the sort, with a fixed priority, so the refusal never depends on insertion order. - The doc now states what an in-process proxy can still do, and that the size of a serialized input is bounded at the HTTP body limit. B (LOW): an inherited index could fill an array hole. Every element must now be the array's own. D (pre-existing): a non-array edges value became [], silently dropping the dependencies. It is now invalid-plan-field "edges"; an empty list is still "no dependencies". E: lone surrogates. Rather than depend on every consumer escaping them alike, plan JSON refuses ill-formed UTF-16 in values and keys ("ill-formed-string"). Well-formed surrogate pairs still pass. Tests: 6 new. Among them: astra's own numbers (an omitted quote and an explicit quote equal to the gross are refused identically, and the whole net passes both); a proxy counting descriptor reads (0 before the bound); non-enumerable keys counted; the priority in both insertion orders; lone surrogates in values and keys. Full spec suite: 904 passed, golden vectors unchanged. Mutations: 12 of 12 killed (4 on the compiler, 8 on plan-json). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
… one transaction, refuses a reused id, and the parser enforces assurance structure (astra, round 2 of R13) astra's review of ac3db65 (pack 45) returned DO-NOT-SHIP. Every finding was REPRODUCED first at 45d0cde (the review loop): 12 new tests fail there and pass here. A (HIGH): issue() re-read the caller's maxAmountBaseUnits for validation, both ceilings and the insert, so astra's getter (1n three times, then 100n) passed with 1 and inserted 100 USDC. consume() also re-read reservationId through its final UPDATE: checks on the caller's reservation consumed SOMEONE ELSE'S (reproduced), and the stored bytes could differ from those hashed. Every field, the nested parent reference included, is now read ONCE into an owned, frozen copy. B (HIGH): ensureBudgetReservationsSchema inspected, counted, dropped and created with no transaction. A committed row landing between the count and the drop was lost (reproduced by interleaving a second connection). It now runs as ONE immediate transaction. Compatibility is a RECORDED version (pcc_schema_versions, written by the DDL itself), never SQL text, which had refused a correct, differently written table (reproduced). C (MEDIUM): with recursive triggers off, INSERT OR REPLACE reopened a consumed reservation, and a NULL id was accepted. Both were reproduced. The insert guard now refuses a reused id, and id is NOT NULL (schema version 2). D (MEDIUM): parseSealedDeal accepted tier 2 with no program, tier 0 with a program, and two distinct programs. The store then honoured a forged tier-2 deal under a floor-2 reservation (reproduced). The parser now refuses all three as "bad-assurance", mirroring the compiler (v2: at most one program; per job from accepted-deal v3). Migration 0004 is regenerated from the runtime DDL, statement for statement. Not in this commit: the compiler accepts reclaimAt above uint64, which the parser refuses (reproduced). The fix belongs to #351, which has a queued pack; today the mismatch is fail-closed at consume. Tests: db 231, spec 912, gateway 3056 passed. The capture suites do not load without a built @pcc/verifier dist. Mutations: 10 of 10 killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
marked this pull request as ready for review
September 30, 2026 19:16
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
) #351 (59ee4ae, on master) holds an omitted quote to the gross floor: under an economics split, the legs to a node's own payout address must carry at least the net of its quote, and no quote means the whole gross. The three sealed-deal fixtures that use a split stated no quote, so on master they no longer compile (operator-below-quote). The lane's merge-order fold build found it: #402's own head passes 14/14, the fold fails 3. Each fixture now quotes exactly what its operator keeps: 9 of 10 USDC under the 10% royalty split (its net equals the operator's leg), and gross minus 15 under the 16-leg split. The assertions are unchanged; 14/14 both on this branch and on the fold with master's floor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
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.
Round 7 (2026-09-29): astra's round-6 findings fixed at 59ee4ae
astra's round-6 review of 9d7686a returned SHIP-WITH-FIXES. Every finding is fixed in one commit, 59ee4ae:
operator-below-quotewas bypassable by omitting the quotequoteOf(node, gross) = quoteBaseUnits ?? grossfeeds BOTH the splitter and the floor. An omitted quote is the gross everywhere, so it leaves no room for a royalty. The royalty fixtures now carry explicit quotes below the gross.Reflect.ownKeysis read once, and its count is checked before any descriptor read. Key checks run before the sort, with a fixed priority. The residual proxy and getter limits are documented.edgesbecame[]invalid-plan-fieldedges.Evidence. Full spec suite 904/904, with the golden vectors unchanged. Mutations: 12 of 12 killed.
The round-7 pack is
B-composition-351-r7-59ee4aef.md. It quotes the round-6 verdict verbatim, with one question per finding.Still deferred to accepted-deal v3 (steward #3590 and #3591): ONE program per JOB instead of per plan. The compiler's per-plan refusal and the commitment's single plan-level program change together with the commitment's shape.
Reconciliation row R12 (goal
pcc-reconciliation): the composition half of MS-01's accepted-deal compiler. This PR is a draft. The full external-plan path also needs:What it does
compileAcceptedPlan(input, { assertProgramForTier })takes a plan that an external agent wrote, after the server has re-read its providers and resolved its programs. It outputs two things: the deterministic structure the V-next encoder consumes, and the node → execution-unit map that VCR consumes. PCC does no decomposition here.Every guarantee is a check that fails closed with a typed violation:
PolicyIdentityhas exactly one operator, and that operator signs theJobPolicy(escrow's frozen ABI §2–3). A payee who isn't the operator never signs. So a plan with several operators compiles to one job per operator. It never becomes one escrow that binds one operator's signature over another operator's terms.unitsRootfolds the unit ids in array order. The order is a topological sort with ties broken by nodeId, so the order an LLM listed the nodes in cannot move a unit.f = floor(g·feeBps/10000)andn = g − f. Payouts must conserve exactly for each unit (R34===). No JSNumbertouches the money path.Σ gmust fit inside the server-issued reservation, in its currency, for this request.assertAcceptedProgramForTier, feat(spec): committed programs per CSD tier and the pre-funding assurance gate (must-close 6) #349). A missing hash is refused even if a permissive gate would approve it. With no gate injected, a non-zero tier is refused. Two different programs are refused under v3 rather than silently collapsed into one.acceptedDealDigestis sha256 over the canonical form of the entire compiled deal. That covers every unit field (tier, fee, gross/fee/net, payees, order), the signing operators, the payer, the reservation, the currency and its decimals, and both v3 roots. The v3compositionRootcommits the plan (contract, providers, prices, wallets, program) and is echoed into every unit. It does not commit the settlement terms: two deals that differ only in a tier, the fee or the signing operator share acompositionRootbut get differentacceptedDealDigests.SETTLEMENT_TOKEN_DECIMALS(USDC = 6), never from the caller. An unknown currency is refused.5 ≤ g ≤ 2^128−1feeBps ≤ 1000n > 0Economics split hook (af425ed, R12 ↔ R15)
Economics asked where
compileAcceptedPlanmeets itscompileEconomics(#2252). Composition adopts economics' own output shape:CompiledEconomics.units[]{unitRef, gross, fee, net, payouts[]}pluseconomicTermsHashandrightsTermsHash. It is injected asCompileDeps.splitNet, withunitRef= nodeId.The compiler calls the splitter once, in canonical plan order. It holds the answer to its own numbers and refuses, never repairs, when any of these hold:
floor(g·feeBps/10000)andn = g − f.Both terms hashes are sealed in
acceptedDealDigest, and so is economics'agreementHash(see the next section). All three are null without an agreement. Without a splitter, nothing changes: one leg, and the payout address receivesn.agreementHash is sealed (2de38d7, economics #2755)
Economics asked, after an adversarial review of #360 (@bad29552), that acceptance bind
agreementHash, not only the two terms hashes. The terms hashes do not cover the agreement's envelope (asOf, version, deadline), so sealing only them would leaveasOfmutable after acceptance.NetSplitResultok requiresagreementHash. It is now structurally identical to economics'PlanSplitResult.snapshotSplitreads it exactly once, with the other scalars.economics-malformed "agreement-hash"; a throwing getter is"result". It is never a throw.CompiledAcceptedPlan.agreementHashis lowercased, or null without an agreement. It is in the digest preimage.PCC:accepted-deal:v2, because the preimage schema changed. Nothing is persisted under v1: the R13 store is not built, and VCR carries the digest as opaque bytes.8 new tests. 8 mutation checks, each caught by a failing test.
Hardening from cross-family review of the split hook (4479b79, ce18cf4)
Round 1: DO-NOT-SHIP. A
recipientgetter could grow the payouts array past the 16-leg cap. A hash getter could return one value to the check and another to the use. Throwing getters and proxies threw out of the compiler.Round 2 closed all six findings. astra withdrew its "
$admits\n" claim, which is correct for JavaScript. It found one new escape: an answer getter, or the program gate, could mutate the caller's input through a shared reference after validation.Round 3: DO-NOT-SHIP. The general guarantee was still open:
callable objects and non-array containers kept caller references
the catch could throw on a hostile thrown value
the gate's answer was read unprotected
Fix (0571c00): every structurally invalid value becomes a frozen compiler-owned sentinel (
NOT_DATA), so no caller reference survives. A bound is recognized by the identity of a token the compiler owns. The gate's answer is read once, defensively.New test: a deep read-counting proxy proves every input property is read at most once, and that the result equals the plain compile.
Round 4: the round-3 findings are CLOSED. It blocked on the
depscallbacks being read twice: a callback could swap a dependency between reads.Round 5: SHIP. No validation/use mismatch, retained caller reference or escaping exception remains beyond the stated server faults (the gate or splitter function throwing), and there are no material test gaps. Its one nonblocking suggestion is applied in 69012b4.
N25: the deal seals each node's execution contract (6060331)
VCR recomputes
planHash = sha256(canonicalize(canonicalPlan))from the plan bytes it receives, and requires it to equal the node's planHash sealed inacceptedDealDigest(VCR #2300 and #2332). Before this, the deal sealed money and assurance but not WHICH document gets printed.plan-json.ts. A node'sinputsandconstraintsare the caller's plain-JSON CONTENT: WHAT runs, never who is paid. The copy is exactly JSON and bounded (depth, keys, lengths, values, 8 KiB canonical) and reads every property once. Bad JSON is refused asinvalid-execution-jsonfor every bad field, independent of node order.canonical-plan.ts. VCR's per-node contract:planHash is byte-exact against VCR's crossrepo-accepted-bundle-v1, including its one-byte mutation.
Sealing. Each
NodeUnitBindingcarriescanonicalPlan+planHash. The preimage (domainPCC:accepted-deal:v2) seals the carried planHash AND a hash recomputed from the carried content, so the consumer's recompute-and-compare catches tampering of either.Golden vector for VCR:
execution-contract.vectors.json(the deal, canonicalPlans, planHashes, digest and 3 mutations). A copy is at/mnt/sparkbulk/pcc-reconciliation/vectors/composition-accepted-deal-v2.json.13 tests; 25 mutation checks, all killed.
Economics option (b), royalties on top (9be1e8b, economics #3025)
With an agreement, a unit's gross is the agreement's, and the operator's live quote is at most that gross. An optional per-node
quoteBaseUnitsis read once and must be at most the gross; absent means quote = gross. It is passed to the splitter asquote, so economics can hold the operator's legs to at leastquote - floor(quote*feeBps/1e4). Nothing new is sealed: the live quote is committed throughmatchedCapabilityDigest, and the grossed-up price through g/f/n and the legs. The golden vector is unchanged. 3 tests; 4 mutation checks, all killed.Cross-family status: round 6 (agreementHash + N25 + quote, now including the 9d7686a self-review fixes below) goes through the operator's ChatGPT review as pack
B-composition-351-r6c-9d7686af.md.Self-review fixes (9d7686a)
An adversarial self-review of 9be1e8b (same model family, so it does not replace the cross-family round; its verdict was SHIP-WITH-FIXES) found one HIGH and several LOW gaps. Each fix has a test that fails without it.
unsafe-integer— VCR, the oracle and fix(spec): canonicalize refuses values that have no JSON form #359 refuse to hash one (the shared vector's policyD5), so sealing one sealed a planHash nobody could recompute. A larger value must be sent as a decimal string.JSON.stringifyignores them (finding 10).quote-without-agreement). With an agreement, the legs to the node's payout address must carry at least the quote's net (operator-below-quote). Addresses are compared case-insensitively.SplitUnitInputliteral lackedquote.execution-contract.vectors.jsongains two sections; every existing value is unchanged:refusals(the D5 case) andedgeCases/F7 (Unicode keys in UTF-16 code-unit order, JavaScript's shortest number forms, and-0).Findings 9 (cross-language string hazards) and 11 (planHash is unique only through planId) are INFO and are noted in the PR, not changed here.
Also in this head (79b6c1d, amendment #3231):
acceptedDealPreimageis now exported — the canonical bytesacceptedDealDigesthashes. R13 stores the sealed deal as the digest's own preimage, sosha256(stored bytes) == consumed_deal_digestholds literally (steward condition (a), #3235).acceptedDealDigestis nowsha256ofacceptedDealPreimage(plan). Behaviour and digest are unchanged; the golden vector still matches.Consumer contract (R13 / R14 must honor this; flagged by review)
acceptedDealDigestfrom the compiled plan it is about to encode, compare it to the digest it seals, and do both in the same atomic step that consumes the reservation. Carrying the digest along, or authorizing bycompositionRootalone, would reopen the "different deals, same root" hole.prePolicyRootthen binds those units.ReservationRefmust come from the durable store. The route (R9) loads it from the store by id (R13). It is never taken from a receipt the caller presents.Not in this PR (by design)
canonicalPlanemitter. Its shape is still to be pinned with VCR. This PR emits the node→unit binding, including thestepIdstring andkeccak256(utf8(stepId)).Cross-family review
sol + astra on 44fd8d1: DO-NOT-SHIP. Findings:
Fixed in fb68b1d.
astra's confirmation of fb68b1d: SHIP-WITH-FIXES. Findings 1–3 are CLOSED. Finding 4 was still open: when two nodes shared an id, which violations got reported depended on which copy came first. Astra also found a robustness gap: a non-string operator threw a
TypeErrorbefore the rejection was returned. Both are fixed in e2100a3.Coverage. The reviewer found no settlement-significant compiled field that can change without changing the digest.
Rounds 3-5 (astra), on the read-once input snapshot and dependency capture. Round 5 on 2c6324d returned SHIP; 69012b4 added its one nonblocking suggestion.
Round 6, at 9d7686a (agreementHash, N25, the quote, the preimage export and the self-review fixes): goes through the operator's ChatGPT review as pack
B-composition-351-r6c-9d7686af.md.Tests
76 tests in
packages/spec/src/csd/accepted-plan-compiler.test.ts, plus 13 inexecution-contract.test.ts. They include the required negatives:Full
@pcc/spec: 898/898 at 9d7686a (887/887 after 79b6c1d's preimage export; 886/886 before it).tsc --noEmitis clean, for the package and for the test files.Mutation checks. Each of these mutations turns a test red:
The four tests added for astra's confirmation each fail against fb68b1d.
9d7686a self-review: 21 mutation checks, all killed, each by a failing test. Three first-pass gaps got tests before the run: the key-size floor, the tier tie-break, and case-insensitive payout-address matching. A test-file type error from 9be1e8b — a
SplitUnitInputliteral missingquote— is fixed in this commit.Reconciliation note:
/mnt/sparkbulk/pcc-reconciliation/returns/pcc-composition-reconciliation-note.md(findings A–D).🤖 Generated with Claude Code
https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz