Skip to content

feat(spec): economic agreements v1, an exact compiler to V-next per-unit payouts - #360

Merged
LamaSu merged 19 commits into
masterfrom
feat/economics-agreement-compiler
Oct 3, 2026
Merged

LamaSu merged 19 commits into
masterfrom
feat/economics-agreement-compiler

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Reconciliation lane pcc-economics (df42dbe5). Ledger rows R15, R17 and R18 (plus R16 as an optional template); technical pack §8. Draft: independent verification is still running (see Status).

Why

Nothing on master turns an accepted agreement into the V-next escrow's PayoutEntry[]. git grep PayoutEntry\|UnitConfig\|prePolicyRoot -- '*.ts' finds no TypeScript producer. Nothing represents a license as a versioned, compatibility-checked object either. The existing royalty walkers have verified defects:

  • buildPayoutMap double-pays co-authors (payouts.ts:231-234).
  • getRoyaltyDistributionRich pays a model author's share twice (licensing-engine.ts:648-655 + :717-752).
  • evaluateLicense fails open when no terms exist (:152-165).
  • The schedule overlap guard is disabled by an open-ended segment (rate-schedule.ts:379).

What

One pure bigint compiler in @pcc/spec (subpath @pcc/spec/economics, and the economics namespace from the root). Normative spec: docs/ECONOMIC_AGREEMENTS.md.

  • EconomicAgreementV1. Payment clauses in economic language: fixed, per-use, pass-through, percent of a named base with min/max, residual, and nested splits. Rights (licenses and intended use) are a separate object with a separate hash. rightsTermsHash and economicTermsHash are both bound by agreementHash.
  • One rounding rule. Largest remainder in exact integers. Each unit's legs sum to exactly its net, so the escrow's PayoutSumMismatch is unreachable for compiled output.
  • Fail closed.
    • Unknown rights refuse; incompatible grants refuse.
    • A license's required payment cannot be dropped, lowered or re-routed.
    • A remainder with no named residual recipient refuses: nobody is paid by default.
    • Cross-job metered and downstream terms refuse (ECONOMICS_UNDECIDED_OD4; OD-4 is the operator's decision).
    • There are 28 named refusal codes (1 schema, 12 structure, 7 rights, 8 money), and each is provoked by a test. (Commit 9918f0e's message says 24; that count is wrong.)
  • Participation. A clause can apply only where a component runs, so it rides on that unit's own evidence release or refund.
  • Identity. A leg is identified by (payTo, role, subject). One wallet in two roles stays two legs. Legs are merged by wallet only when needed to fit the escrow's 16-leg limit.
  • verifyAcceptedAgreement. A license or rate changed after acceptance is refused, and the refusal says which half moved.
  • simulateEconomics. What-if scenarios (price, participation, released / refunded / pending) run on the same compiler.
  • Adapters. CompositionManifest, TrainingManifest and an optional ContributionGraphV1 produce the same IR. The graph compiles to byte-identical units as the same splits written by hand: one engine, not two.
  • netSplitterFor. The economics half of composition's accepted-plan seam (feat(spec): accepted-plan compiler — composition half of MS-01 (R12) #351 CompileDeps.splitNet). It refuses unless the agreement matches what was accepted and every authority-bearing field matches server state: fee, currency, per-unit quote, and licenses as the server's registry copies, including authority.
  • Examples. Five worked agreements in plain language (spare printer with a kit royalty; print-and-mail with a composer margin; a guild with a treasury and members by hours; a lab assay with a minimum royalty and model lineage; a deck built in three milestones), plus one refused incompatible license. Golden outputs are hand-checked.

It does not encode UnitConfig / prePolicyRoot. That is the escrow lane's V-next compiler, which this output feeds.

Status (head 8dc089e5)

  • @pcc/spec: 46 files, 986/986 tests (economics: 187). tsc --noEmit is clean. Contracts ts 262/262 and gateway contributors 40/40 after the rate-schedule change.
  • coord-watch cross-family review (astra) @ 2225ad5f: DO-NOT-SHIP, 4 x P1. All four are fixed @ 0ab9cdbf. The re-review is requested on the current head, 8dc089e5 (#3086, #3131).
    • P1-1: server facts are validated at runtime (SERVER_FACTS_INVALID), so a missing or NaN clock no longer skips checks.
    • P1-2: every paid party must be registry-bound, not only license-named ones.
    • P1-3: every payout-bearing unverifiable rate refuses (RATE_UNVERIFIED at the clause).
    • P1-4: OFFER_EXPIRED is judged at the server's time.
  • Adversarial review: returned 1 critical, 3 high, 7 medium and 12 low findings, plus 6 notes. Every finding is fixed in bad29552, or specified where the code was right and the text was not.
    • Critical: a split DAG could demand millions of allocations. They are now counted before expansion (TOO_MANY_ALLOCATIONS).
    • High 1: a rate pin was checked at a moment the composer picked. It is now evaluated at asOf, per unit value, in exact integers.
    • High 2: use.commercial=false could dodge a noncommercial license. It is now the literal true.
    • High 3: one clause could satisfy two identical required payments. Requirements and clauses now pair one to one.
    • Per-finding disposition and tests: returns/pcc-economics-work/review/DISPOSITION.md. 35 of the 45 new regression tests fail on the pre-fix code.
  • Clean room, round 1 (Python, from the spec alone):
    • It reproduced all 27 payout legs and 13 of 15 hashes. The 2 misses were a spec omission, fixed in f6beb9e6.
    • Its 16 probes drove the normative refusal path grammar (§3).
    • Round 2, a fresh reimplementation from the spec at bad29552, reproduced all 24 hashes and all 27 payout legs, and 82 of 86 probes (P01–P78).
      • The 4 mismatches are closed in 2225ad5f. One was a real bug: with a duplicated split id, refusals depended on array order.
      • Report: returns/pcc-economics-work/cleanroom/round2/REPORT.md.
    • Round 3 (spec at 0ab9cdbf) matched 97/99 probes, 3b (15208e38) 113/114, and 3c (6760a050) 127/127, with no throw and nothing left to guess. Every miss was closed:
      • 15208e38: a pin on a clause that pays in no unit was reported verified: true without being compared, and the PX-12 preview showed it as checked. It is now verified: false. The spec also covers 8 points it left implicit.
      • 6760a050: well-formedness rule 5. Schedule-body integers are 0..2^53−1, version is 1..10^9, and scale/decayPerSecond are finite. JSON 1e400 (Infinity) used to make compileEconomics throw. The registry's publish route and the licensing engine apply the same rule, so the registry can no longer seal a body the compiler can't read. The spec also says how a schedule's real numbers are hashed: ECMAScript Number::toString.
      • 8dc089e5: rule 5 walks only a segment's defined fields, so unknown fields stay ignored for the licensing engine too.
      • Report: returns/pcc-economics-work/cleanroom/round3/REPORT.md.
  • Forge seam test (273bd9f1): compiled payouts fund a real VNextSettlementEscrow and release to the exact legs. CI passed 7/7; the whole forge run passed 974/974.
  • Durable license registry and agreement store: a schema change, parked for the operator.

Interface changes in bad29552:

  • oncePerJobUsing becomes oncePerAgreementUsing, and per: "job" becomes "agreement". The amount is spread over the using units by gross.
  • use.commercial is true.
  • The compile options (fee, rateFacts) are validated.
  • ServerEconomicsFacts gains now, intendedUse, parties and unitFacts.
  • PlanSplitResult carries agreementHash.
  • AGREEMENT_TEMPLATES is exported for agents.

Also in this PR:

  • Rate-schedule overlap guard, and co-authors share one allocation in buildPayoutMap (52fbfac9).
  • Money-like gateway routes with no real source answer 501, never fixtures (df24aa2e).
  • Story real mode fails closed with STORY_NOT_EXECUTED, and licensing terms are versioned and append-only (6e0a48cb).

Also since the review: c28bd34e, the operator's quote floor, agreed with composition (#2762 → #3025): PlanSplitUnit.quote, QUOTE_NOT_COVERED, OPERATOR_BELOW_QUOTE and agreementUnitGross().

Review: needs a coord-watch cross-family verdict on the exact merge SHA (money path).

Merging is the operator's decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K

LamaSu and others added 10 commits September 24, 2026 05:42
…nit payouts

pcc-economics (df42dbe5), ledger rows R15/R17/R18, technical pack section 8.

Nothing on master turned an accepted agreement into V-next PayoutEntry[]
(no TypeScript producer existed) and nothing represented a license as a
versioned, compatibility-checked object. This adds both, as one pure
bigint compiler in @pcc/spec (subpath @pcc/spec/economics, and the
`economics` namespace from the root):

- EconomicAgreementV1: payment clauses in economic language (fixed,
  per_use, pass_through, percent of a named base with min/max, residual,
  nested splits) kept separate from rights (licenses + intended use).
  Separate rightsTermsHash / economicTermsHash, bound by agreementHash.
- One rounding rule (largest remainder, exact integers). Each unit's legs
  sum to its net exactly, so the escrow's PayoutSumMismatch is unreachable
  for compiled output.
- Fail closed: unknown rights refuse, incompatible grants refuse, a
  remainder with no named residual recipient refuses, and cross-job
  metered/downstream terms refuse (OD-4 is the operator's decision).
  24 named refusal codes, each provoked by a test.
- Participation: a clause can apply only where a component runs, so it
  rides on that unit's own evidence release or refund.
- Leg identity (payTo, role, subject): one wallet in two roles stays two
  legs; compaction by wallet only when the 16-leg limit requires it.
- verifyAcceptedAgreement: a license or rate changed after acceptance is
  refused and named as a rights or economics change.
- simulateEconomics: what-if scenarios on the same compiler.
- Adapters from CompositionManifest, TrainingManifest and an optional
  ContributionGraph produce the same IR (no second engine).
- Five worked example agreements plus a refused incompatible-license case.

Spec: docs/ECONOMIC_AGREEMENTS.md. Tests: 69 (exact math, required
lane tests, determinism under shuffling, every refusal provoked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…c agreements

pcc-economics (df42dbe5). Pins the five worked agreements' exact payouts and
hashes (fixtures/economics-golden-v1.json, hand-checked), the simulator's
escrow outcome rules (release pays legs and fee, refund returns gross with
no fee, pending stays reserved, an unfundable price is the compiler's own
refusal), and the adapters:

- CompositionManifest: co-authors share ONE role allocation (the old
  buildPayoutMap paid each the full rate).
- TrainingManifest: a 3-level lineage subdivides the model allocation and
  sums to it exactly (the old rich walker paid it twice).
- ContributionGraph: compiles to byte-identical V-next units as the same
  splits written by hand, so it is a template and not a second engine;
  unaccepted and non-participating contributors are paid nothing; a
  diamond is paid once per path without inflating the pool.

@pcc/spec: 43 files, 888/888 tests; economics 91.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…ts span jobs

pcc-economics (df42dbe5), for composition #351 (CompileDeps.splitNet, unitRef = nodeId).

- Id grammar is now exactly composition's ID_PATTERN (1-128 printable ASCII,
  no space), so every plan node id is a valid unitRef. Canonical JSON still
  escapes only quote and backslash, so hashes stay cross-language exact.
- An agreement may span several V-next jobs (one per operator): the unit cap
  is 256; the escrow's per-job limits (16 units, 256 legs) are enforced by
  the accepted-plan compiler, which groups units into jobs. The 16-legs-per-
  unit cap stays here.

Golden vectors unchanged. Tests: economics 93.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
pcc-economics (df42dbe5), for composition #351 CompileDeps.splitNet (R12 <-> R15).

The agreement is never taken on the caller's word. The splitter refuses,
never repairs, unless (1) the agreement still hashes to what the payer
accepted (verifyAcceptedAgreement; the refusal names rights / economics /
envelope), (2) every authority-bearing field equals server state: the
plan's fee and currency, the unit set and each unit's server-quoted gross,
and every license equals the server's registry copy verbatim, including its
authority level, and (3) compileEconomics accepts it with the server's
sealed schedule bodies and forbidden recipients. It snapshots its inputs at
construction, re-checks the fee rule against the plan's f/n, and returns
units in plan order in composition's NetSplitResult shape (conformance
typechecked against a verbatim copy of #351 69012b4's NetSplitter type).

@pcc/spec: 44 files, 899/899 tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…eal escrow

pcc-economics (df42dbe5). Invariant 12: prove the seam, not a unit.

VNextEconomicsSeam.t.sol funds a real VNextSettlementEscrow with the exact
per-unit payouts compileEconomics emits for three worked agreements and
releases them. Every party, the fee recipient and the payer end with
exactly the compiled amounts:

- print-and-mail: fixed upstream prices, postage at cost (two legs, one
  wallet), a per-use method fee, composer margin; payer spends 22.000000
- guild repair: the nested split's leftover base units land where the
  largest-remainder rule put them
- deck milestones: design and inspection release, the build step is
  refunded after its deadline; only that step's parties go unpaid and no
  fee is taken on it

Negative controls on-chain: one base unit over -> PayoutSumMismatch; 17
legs -> BadLegCount; the settlement token as a recipient ->
ForbiddenRecipient; the same legs reordered cannot fund the clone
committed to the compiled order -> PolicyRootMismatch.

economics-forge-parity.test.ts parses the Solidity literals and fails if
they differ from compileEconomics output (4/4). Forge runs in CI (the
submodules are not vendored on the Spark and are not fetched here).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
… allocation

pcc-economics (df42dbe5), defects re-verified at ac86a40 (reconciliation note D1-D3).

D1: assertScheduleIsWellFormed let an open-ended segment (endTime null)
reset its overlap guard, so every later segment escaped the check and,
since evaluation takes the first covering segment, could never apply. It
now refuses any segment after an open-ended one. The live publish route
POST /api/contributors/schedules never called the guard at all and sealed
malformed schedules immutably under their hash; it now returns 400
malformed_schedule before sealing.

D2: buildPayoutMap paid each co-author of one role the full rate when
groupBps was absent, contrary to CompositionEntry's documented equal
split (two co-authors = 2x, and different recipients passed setPayoutMap's
dedup). Co-authors sharing (role, ipId, rateScheduleHash) now split the
role's bps exactly (floor each, leftover bps to the earliest in manifest
order). Explicit groupBps behaviour is unchanged. Marked @deprecated for
new deals: V-next funds exact amounts via economics.compileEconomics.

D3/D5: LicensingEngine.getRoyaltyDistributionRich is @deprecated for
payouts (resolves rates by ipId from a mutable cache; pays a model's share
to its author and again to its datasets). No production callers; the
correct subdivision is economics.splitsFromTrainingManifest.

Mutation-checked: the new route tests and co-author tests fail against the
previous code and pass now. spec rate-schedule 37/37; gateway contributors
39/39; contracts TS 12 files 248/248; tsc clean (spec, contracts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
… fixtures

pcc-economics (df42dbe5), from readmodels' server-side fabrication census
(2026-09-24), which assigned this family to economics.

- POST /api/swf/epochs/:epochId/distribute scored every participant with
  Math.random() (jobs, reputation, uptime, votes) and then DISTRIBUTED the
  epoch on those scores. It now refuses with 501 not_available until real
  per-epoch contribution inputs exist; the epoch is left untouched.
- /api/rewards/*, /api/certificates* and /api/treasury/summary served
  fixtures: reward epochs and payouts, "claimed" claims with a fake tx
  hash, a mint that reported minted:true, and a 50000 USDC / 85000 USD
  treasury. All now answer 501 not_available with pointers to the real
  reads (escrow, jobs, capabilities, compliance), following PR #362's
  pattern. Paths stay registered so clients get an honest answer.
- MCP pcc_depin_stats and the CLI depin command report each part on its
  own (pccFetchEach): an unavailable part is { unavailable, reason }, never
  zero and never a thrown whole.

Tests: gateway economics-no-fabrication 10/10 (asserts no fixture value
leaks and the refused distribution leaves the epoch unchanged); full
gateway 2939 passed locally (the 2 capture files need @pcc/verifier
built; CI builds it). mcp-server 71/71, tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…sioned

pcc-economics (df42dbe5), board row N10b (N10a, the /api/ip/* route
authorization, is gateway's WP-E).

Story Protocol real mode (STORY_MOCK=false) returned success it never
earned: payJobRoyalty, claimRevenue and distributeRoyaltyTokens returned
made-up deterministicHex transaction hashes, getRevenueSnapshot returned a
zero-revenue "vault" at a made-up address, raiseDispute returned a dispute
never raised, registerJobAsDerivative registered a fabricated child IP id
with a hard-coded license token, registerCapabilityAsIP committed a
fabricated metadata hash, and the reads answered from an in-process map
("not registered" after any restart). Each now throws
StoryNotExecutedError (code STORY_NOT_EXECUTED): nothing was sent and no
transaction hash exists. Routes should map it to 501 not_executed. The SDK
wiring that was removed stays in git history (master ac86a40).

Mock mode is unchanged but every mock result now carries simulated: true
(optional field on the Story spec types), so no consumer can take a
simulated royalty or balance for a real one.

LicensingEngine.setTerms was an unconditional overwrite. Terms are now
versioned and append-only: a change appends version n+1, earlier versions
stay readable (getTermsVersion), identical terms are a no-op, and reads
return copies. setTerms returns the version in force.

Tests: story-ip-service 42/42 (11 new: every real-mode operation rejects
with STORY_NOT_EXECUTED; mock results are simulated); licensing-engine +3;
contracts TS 12 files 262/262; tsc clean (spec, contracts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
pcc-economics (df42dbe5). An independent Python implementation written from
docs/ECONOMIC_AGREEMENTS.md alone (returns/pcc-economics-work/cleanroom)
reproduced every payout of the five golden agreements (9 units, 27 legs)
and 13 of 15 hashes. Both misses had one cause: the spec never said to sort
a license requirement's payee.distribution (the code sorts it by party).
Its 16 probes found further places where two faithful implementations
could disagree. The spec now fixes each one, and the code follows:

- canonical form: payee.distribution sorted by party; Hash defined (either
  case, lowercased); bounds and uniqueness rules stated (Amount <= 78
  digits, per-unit components/measures <= 64, versions <= 1e9, unique
  requirementIds and distribution parties, outbound share-alike tag rule)
- refusals: identity is (code, path) and messages are informative, so the
  compiler now dedupes on (code, path) only (it keyed on messages); paths
  compare element by element (ids may contain '/'); a normative path
  grammar for every code; the money-check order per unit; one FEE_INVALID
  at most; SPLIT_CYCLE per split on a cycle; SPLIT_TOO_DEEP for any split;
  rights checked once per component; SCHEMA_INVALID normative by code only
- a payee distribution naming an unknown party is now UNKNOWN_REFERENCE
  (structure), not a payment mismatch
- result orders stated for attribution, zeroLegs, clauses, rights, rates
- verifyAcceptedAgreement's documented output corrected to what it returns

Golden vectors unchanged; the fixture records its compile options.
Economics tests 110/110.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
An adversarial review (FINDINGS.md) found 1 critical, 3 high and 7 medium defects, all confirmed by repro.

Critical
- C1: the split fan-out is counted before anything is expanded. The new refusal TOO_MANY_ALLOCATIONS fires above 65,536 allocations. The argument spread that overflowed the stack is gone. A 32 KB agreement that killed the process is now refused in milliseconds.

High
- H1: a pinned rate is re-evaluated at asOf, for every unit's own value in cents, with the server's rate facts. The composer's recorded evaluatedAt and context are no longer trusted.
- H2: use.commercial is the literal true, because every compiled deal is paid work.
- H3: license requirements and clauses pair one to one, by a key. Two identical requirements need two clauses, and a surplus clause is UNMATCHED.

Medium
- M1: oncePerJobUsing becomes oncePerAgreementUsing. Its fixed or pass-through amount is spread over the using units by gross, so a refunded unit refunds only its share.
- M2: the option fee is the server's fee. Any other fee is FEE_INVALID [fee, server-fee].
- M3 and N5 (bind): asOf must be a recent server moment, and use is the server's intended use. Licensed parties are paid at their registry addresses, and unit components, uses and measures are the server's.
- M4 and L1: options are schema-validated. A bad option is SCHEMA_INVALID [options], never ignored and never thrown.
- M5 is specified: a routing node is a pool.
- M6: the root obeys participation.
- M7: the training manifest's own schema is enforced (MANIFEST_INVALID).

Low
- L2: input and options are read once into plain JSON data, so an accessor, a Proxy or a cycle is a refusal, not an exception.
- L3: a schedule body is trusted only for the hash it has.
- L4: a schema failure is one refusal at [], independent of array order.
- L5 and N1: scenario overrides are unique per unit, with limits sized for 256 units.
- L6: a required payment must be about the licensed subject.
- L7 and L8: adapter lookups use own keys only; ids must be valid; zero-weight co-authors are dropped.
- L10: bind returns agreementHash, and the spec says acceptance binds it.

Notes
- N2: canonical string escaping is pinned in the spec.
- N3: rates are evaluated in exact integers, and exponential decay is unverifiable.

SCHEDULE_HASH_MISMATCH is removed: a mislabeled body is now a malformed option. There are 28 refusal codes. AGREEMENT_TEMPLATES is added for ADK.

Golden vectors are unchanged. Economics tests pass 160/160 and the spec suite 958/958. 35 of the 45 new review tests fail on f6beb9e. The other 10 are controls, exact-arithmetic unit tests, behaviors fixed or specified earlier, and one read-once consistency check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
…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
…efusals on duplicate ids)

A clean-room reimplementation from the spec at bad2955 (Python, spec only) matched all 24 hashes, all 27 payout legs and 82 of 86 probes. This closes the 4 mismatches and the gaps it named.

- P28 (code bug): with a duplicated split id, the refusals depended on array order, because the split map kept the later definition. When any DUPLICATE_ID is reported, the checks that resolve ids now do not run: SPLIT_CYCLE, SPLIT_TOO_DEEP, TOO_MANY_ALLOCATIONS and RATE_PIN_MISMATCH.
- P62: each rateFacts field may be omitted (meaning null), as §2.4 says.
- P73/P74: the spec now lists every schedule well-formedness rule the options check enforces.
- The server's fee option obeys §2's rule itself: it has a recipient exactly when the fee is nonzero.
- Specified as a v1 limitation, pinned by a test: a required scheduled royalty whose units fall in different value bands is refused, never underpaid.
- Spec states what both implementations already agreed on silently:
  - labels count code points;
  - versions sort numerically;
  - attribution lists positive allocations only;
  - the membership of rights and byParty;
  - a pin that applies to no unit is verified;
  - null options are refused;
  - duplicate forbidden recipients change nothing.

Economics tests: 165/165.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
…efuses

pcc-economics N10b (#360) makes Story real mode refuse operations it cannot execute, with StoryNotExecutedError (code STORY_NOT_EXECUTED). Whichever PR merges first, the IP routes now answer that refusal as 501 not_executed, never as a generic 500 that a client could mistake for a transient fault.

settle-royalties answers 501 when Story executed none of the payments. A mix of paid and failed rows stays 502 settlement_incomplete, with the per-row outcomes.

The check is duck-typed on err.code, so it holds on master before #360 lands. ip-authz: 20/20.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…n top (seam floor)

Composition asked (#2762) which rule compileEconomics assumes for royalties: (a) inside the operator's quote, or (b) on top. The answer is (b). An operator's quote is its price for its own work, and anything the agreement adds (licenses, modules, the composer's margin) is paid by the buyer on top of it.

- PlanSplitUnit carries the operator's live `quote`. `g` is the unit gross the buyer funds, which is the agreement's.
- New QUOTE_NOT_COVERED: a unit's g is below the quote.
- New OPERATOR_BELOW_QUOTE: after the compile, the legs to the operator's payout address must total at least quote - floor(quote * feeBps / 10000).
- New agreementUnitGross(): the plan compiler reads each unit's gross to reserve it before compiling. The splitter re-checks everything, so reading it early grants nothing.
- Spec section 9 states the rule.

Economics tests: 167/167.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu and others added 2 commits September 24, 2026 15:32
coord-watch's astra verdict on 2225ad5 (review-router/r15core-360-econ-astra) was DO-NOT-SHIP, with four P1 findings. All four are fixed here.

P1-1 (server facts were not validated at runtime): a missing or NaN `now` made both time comparisons false and skipped the check, and missing facts threw.
- ServerEconomicsFactsSchema now validates a read-once copy.
- Malformed or unreadable facts give economics:SERVER_FACTS_INVALID:<field>; malformed plan units give PLAN_UNITS_INVALID.
- Nothing throws and nothing is skipped.

P1-2 (only license-named parties were bound to the registry): a residual, fixed or split recipient could be paid at an address the registry contradicts. After the compile, every party the compile pays must be registered at the same address (PARTY_NOT_REGISTERED / PARTY_MISMATCH). This includes the reviewer's counterexample and a split.

P1-3 (an unlicensed clause with an unknown or unevaluable rate source still paid out): RATE_UNVERIFIED is now a structure refusal at ["clause", id, "rateSource"] for any clause that names a rate source and pays in some unit, whether or not a license requires it. A clause that pays in no unit is not refused.

P1-4 (an expired offer passed on the agreement's own asOf): the binding refuses OFFER_EXPIRED when the server's now is past terms.acceptBy.

Also fixed: the example builders shared nested objects (terms, fee, currency, use), so mutating one build changed later ones. They now return fresh objects; a test pins that, and the golden hashes are unchanged.

Spec sections 2.4, 3, 4.1, 5 and 9 are updated. Economics tests 174/174, spec 973/973, tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…und 3

Clean-room round 3 (an independent Python implementation written from the
spec at 0ab9cdb) matched 24/24 hashes, 27/27 payout legs and 97 of 99
probes. Both misses had one cause, a spec gap. It also listed eight places
where the spec made it guess, and one of them was a real defect:

- Fix: a clause that pays in no unit was reported `verified: true` whenever
  its schedule body was supplied, even when the pinned bps contradicted the
  schedule, because nothing was ever compared. The PX-12 preview turns
  that flag into "Checked against the published rate schedule". Such a pin
  is now `verified: false`. It is still not refused, since it pays nothing.

Spec (docs/ECONOMIC_AGREEMENTS.md), current behavior now written down:
- Unknown fields in a schedule body are dropped before hashing at every
  depth, including inside segments, as the registry's publish route does
  (P84). byClass is the one closed object.
- Every agreement field is required; nullable ones are present as null.
- A `units` name that is not a unit is UNKNOWN_REFERENCE and selects
  nothing (P89).
- Once-per-agreement spread weights are each gross as written; all-zero
  weights give zero shares (P78).
- Exactness depends only on the segment at asOf and on rateFacts, so
  RATE_PIN_MISMATCH and RATE_UNVERIFIED never both apply to one clause.
  This replaces a precedence rule that could never fire.
- The option fee's feeBps is 0..1000.
- How input depth and the value count are measured; only own enumerable
  properties are read.

Tests: 8 new. The P81 test fails on the pre-fix code. Spec suite 981/981,
tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
…ied) into PX-12

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
With #360 at 15208e3, a compiled deal marks a pinned rate unchecked only
when its clause pays nothing in the agreement; every rate that pays out is
checked or the deal is refused. The preview used to explain any unchecked
rate as "the published rate schedule was not available", which is not the
reason. It now says "Not checked, because this clause pays nothing in this
agreement". Any other unchecked rate gets a neutral "Not checked against
the published rate schedule".

Spec 999/999, tsc clean. Dashboard 227/227 and gateway economics 20/20
against the rebuilt spec dist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…se clean-room round 3b

Clean-room round 3b (the spec at 15208e3) matched 24/24 hashes, 27/27 legs
and 113 of 114 probes. It confirmed the round-3 fixes as a black box. The
one mismatch, P100c, was a spec gap: integers inside a rate-schedule body
had no range.
- A body with thresholdCents 2^53 + 1 reads as 2^53 in JavaScript, so the
  lane refused its hash and an exact reader did not.
- Nothing refused 2^53 itself, or a version above 10^9.
The same gap let JSON 1e400, which parses as Infinity, reach the exact
evaluator: compileEconomics THREW a RangeError on an adoption-indexed
scale of Infinity, breaking its never-throws contract.

- assertScheduleIsWellFormed gains rule 5. Integer fields must be
  0..2^53-1 and scale/decayPerSecond finite. When a version is given, it
  must be 1..10^9.
- The compiler (option schedules), the registry's publish route and the
  licensing engine all use this check, so the registry can no longer seal
  a body the compiler cannot read.
- Spec: well-formedness rule 5, and the known fields outside the hash are
  still type-checked. Canonical JSON now says how a schedule's real
  numbers are written: ECMAScript Number.prototype.toString, so 1e-9, not
  1e-09.

Tests: 4 in spec and 1 in gateway. The P100, P100b, Infinity and registry
tests fail on the pre-fix code. Spec 985/985, contracts ts 262/262,
gateway contributors 40/40. tsc clean on spec, gateway and contracts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
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
…nto PX-12

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
…try schedules

Found in a self-review of the PX-12 preview.

1. Size. Every allocation became its own payee line. A valid 256-step
   agreement paying 16 pools of 15 members (61,696 allocations) produced a
   12 MB preview that no browser or person can use, from one ~0.5 s
   request. Each payee now shows at most 24 lines, and they always re-add
   exactly to its total:
   - one line per step and reason, when that fits;
   - else one line per reason, summed over its steps ("256 steps");
   - else the largest reasons, with the rest summed per kind of payment
     ("20 more payments: Licenses and contributors").
   The worst case is now well under 1 MB. Small agreements are unchanged.
   PreviewLine.unitRef is null for a line that sums several steps.

2. Unusable registry bodies. A schedule sealed before the registry
   checked number ranges (#360 at 6760a05), or a stored copy that no
   longer hashes to its label, used to refuse the whole agreement as
   malformed options. It is now left out, so the pin it would have
   verified is refused at its own clause (RATE_UNVERIFIED). The compiler
   still re-checks both.

Tests: 3 in spec and 1 in gateway. Both large-agreement tests and the
route test fail on the old code. Spec 1006/1006, dashboard 227/227,
gateway economics 21/21, tsc clean on spec, gateway and dashboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
Clean-room round 3c (the spec at 6760a05) matched all 127 compiles, with
no mismatches and no throws, and reported nothing left to guess.

Its P104/P105 probes put unknown numeric fields (1.5, 1e400) inside a
segment. The compiler and the registry accepted them, as the spec says:
their zod parse strips unknown keys before the check runs. But the
licensing engine hands raw objects to assertScheduleIsWellFormed, and
there the new loop over every numeric field refused a field the spec says
is ignored. The check now walks only the fields a segment kind defines:
the eleven integer fields, plus scale and decayPerSecond.

Test: unknown fields pass the check even without a parser, while a known
field out of range still throws. It fails on the previous loop. Spec
986/986, tsc clean; contracts ts 262/262, gateway contributors 40/40.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 24, 2026
… demo path

Economics #3315 and the steward's SWF ruling (operator item 69): the fund
stays a design artifact and its money routes stay 501; distribute is on
N46's money-floor list. So POST /api/swf/epochs/:epochId/distribute
refuses whatever PCC_DEMO_ROUTES says, before any read, score or write
(the random-score demo distribution is removed, and with it swf.ts's
last Math.random calls and its ratchet entry). The refusal keeps the
reviewed wording and METHOD-path `see` pointers, now also pointing at
GET /api/swf/accruals. #360 makes the same handler an unconditional 501,
so whichever lands second resolves a wording-only conflict.

agent: pcc-readmodels (c255d7dc)
LamaSu and others added 2 commits September 29, 2026 19:32
…s at 0

Cross-family review EC1 (astra, DO-NOT-SHIP at 8dc089e; all four P1s CLOSED). Each
finding was reproduced at 8dc089e with a test that fails there, then fixed:

- H1 (HIGH): netSplitterFor validated plan units by reading the caller's objects, then read
  them again to enforce. Astra's counterexample, a $12.30 quote that reads 12300000 twice and
  0 after, returned ok at 8dc089e instead of OPERATOR_BELOW_QUOTE. Now readPlanUnits copies
  each unit's six decision fields once, as own data properties, into frozen plain data, and
  every later check reads only that copy. An accessor is refused (PLAN_UNITS_INVALID), as
  snapshotJson refuses one in the agreement. A Proxy's get trap is never consulted, so a
  lying proxy is judged on its one data read (OPERATOR_BELOW_QUOTE).
- H2 (HIGH): a Proxy unit whose get trap throws escaped from the seam ("Error: trap"). So did
  a Proxy around the input itself. Every read is now an own-data read inside try/catch: a
  throwing unit, a revoked unit list, and a throwing or revoked input all refuse
  (PLAN_UNITS_INVALID, SERVER_FACTS_INVALID:unreadable). An input whose accepted field cannot
  be read is checked and refused, never treated as null.
- M3 (MEDIUM): rule 5 checked Number.isSafeInteger only, so a correctly hashed schedule with
  startTime -1 passed assertScheduleIsWellFormed and LicensingEngine.setRateSchedule (which
  hands it raw objects). The range now starts at 0, as the parsers' does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cross-family review EC1b (astra, SHIP-WITH-FIXES at a3d3d76): H1 and H2 CLOSED; M3 still
OPEN because the shared helper was narrower than RateScheduleSchema for raw callers.

Reproduced at a3d3d76. assertScheduleIsWellFormed accepted all ten of these, and
RateSegmentSchema refuses each:
- an adoption-indexed scale of 0;
- an exponential decayPerSecond of 0 or -1;
- a bps of 10001, or a bps given as a string;
- a capture class rate of 10001 or -1, or an unknown class (CC9);
- an unknown kind;
- an empty segment list.
LicensingEngine.setRateSchedule stored the correctly hashed scale-0 schedule.

Fixed: after rule 5's own checks, which keep their messages, every segment must pass
RateSegmentSchema, and a schedule needs at least one segment. Unknown keys stay ignored, as
the schema ignores them (round 3c's P104/P105 test still passes). setRateSchedule and
unsafeReplaceRateSchedule now refuse all ten, and store nothing.

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 02:59
LamaSu added a commit that referenced this pull request Sep 30, 2026
#360's review loop added a3d3d76 (the seam reads plan units and its input once; rule 5 starts at
0) and c73d708 (rule 5 admits exactly the segments the schema admits). PX-12 stays stacked on
#360's reviewed head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#360 was SHIP at c73d708 (EC1c) with full CI green, but master moved on 9/30–10/01 (#424,
#459 N80, #436, #338, #477, #483 and others), and three files conflicted. Resolutions:

- packages/spec/src/index.ts: both sides added exports at the same place. Both are kept: #360's
  `economics` namespace, and master's money-status and readmodels exports.
- packages/gateway/src/routes/swf.ts: both sides made POST /api/swf/epochs/:epochId/distribute
  answer 501 not_available before anything is read. Master's hunk (#421's, reviewed and merged
  with #424) is kept. The behavior is identical.
- packages/gateway/src/routes/rewards.ts: #360 made the reward, certificate and treasury routes
  answer 501 and deleted their fixtures. Master's N80 (#459, reviewed and merged) changed only
  the certificates: an empty list, 404 for one, and a 501 mint that claims nothing. It kept the
  reward and treasury fixtures. Kept: master's N80 certificates, and #360's 501s for rewards and
  treasury, with no fixture import. economics-no-fabrication.test.ts drops its three certificate
  rows (certificates-n80.test.ts pins N80), and the reward and treasury rows still assert 501.

On the merged tree: spec 1286/1286; contracts 267/267; mcp-server 76/76; gateway 3717 passed,
13 skipped, 0 failed; dashboard 353/353; tsc clean for contracts, mcp-server, gateway and
dashboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Oct 3, 2026
#360 merged master @753feb43 after EC1c's SHIP (three conflicts, resolved in 8fe16b6). PX-12
stays stacked on #360's head. This merge had no conflicts. On the merged tree: spec 1311/1311,
gateway 3732 passed (13 skipped), dashboard 361/361; tsc clean for gateway and dashboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Oct 3, 2026
…5678)

#474 and #360 (MERGE NOW #8) both add exports to packages/spec/src/index.ts
right after ./payouts.js, so #474 sits behind #360 on the merge line. This
merge takes #360's head @8fe16b69 now, so the conflict is resolved and
reviewed before #360 lands.

The only conflict is packages/spec/src/index.ts. It is resolved as the union
of both sides, verbatim: #474's intake and research barrels, then #360's
namespaced `economics` export. Every other path that differs from 069f9df
(44 of them) equals #360's blob exactly, so no file was auto-merged. Spec:
58 files, 1731 tests pass; both typechecks are clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
@LamaSu
LamaSu merged commit 0972247 into master Oct 3, 2026
9 checks passed
LamaSu added a commit that referenced this pull request Oct 4, 2026
A plain merge with no manual edits. #474 had two merge bases with master
(7d688ce and 8fe16b6), a criss-cross left by its pre-staged merge-up
with #360, so GitHub showed it DIRTY although it merges cleanly. The
resulting tree equals git merge-tree --write-tree cd9d877 b4bf06b.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu added a commit that referenced this pull request Oct 4, 2026
#360 merged as 0972247, so #492 now targets master. No file changed on both sides since 8fe16b6; the PR's own diff is unchanged from the reviewed delta 8fe16b6..8fbbc86 (astra EC4b SHIP).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Oct 4, 2026
#360 merged as 0972247, so #394 now targets master. One file changed on both sides since 8fe16b6, packages/gateway/src/server.ts, and git merged it with no conflict; #394's own hunk there is unchanged. Every other file in the PR's diff is unchanged from the reviewed delta 8fe16b6..92613a3 (astra EC3b SHIP).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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