Repository navigation
feat(gateway): PlanPresentation read model for caller-authored plans (product section 5) - #357
Merged
Merged
Conversation
… trace (reconciliation R9, pure part)
acceptExternalPlan(submission, { principal, tenantId }, deps) chains
R10 (live re-read) -> R11 (server-resolved programs + evidence's gate) ->
R12 (compileAcceptedPlan) with every read injected, so the accept decision
is a pure function of (submission, principal, live state). The agent
supplies only the plan's shape and what it believes each node costs; every
settlement term is the server's: operator and payout, gross, currency,
program, evidence requirements, payer (from the reservation), fee and
reclaim time (policy), and the plan id itself (derived from the reservation,
so a caller cannot pick job ids that collide with another plan's).
Refusals are typed by stage: submission, reservation (not-found / not-issued
/ expired / wrong-principal / wrong-request), revalidation (the non-current
verdicts, stale ones carrying the re-quote), currency, program (claimed
mismatch), evidence (no contract for the tier), compile.
external-plan-trace.test.ts is the return contract's end-to-end trace:
agent DAG (listed mail-first) -> accepted deal (2 V-next jobs, one per
operator, canonical order, bigint economics, sealed digest) -> reservation
consumed exactly once (a labelled R13 STAND-IN implementing the consume
protocol: re-check, RECOMPUTE the digest, consume once, seal) -> V-next
units ready (conservation, step ids, root, reclaimAt). Plus the charter's
negatives through the whole seam: forged cheaper snapshot, unapproved
program, expired reservation, principal/request A used for B, obligation
over reservation, double consume (exactly one), duplicate node, a plan
altered after acceptance (digest-mismatch at consume), caller-chosen
planId/payout ignored, currency and evidence refusals, malformed input.
PCC_TRACE_OUT=<path> writes the happy-path trace as JSON.
13 tests; 7 mutation checks (principal, expiry, node currency, program
cross-check, caller planId, missing evidence, request) each caught. Scoped
tsc clean. Stand-ins: in-memory live rows, program registry + gate (#349
draft), CSD tier -> evidence map (evidence owns it), R13 store (#2240).
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
… agent's plan (R9)
Product pack section 5 ("SERVER COMPOSER") and ledger R9: the server
composer is one optional planner, never canonical. The new
submissionFromComposeResponse() only restates a ComposeResponse as the
claims an agent would submit (nodes step-<index>, dependsOn -> edges, the
composer's operator/kernel/type/tier, and its float estimatedPriceUSD
verbatim). R10 then re-reads and compares those claims like anyone's, so a
composer that drifted from the live rows gets `stale` + a re-quote (or
`invalid-claim` for an exponent-form float), never a deal on its own say.
Non-proposed or expired proposals, dangling dependencies and duplicate
step indices are refused before the seam.
Tests: 3 new in external-plan-trace.test.ts (16 total there): accepted
through the ordinary seam; float drift -> stale; 1e-7 -> malformed claim;
expired / over_budget / dangling dep / duplicate index refused. 4 mutation
checks (expiry, status, dangling dep, duplicate index) each caught. 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
Astra's cross-family review of #356: SHIP-WITH-FIXES. It confirmed the authority chain: payer from the reservation; operator and payout from the live kernel; gross, currency and program from the server; the reservationId is only a lookup key, which satisfies the charter's "no caller reservation id as authority". Fixed: - Tier: the tier is the principal's purchase choice, constrained by the live offer. A reservation may now carry `minTier`, a floor the payer set (invariant 11). A delegate spending it cannot downgrade assurance (new refusal stage `tier`). The header says so instead of "every term is the server's". - Compose adapter: JSON-shaped values whose coercion throws (status, expiresAt or assuranceTier with a null toString) crashed it. Every field's type is now checked before any coercion, and a string tier "2" no longer becomes "tier2". The validation boundary is explicit: structure here, meaning in R10. - Seam: an empty or missing principal matches nothing. The clock is floored (a `Date.now()/1000` clock made BigInt throw). A non-finite clock throws rather than compare as "not expired". - R13 stand-in consume contract, which was overstated: it now re-checks plan id and EACH job's payer, and DERIVES the obligation from the units. It documents that a recomputed digest proves integrity, not authority (a resealed plan with another payer is refused). Tests: 23 in external-plan-trace.test.ts (7 new). The fix-driven ones fail against f2d74b3. New negatives: consume-time expiry / principal / resealed payer / understated total / foreign plan id; a resolved program the gate rejects; tier below the reservation minimum; nested malformed nodes and edges; adapter coercion crashes. Mutation checks: seam 10/10 caught, adapter 6/6 caught. 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
…(product pack section 5, item 1)
presentPlan({ submission, outcome?, sealedDigest?, asOf }) builds the
typed read model a UI or an agent renders for a caller-authored plan.
It uses only server truth: the submission as proposed, R10's verdicts,
the seam's refusal or compiled deal, and the reservation store's seal. It
is separate from the settlement encoding (nothing here is hashed, signed
or funded) and offers no way to accept (item 7).
- Authority is server-assigned. `layer` is "B" only when the reservation
store sealed EXACTLY this plan's acceptedDealDigest. Compiled but not
sealed is still "C". No field in the submission can raise it.
- Stable node identity (item 2): the caller's node id is carried through
proposed -> current | stale (diffs + live re-quote) | refused ->
compiled -> sealed, and into the execution unit (job, milestone,
stepId).
- Typed states (item 3); server snapshots plus an asOf quote timestamp
(item 4); the FieldDiff object (item 5); a preview (item 6) with exact
gross/fee/net, payouts by recipient, tier per node and reclaimAt, all
money as base-unit strings; the deal's commitments with a `sealed` flag
(item 8).
- The seam now returns every R10 verdict on each outcome after R10 runs
(additive), so the model has live terms for every node.
Tests: 8 new in external-plan-trace.test.ts (31 there). 6 mutation
checks (layer B without a seal, any sealed digest accepted, refusal shown
as a re-quote, diffs dropped, money as a float, unsafe coercion) each
caught. 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
…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
… 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
# Conflicts: # packages/gateway/src/__tests__/external-plan-trace.test.ts # packages/gateway/src/services/external-plan-seam.ts
… input; never throws (astra review of #357) Astra's first review of #357: DO-NOT-SHIP. The normal path was sound, but the builder did not enforce its own contract: - A genuine sealed outcome paired with ANOTHER submission (same node ids, different capability, request, reservation or edges) showed the replacement as Layer B. - The seal was two supplied strings compared. A plan altered after sealing with its old digest kept still passed, and the seal named no reservation. - A forged verdict status could mark a node "sealed". An empty refusal read as a re-quote. reclaimAt was fabricated as "0". Many malformed outcomes threw. Now: - The outcome must carry the seam's submissionDigest of THIS submission, or the result is "invalid: submission-mismatch". - The plan is copied in full (every field the deal digest commits to), and acceptedDealDigest is RE-DERIVED from that copy. Otherwise the result is "invalid: plan-integrity". The plan must also be bound to the submission's request, reservation, plan id and exact node set, with every binding naming its own unit; otherwise "invalid: plan-binding". - The seal is a record {reservationId, acceptedDealDigest} that must name this reservation and exactly this digest before Layer B. - Verdict statuses are whitelisted. Re-quote requires a non-empty, all-stale refusal. Units must share one reclaimAt (no fabrication). - Every input is read once. Any malformed input is state "invalid", Layer C, with no nodes, money or deal shown (never a mix). The builder never throws. Tests: 44 in external-plan-trace.test.ts (6 new or reworked): submission-mismatch (capability, edges, reservation), post-seal alteration, a re-sealed plan bound elsewhere, uneven reclaim times, forged statuses (plan and refusal), malformed verdicts/outcomes/amounts, a missing submission digest, throwing getters, an empty refusal, a wrong-reservation seal, a non-string asOf. 11 mutation checks each caught (8 new plus 3 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
…o feat/external-plan-seam
…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
…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
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…valid execution JSON is 400 and consumes nothing Merges #357 first (commit "merge: bring #357's N25 presentation..."), so this branch carries the whole N25 path. A test pins the route's view of it: - A 200 plan carries each node's canonicalPlan with the agent's inputs and its planHash. - The Layer-B presentation shows the same execution, and the store sealed exactly that digest. - A non-object inputs, or one beyond the key bound (both sendable over HTTP), gets 400 with the seam's refusal naming the field. Nothing is consumed. Route tests 17/17. Full gateway suite 3043 passed, 6 skipped. The known capture suites don't load (unbuilt verifier dist), and completion-real-tier timed out at load average ~20 but passes alone (3/3). 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
…on b: royalties on top)
SeamDeps.economics is optional and present only when an agreement applies.
It is { unitGross, splitNet }: economics' agreementUnitGross and
netSplitterFor, bound per request by the route to the agreement and the
server's facts.
- It is read once with every other dependency. Anything else is the
defined wiring TypeError. Both functions are called with the binding as
their receiver.
- The agreement's gross map is read once into owned data; bigints only.
- For each node, the gross must exist and must cover the operator's live
quote from R10. Otherwise it is a typed refusal at the new "economics"
stage: agreement-refused (with economics' code), agreement-unreadable,
unit-gross-missing or quote-not-covered, all before the compile.
- The compiler receives gross = the agreement's, quoteBaseUnits = the live
quote, and splitNet. So the reservation must cover the GROSSED-UP total,
and economics can hold the operator's legs to its quote floor.
- Without an agreement nothing changes: one leg, gross = the quote.
Tests: 4 new, trace 39/39, and the files typecheck. 8 mutation checks, all
killed.
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
…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
…before reading the submission (astra, round 2 of #356) astra's review of a16095a (pack B-composition-356-r2b) returned DO-NOT-SHIP under the in-process getter and callback threat model. A (authority): only the REFERENCE to deps.revalidation was captured before the submission was read. A submission getter or a reservation callback could then swap loadCapabilities, loadKernels or csdForType, and R10 would authenticate the caller's own terms against fabricated rows. The three callables are now captured and validated with every other dependency, before any submission property is read. R10 gets only a frozen object of pinned wrappers that call them with their original receiver. C (authority): ctx.principal and ctx.tenantId were copied AFTER the submission was read, so a getter could make the caller the reservation's owner, or switch its tenant. The context is now read once, before the submission. The checks, and the order of refusals, are unchanged. B (digest): tag() encoded a symbol and NOT_DATA alike, and JSON encoded -0 as 0. leaf() now turns a symbol into NOT_DATA and -0 into 0, so the snapshot never keeps two values that encode alike. Both are semantics-preserving. Tests: 7 new. - A: a submission getter, and a reservation callback, swapping all three R10 dependencies both give the baseline outcome. - A: a non-function R10 callable is a wiring fault raised before any submission property is read. - A: method-style (class) loaders keep their receiver. - C: a getter making the caller the owner is still wrong-principal. - C: a getter switching the tenant gives the same refusal as the original tenant (with a control that the tenant matters). - B: symbols and objects snapshot to one NOT_DATA, -0 to 0, and distinct values keep distinct digests. Full gateway suite: 3020 passed, 6 skipped. The capture suites do not load without a built @pcc/verifier dist. Mutations: 6 of 6 killed. S6 (callables not validated) first survived; that exposed a missing test, which was then added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…code (#356, review round 3) The seam now takes an owned copy of the submission before reading any field: only plain objects and real arrays are walked, through their own data properties, with the Proxy check (which invokes no trap) first. A Proxy or an accessor anywhere refuses the submission as malformed; an object or array with another prototype is never read. The authenticated context's principal and tenant are read the same way. The copy is bounded in depth, value count and list length. The header states the trust boundary: caller inputs are data, the dependencies are trusted server wiring. Tests: a submission getter that rewrites a method-style loader's state, a Proxy submission, getters in node fields, execution inputs and arrays, a class instance, a context accessor, the three bounds and a non-plain array. The earlier read-once tests now assert that the getter never runs. 11 of 11 mutants killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…recondition (#356, review round 4) astra's round-4 review of 8990a11 (SHIP-WITH-FIXES) found one MEDIUM: the no-code copy skipped symbol keys without counting them, and it bounds nothing about an object's width before Reflect.ownKeys enumerates it. That was reproduced first. At 8990a11, a submission with a symbol-keyed input, or with more than 1,000,000 symbol keys, was accepted (2 failing tests). A symbol key now refuses the submission, like an accessor or a Proxy: JSON cannot produce one, and a skipped key goes uncounted. The header now states the size bound as the seam's precondition. The submission is parsed from an HTTP body the gateway caps at 1 MiB (server.ts bodyLimit), and JavaScript cannot count an object's own keys without materializing them, so the width bound lives with the caller. A test also pins the string-key variant, which the value bound refuses. Tests: external-plan-trace 60/60. Mutation: symbol keys skipped again is killed (2 tests fail). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…lanHash, binding and unit (#357, review round 2b) M1 (astra, #357 round 2b): planIsBound checked only identity (plan, node, job, milestone), so a contract edited under its old planHash, with the deal digest recomputed and sealed, still reached Layer B showing a planHash that its displayed contract does not hash to. planIsBound now also requires, per node: - planHash equals planHashOf(canonicalPlan); - the contract's step, operator, tier and committed program equal the binding's (addresses and hashes compare lowercased, since hex case carries no meaning and the compiler lowercases the contract); - the contract's amount equals the unit's gross as the exact canonical base-unit string, and its currency and decimals equal the deal's. A mismatch is a plan-binding failure: Layer C, an empty invalid presentation. Tests: the reviewer's reproduction through the stand-in store protocol, plus one test per added equality (each recomputes the planHash and the digest so only that field disagrees), the hex-case pins for operator and program, and a resealed-unchanged control. All failed at 1b5e77e. Agent: implementer-india Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
… a refusal's two lists must agree (#357, review round 2b) M2 (astra, #357 round 2b): the checks required only that every verdict named some submitted node and, with a plan, that the count matched. Two current verdicts for node A could replace A+B (node B was then shown compiled or sealed without its own live verdict), and for a refusal the top-level list drove node rendering while the nested list drove the re-quote state, with no agreement check. presentPlan now requires: - an outcome that carries verdicts has one per submitted node: every submitted id has a verdict and no id has more verdicts than nodes were submitted under it (R10 gives a readable id submitted twice one verdict, and an unreadable id one verdict per claim, so the bound is the submitted count); - for a refusal, the nested verdicts equal the top-level verdicts that are not current: the same nodes with the same statuses (the seam builds the nested list as that subset, so this is the exact-agreement rule that honest seam output satisfies); an absent nested list is an empty one. The top-level list is the only source of node states; the nested-only fallback is gone. Tests: two copies of one verdict (the reviewer's case), a node without a verdict (refusal and plan), five disagreement shapes plus a left-out node, and pins that the seam's own refusals still present (mixed refusal, a node submitted twice, two nodes sharing an unreadable id, a pre-R10 refusal). All the reproductions failed at 1b5e77e. Agent: implementer-india Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…sal agreement check (#357, review round 2b) M2 follow-up. Mutation analysis of the M2 change found two survivors: - dropping the per-id upper bound of the cover rule (membership plus coverage only) survived, because two copies of one verdict always leave the other node uncovered; a refusal where every node has a verdict and one has two, the nested list repeating it, now distinguishes them; - comparing the two verdict lists by status only survived; the right status on the wrong node now distinguishes it. Tests only; no source change. Agent: implementer-india Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…ment deal (#357, review round 2b) M1 compares the contract's amount with the unit's gross. Under an economics agreement (royalties on top) that gross is the agreement's and legitimately exceeds the operator's live quote, so the check must use the unit's gross and never the quote. This test builds a real agreement deal through the seam, shows it still presents as sealed, and shows a contract that names the quote instead of the gross is refused. Tests only; no source change. Agent: implementer-india 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
Oct 1, 2026
…t-intents #432 is based on #357. This brings in 3475654 (M1: the execution contract is bound to its planHash, binding and unit), 22fe64d (M2: verdicts are an exact cover of the nodes and a refusal's two lists agree) and 2a6a71a (their mutation-driven test additions). No conflicts. Agent: implementer-india 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
Oct 1, 2026
…ents Merges 384e097 (a test pinning that M1 keeps presenting an economics-agreement deal). Tests only on the #357 side; no conflicts. Agent: implementer-india 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
Oct 1, 2026
…view-completeness #434 is based on #357. This brings in 3475654 (M1: the execution contract is bound to its planHash, binding and unit), 22fe64d (M2: verdicts are an exact cover of the nodes and a refusal's two lists agree) and 2a6a71a (their mutation-driven test additions). The merge is automatic; the two branches' hunks in plan-presentation.ts and in external-plan-trace.test.ts do not overlap. Agent: implementer-india 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
Oct 1, 2026
…ed (#434, MEDIUM: duplicate case) #434 (astra r1, MEDIUM), first half: replacing both verdicts of an accepted outcome with the same current verdict passed validation and stayed Layer B/sealed at 6248cf9 (the reviewer's second case; reproduced there). The merge-up of #357's M2 (22fe64d, an exact one-to-one cover of the nodes) already refuses it as a malformed outcome, so no source change is needed here. This test pins it on this branch. Agent: implementer-india 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
Oct 1, 2026
…completeness Merges 384e097 (a test pinning that M1 keeps presenting an economics-agreement deal). Tests only on the #357 side; no conflicts. On this branch the same deal also passes the live-terms check. Agent: implementer-india Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…ion (#357) Pack 133 passed as #356's confirmation (steward #5847), which releases the held merge-up of the accepted-plan stack. A merge-tree simulation of the whole stack over master found one conflict: this one. The conflict was in packages/gateway/src/__tests__/external-plan-trace.test.ts: #357 and #356 each appended a describe block at the end of the file. Both are kept, #357's block first, closed explicitly, then #356's. The seam source (services/external-plan-seam.ts) merged without conflict. Results: external-plan-trace 93/93. Full gateway suite: 3121 passed, 6 skipped. tsc is clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
merged; plain merge, no edits) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…after #355 merged; plain merge, no edits) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…am (plain merge, no edits; fresh CI on the green master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…resentation (plain merge, no edits; fresh CI on the green master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
marked this pull request as ready for review
October 4, 2026 02:20
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…e, no edits; #357 landed, retarget to master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ain merge, no edits; #357 landed, retarget to master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…e, no edits; #357 landed, retarget to master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…ed once canonicalize refuses it (D5); either order #359's D5 (canonicalize refuses NaN, a Date and other content with no JSON form) met #357's external-plan-trace test: its reseal() helper recomputed acceptedDealDigest over a plan whose inputs were NaN or a Date, which now throws NonCanonicalValueError. Product code is unaffected: presentPlan's own never-a-throw check passes, and the seam refuses non-JSON inputs at submission. The test now reseals inside try/catch. Where canonicalize refuses (D5) it asserts NonCanonicalValueError, since the sealing itself refuses, which is stronger; otherwise it asserts malformed-outcome as before. 93/93 with D5 and 93/93 without it. Approved by the steward (#6532) to ride on #359, which lands second. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
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.
Product pack section 5, item 1 (goal
pcc-reconciliation): the PlanPresentation read model for caller-authored plans.Stacked on #356 (the seam), which is stacked on #355 (R10) and #351 (R12).
What it is
presentPlan({ submission, outcome?, sealedDigest?, asOf })is the typed read model a UI or an agent renders. It is built only from server truth:It is separate from the settlement encoding: nothing here is hashed, signed or funded. It offers no way to accept (item 7); acceptance is the seam plus the reservation consume.
layer: "B"only when the reservation store sealed exactly this plan'sacceptedDealDigest. A plan that is compiled but not yet sealed is still"C", and no field in a submission can raise it.stepIdasOfquote timestamp (item 4).FieldDifffor changes (item 5).reclaimAt. All money is base-unit strings, never floats.sealedflag (item 8).The seam change is additive: it now returns every R10 verdict on each outcome after R10 runs, so the model has live terms for every node.
Reuse check: PR #307 (
feat/plan-card@ af7467a, the status board's reuse candidate for PX-8)toPlanCardprojects the server decomposer'sDecompositionResultfor the Runtype widget. I reviewed it.Carried over:
proposed(Layer C), and only R10's live price is the server's.stateandlayercome from the seam and the seal, not from the content.Not reusable for the accepted-plan path:
toFixed(2)andNumbersums.Proposal: retire #307 once this lands (product invariant 7). Closing it is the operator's call. Its critical-path ETA idea is noted as a follow-up.
Review round 1 (astra: DO-NOT-SHIP) — fixed in 96e12d9
submissionDigestof the displayed submission; otherwise the result isinvalid: submission-mismatch. So a genuine sealed outcome can no longer be shown under another submission.acceptedDealDigestis recomputed from the copy; otherwise the result isinvalid: plan-integrity. The plan must also be bound to the request, the reservation, the plan id and the exact node set, with every binding naming its own unit; otherwiseinvalid: plan-binding.{reservationId, acceptedDealDigest}. It must name this reservation and exactly this digest before the plan is Layer B."0")state: "invalid", shows nothing, and never throwsN25 and R15 adaptations (0d3ac12, 1b5e77e)
agreementHash, and each binding'splanHashplus a field-by-field copy of itscanonicalPlan. Inputs and constraints are read once throughcopyPlanJson; non-JSON is invalid even inside a resealed plan.plan-binding).execution: {planHash, inputs, constraints}, ONLY from the intact, bound plan. It is documented as the caller's content, sealed as agreed, not facts PCC verified: render it as data.agreementHash.invalid-execution-json(one code per field) and theeconomicsstage (node plus economics' code).Cross-family status: round 2 (the round-1 fixes + these adaptations) is queued for the 19:43 PDT codex window at 1b5e77e (#3177).
Not yet
Tests
tscis clean.🤖 Generated with Claude Code
https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz