Skip to content

feat(spec): accepted-plan compiler — composition half of MS-01 (R12) - #351

Merged
LamaSu merged 15 commits into
masterfrom
feat/accepted-plan-compiler
Sep 30, 2026
Merged

LamaSu merged 15 commits into
masterfrom
feat/accepted-plan-compiler

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

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:

Finding Fix
F (HIGH) operator-below-quote was bypassable by omitting the quote One quoteOf(node, gross) = quoteBaseUnits ?? gross feeds 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.
B (MEDIUM) the key list was enumerated before the bound, and keys sorted before their lengths were checked Reflect.ownKeys is 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.
B (LOW) an inherited index could fill an array hole Every element must be the array's own.
D (pre-existing) a non-array edges became [] It is refused as invalid-plan-field edges.
E test gaps, including lone surrogates The tests are added. Plan JSON refuses ill-formed UTF-16 in values and keys.

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:

  • R10: live provider re-read
  • R13: one-use reservations. The schema is an operator decision (bus #2240).
  • escrow's R14 canonical V-next compiler. This PR deliberately does not duplicate it.

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:

  • One V-next job per operator. PolicyIdentity has exactly one operator, and that operator signs the JobPolicy (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.
  • Canonical unit order. Unit order is money-significant, because unitsRoot folds 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.
  • Exact bigint economics. f = floor(g·feeBps/10000) and n = g − f. Payouts must conserve exactly for each unit (R34 ===). No JS Number touches the money path.
  • Bounded authority. Σ g must fit inside the server-issued reservation, in its currency, for this request.
  • Assurance. A non-zero tier needs a program hash, and the hash must pass the injected program gate (evidence's 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.
  • A sealed deal. acceptedDealDigest is 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 v3 compositionRoot commits 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 a compositionRoot but get different acceptedDealDigests.
  • Server-resolved decimals. Decimals come from SETTLEMENT_TOKEN_DECIMALS (USDC = 6), never from the caller. An unknown currency is refused.
  • Typed rejection. Malformed runtime input is refused with a typed violation, never with a throw or a coercion. That includes a non-string id and an array that stringifies to a valid id, currency or tier. Every node occurrence is validated, including duplicates, and the violations are sorted. So the diagnostics depend neither on input order nor on which duplicate came first.
  • Frozen-ABI limits.
    • at most 16 units per job
    • 5 ≤ g ≤ 2^128−1
    • feeBps ≤ 1000
    • n > 0
    • operator ≠ payer
    • no zero addresses

Economics split hook (af425ed, R12 ↔ R15)

Economics asked where compileAcceptedPlan meets its compileEconomics (#2252). Composition adopts economics' own output shape: CompiledEconomics.units[] {unitRef, gross, fee, net, payouts[]} plus economicTermsHash and rightsTermsHash. It is injected as CompileDeps.splitNet, with unitRef = 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:

  • The unit set is not exactly the plan's.
  • gross/fee/net differ from floor(g·feeBps/10000) and n = g − f.
  • A unit has fewer than 1 or more than 16 legs.
  • A leg goes to the zero address, or has an amount that is not a positive integer base-unit string.
  • A unit's legs don't conserve exactly. Every such unit is reported, not just the first.

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 receives n.

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 leave asOf mutable after acceptance.

  • NetSplitResult ok 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 is lowercased, or null without an agreement. It is in the digest preimage.
  • The domain moves to 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.
  • 3 new tests: sealed alone, required, and read once. Six mutation checks are 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 the PR after its round-5 SHIP, so round 6 goes back to cross-family review, queued with coord-watch.

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 recipient getter 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.

  • Fix: the economics answer is copied into plain data, reading every property and every length once, inside one catch boundary. A throwing splitter still propagates as a server fault.

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.

  • Fix: the whole input is snapshotted into frozen, compiler-owned data before anything runs. Validation, the gate, the splitter, unit construction and the digest see only that copy.
  • Bounds: 1024 nodes, 4096 edges, 64 evidence requirements per node.

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 deps callbacks being read twice: a callback could swap a dependency between reads.

  • Fix (2c6324d): both callbacks are read once, before any input. A present-but-non-function slot fails closed. The answer's scalars are normalized.

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 in acceptedDealDigest (VCR #2300 and #2332). Before this, the deal sealed money and assurance but not WHICH document gets printed.

  • plan-json.ts. A node's inputs and constraints are 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 as invalid-execution-json for every bad field, independent of node order.

  • canonical-plan.ts. VCR's per-node contract:

    • schema, plan and node id, capability (type, id, CSD, digest);
    • operator, payTo;
    • the exact amount in base units;
    • job and unit;
    • assurance (tier, program, evidence);
    • inputs and constraints.

    planHash is byte-exact against VCR's crossrepo-accepted-bundle-v1, including its one-byte mutation.

  • Sealing. Each NodeUnitBinding carries canonicalPlan + planHash. The preimage (domain PCC: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 quoteBaseUnits is read once and must be at most the gross; absent means quote = gross. It is passed to the splitter as quote, so economics can hold the operator's legs to at least quote - floor(quote*feeBps/1e4). Nothing new is sealed: the live quote is committed through matchedCapabilityDigest, 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.

  • D5 (HIGH), plan-json number policy. An integer outside ±(2^53−1) is refused as 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.
  • plan-json. Keys are visited in sorted order, so which refusal an input gets no longer depends on key insertion order (finding 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 (finding 7). The header now says symbol-keyed and non-enumerable properties are ignored, as JSON.stringify ignores them (finding 10).
  • Compiler, evidence requirements (findings 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 (finding 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 (findings 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, and 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) and edgeCases/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): acceptedDealPreimage is now exported — the canonical bytes acceptedDealDigest hashes. 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.

Consumer contract (R13 / R14 must honor this; flagged by review)

  • The reservation consume must recompute and compare. It must recompute acceptedDealDigest from 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 by compositionRoot alone, would reopen the "different deals, same root" hole.
  • The encoder must encode exactly the sealed units. On-chain, each job's prePolicyRoot then binds those units.
  • The ReservationRef must 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)

  • ABI encoding (escrow R14)
  • reservation issue and consume (the R13 store)
  • provider re-read (R10)
  • the HTTP route (R9, which needs R28 scopes)
  • a VCR canonicalPlan emitter. Its shape is still to be pinned with VCR. This PR emits the node→unit binding, including the stepId string and keccak256(utf8(stepId)).

Cross-family review

  1. sol + astra on 44fd8d1: DO-NOT-SHIP. Findings:

    • the plan root didn't bind tier, fee or signing operator
    • caller-controlled decimals
    • a null program on a non-zero tier got past a permissive gate
    • input-ordered diagnostics

    Fixed in fb68b1d.

  2. 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 TypeError before the rejection was returned. Both are fixed in e2100a3.

  3. Coverage. The reviewer found no settlement-significant compiled field that can change without changing the digest.

  4. 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.

  5. 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 in execution-contract.test.ts. They include the required negatives:

    • obligation over the reservation
    • a reservation for another request or currency
    • an unapproved program; a missing gate; a program on tier 0; mixed programs; a null program on a non-zero tier
    • a duplicate node (no double obligation, order-independent diagnostics)
    • a cycle, self-loop, unknown edge or duplicate edge
    • more than 16 units per operator
    • operator == payer
    • fee rules; gross bounds
    • malformed and coerced input
    • permutation and idempotence determinism
    • each settlement term (tier in isolation, fee, operator, decimals) changes the sealed digest
  • Full @pcc/spec: 898/898 at 9d7686a (887/887 after 79b6c1d's preimage export; 886/886 before it). tsc --noEmit is clean, for the package and for the test files.

  • Mutation checks. Each of these mutations turns a test red:

    • removing the reservation cap
    • skipping the missing-gate refusal
    • reversing the tie-break
    • rounding the fee up
    • dropping the tier from the deal digest

    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 SplitUnitInput literal missing quote — 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

LamaSu and others added 3 commits September 24, 2026 05:23
…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
…<-> 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
…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
…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
…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
…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
…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
…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
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
…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
…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
LamaSu and others added 2 commits September 24, 2026 16:23
…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
LamaSu marked this pull request as ready for review September 30, 2026 19:16
@LamaSu
LamaSu merged commit d4d167d into master Sep 30, 2026
5 checks passed
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
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