test(e2e): riverdale economy - multi-party, versioned fibers, assets#190
Merged
Conversation
Phase 1 harness enablement for the Riverdale-economy e2e — the riverdale
unit test is cross-fiber (manufacturer->retailer triggers, fed->bank
broadcast, real asset moves), but the runner was single-fiber-per-flow.
These are general-purpose, back-compatible (every new step field is
optional; single-fiber flows are unchanged):
- Named multi-fiber: a `create` step with `as:"alice"` mints a fresh
fiberId and registers an alias; any step with `fiber:"alice"` (or a raw
id) targets it. session.cid remains the default.
- Per-step `signers:["alice"]`: signs with a wallet SUBSET, so the proofs
(== owners/authorizers) model the acting party; enables wrong-party
negative tests. Omit = all wallets (today's behavior).
- Poll-only `assertState` / `assertAsset`: observe a fiber changed
INDIRECTLY by a cross-fiber trigger, or real on-chain asset custody
after a `_transferAsset`/morphism (reads the `/assets/{id}/state-proof`
`.record`). No transaction; standard retry budget.
- Asset ops `createAssetPolicy` / `mintAsset` / `applyMorphism`: plain-JSON
messages (the chain JAR decodes the variants; the published SDK doesn't
yet export asset builders, and batchSign signs any object) confirmed via
registry / asset-proof reads. First e2e to exercise REAL asset custody.
New: lib/assertHelpers.ts (deepEqual/selectSigners/holderMatches/pollers),
lib/asset/{createAssetPolicy,mintAsset,applyMorphism}.ts.
Design: docs/e2e/riverdale-economy-e2e-design.md.
runner.ts + new files are tsc-clean; the remaining e2e-test type errors are
pre-existing SDK-staleness in untouched files (not a typecheck gate — runs
via tsx).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
The first e2e to exercise REAL asset custody and cross-fiber orchestration — a thin 3-fiber "supply-chain + upgrade" slice that de-risks the Phase 1 harness extensions before the full economy fan-out. One causal flow over 3 party-owned fibers (alice=manufacturer, bob=retailer, carol=consumer): - cross-fiber `_triggers`: manufacturer.fulfill_order fires the retailer's receive_shipment (no declared dependency needed — only FiberPolicy acceptedCallers, unset = any caller; verified in EffectExtractor / TriggerDispatcher). - real `_transferAsset` custody: the GOODS instance moves manufacturer fiber -> retailer fiber in the SAME transition, asserted via assertAsset. Recipient is a bare UUID string (parseRecipient maps it to AssetHolder.Fiber), NOT an AssetHolder object. - versioned upgrade: retailer is verified-bound to retailer.machine@1.0.0, upgraded to @2.0.0 with a migration adding loyaltyPoints, then drives a v2-only redeem_loyalty. - two real assets: GOODS (T|C) minted into the manufacturer fiber, RVD (Fungible) minted to carol's wallet; both custody reads asserted. 17 files; uses the new runner steps (as/fiber/signers/assertState/ assertAsset/createAssetPolicy/mintAsset). All wire shapes verified against the chain sources (quoted in the example.ts header + README). CI: own isolated `riverdale-economy` lane (concurrency 1, wallets alice,bob,carol, ordinalThreshold 15) — local cluster bring-up drifted across retries, so CI (fresh clone, no stale state) is the validation path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
Phase 2 (part 1/2) — the new economy state machines, authored against PHASE2-CONTRACT.md. Inert until wired into example.ts (next commit), so the green slice flow is unaffected (retailer v1/v2 gain a process_sale transition; same file used for publish+create keeps the verified-binding logicHash aligned). - consumer: ACTIVE/debt_current/marketplace_selling — loan_funded, buy (triggers retailer.process_sale + transfers RVD shard), make_payment (triggers bank.payment_received + transfers RVD), pay_taxes, list_item (_spawn auction child; accept_bid triggers parent.sale_completed), sale_completed. - bank: operating/loan_servicing — rate_adjustment, underwrite (triggers consumer.loan_funded + transfers RVD), payment_received. - fed: v1 set_rate (triggers bank.rate_adjustment); v2 adds emergency_lending (v2-only) + migration. Versioned like the retailer. - retailer v1+v2: + process_sale (transfers GOODS to buyer + revenue). Still TODO (part 2): gov fiber, events, asset choreography (mint RVD to bank, Fractionalize, Burn), and the two example.ts flows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2: scale the proven de-risk slice into the full 6-party economy, on the now-validated multi-fiber + asset harness. Six party-owned fibers (alice=manufacturer, bob=retailer, carol=consumer, dave=bank, erin=fed, frank=gov) in one causal flow (P0..P12): - supply chain: manufacturer.fulfill_order -> retailer.receive_shipment + _transferAsset GOODS - monetary policy: fed.set_rate -> bank.rate_adjustment (broadcast-capable) - lending: bank.underwrite -> consumer.loan_funded + _transferAsset RVD (bank fiber -> consumer fiber) - commerce: consumer.buy -> retailer.process_sale + RVD consumer->retailer + GOODS retailer->consumer - servicing: consumer.make_payment -> bank.payment_received + RVD - TWO versioned upgrades: retailer.machine AND fed.machine v1->v2 with migrations + v2-only transitions (redeem_loyalty, emergency_lending) - governance tax sweep: gov.collect_taxes broadcasts pay_taxes to all three taxpayers in one tx; consumer remits RVD to the gov fiber - auction: consumer.list_item _spawn's a child auction; bid -> accept -> triggers the parent's sale_completed Plus FLOW 2: graceful negative tests (wrong-party, replay, mint-over-cap, non-monotonic publish -> all ML0 rejections, state unchanged). Assets: GOODS + RVD minted, transferred fiber<->fiber across every economic edge, custody asserted via assertAsset throughout. Morphisms: fiber-held ApplyMorphism is R1-rejected (AssetCombiner requireWalletHolder gates ALL kinds before applyKind), so RVD payment legs are separate whole instances moved via _transferAsset. STAKE is wired LIVE (non-consuming: bumps seq, record survives -> the runner's seq-advance confirm observes it). FRACTIONALIZE + BURN are CONSUMING (they remove the source record), which the current applyMorphism confirm can't observe, so they ship as ready body files + commented steps; wiring them live needs an additive runner consuming-morphism confirm mode (next). CI: riverdale-economy lane timeout 55, wallets alice,bob,carol,dave,erin, frank. tsc-clean; all JSON valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
First full-economy CI run: 58/60 of FLOW 1 passed — every cross-fiber trigger, real _transferAsset custody (lending/commerce money+goods/servicing), the broadcast tax sweep, the spawned auction, and BOTH v1->v2 versioned upgrades worked on a real cluster. Two follow-ups, both informed by that run: - Defer the wallet-context morphisms (Stake/Fractionalize/Burn). STAKE hit a DL1 HTTP 400: the runner waitForDl1Syncs FIBER commits but never waits for an asset's assetCommit to reach DL1 after a mint, so applyMorphism raced DL1. Plus consuming morphisms (Fractionalize/Burn) can't be confirmed by the source-seq-advance predicate. Both need small runner additions — tracked as the morphism fast-follow; body files + ids + policy remain shipped. - Drop the wrong-party negative: the run proved a primary state-machine transition is NOT owner-gated (the guard is the gate). Ownership gates registry ops, script callers, asset holders (R1), and spawned children — not arbitrary transitions. Kept the three valid negatives (replay, mint-over-cap, non-monotonic publish). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first full-economy run (58/60) showed the asset-op path lacked the DL1 sync the fiber path has: the runner waitForDl1Sync's FIBER commits but never waited for an asset's `assetCommit` to reach DL1 after a mint. A subsequent applyMorphism is structurally validated at DL1 against OnChain.assetCommits (AssetRules.applyMorphismStructural — unknown asset is a HARD reject), so a mint→morphism on the same asset raced the ML0→GL0→DL1 propagation and 400'd "unknown asset" (hit even non-consuming STAKE). - Add waitForDl1AssetSync (asset analogue of waitForDl1Sync): after a mintAsset confirms at ML0, gate on every DL1 node carrying assetCommits[assetId] before the next step. - Re-enable the P12 STAKE morphism live: mint RVD into dave's wallet, STAKE it (R1: signer == holder), assert the seq advanced (non-consuming: holder + amount survive). STAKE keeps the morphism pillar live in the economy. FRACTIONALIZE + BURN stay deferred (CONSUMING — they remove the source record, which the source-seq-advance confirm can't observe; they need a terminal/consuming confirm mode). Body files + ids + policy morphisms shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
ottobot-ai
added a commit
that referenced
this pull request
Jun 26, 2026
Follow-up to the green riverdale-economy economy (PR #190): the run was unreadable — the background keepalive + per-send DL1 lines printed live (50+ lines over 20 min) while the flow output was buffered and dumped only at the end, and every step just said "OK" with no view of the economy moving. Harness (shared by all lanes, back-compatible): - keepalive: gate the first-accepted + per-10 heartbeat behind E2E_VERBOSE; stop() prints one line ("[keepalive] silent · N sent, M failed"). - sendDataTransaction: fix describeUpdate (it read message.value on the UNWRAPPED generator output → always "update"; now reads the envelope key + inner fiberId/assetId/name); gate the healthy 3/3 per-send line behind E2E_VERBOSE. Divergence/failure lines always print. - FlowLogger: live (write-through) mode; flush() is a no-op when live. - Concurrency: an INTERACTIVE single-example run (TTY) defaults to 1 so its flows stream live. The isTTY gate keeps CI identical — tictactoe is a single-example lane with 3 parallel flows that must NOT be serialized. Visibility (riverdale-economy): - new poll-only step actions `phase` (section banner) and `economy` (reads the ML0 checkpoint once and prints a per-party table: state + a few stateData fields + held assets, with deltas vs the previous snapshot). - assertState/assertAsset now summarize what they observed (state+seq / holder+amount) instead of a bare OK. - example.ts: 13 phase banners + 12 economy snapshots across P0..P12 (all functional/assert steps unchanged); friendly assetAsset labels. tsc-clean. phase/economy are additive early-continue no-ops; verbose-gating defaults to quiet, so all lanes get a quieter, more readable run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
scasplte2
pushed a commit
that referenced
this pull request
Jun 26, 2026
* feat(e2e): readable economy run - quiet logs + economy snapshots Follow-up to the green riverdale-economy economy (PR #190): the run was unreadable — the background keepalive + per-send DL1 lines printed live (50+ lines over 20 min) while the flow output was buffered and dumped only at the end, and every step just said "OK" with no view of the economy moving. Harness (shared by all lanes, back-compatible): - keepalive: gate the first-accepted + per-10 heartbeat behind E2E_VERBOSE; stop() prints one line ("[keepalive] silent · N sent, M failed"). - sendDataTransaction: fix describeUpdate (it read message.value on the UNWRAPPED generator output → always "update"; now reads the envelope key + inner fiberId/assetId/name); gate the healthy 3/3 per-send line behind E2E_VERBOSE. Divergence/failure lines always print. - FlowLogger: live (write-through) mode; flush() is a no-op when live. - Concurrency: an INTERACTIVE single-example run (TTY) defaults to 1 so its flows stream live. The isTTY gate keeps CI identical — tictactoe is a single-example lane with 3 parallel flows that must NOT be serialized. Visibility (riverdale-economy): - new poll-only step actions `phase` (section banner) and `economy` (reads the ML0 checkpoint once and prints a per-party table: state + a few stateData fields + held assets, with deltas vs the previous snapshot). - assertState/assertAsset now summarize what they observed (state+seq / holder+amount) instead of a bare OK. - example.ts: 13 phase banners + 12 economy snapshots across P0..P12 (all functional/assert steps unchanged); friendly assetAsset labels. tsc-clean. phase/economy are additive early-continue no-ops; verbose-gating defaults to quiet, so all lanes get a quieter, more readable run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt * feat(e2e): wire FRACTIONALIZE/BURN live + color economy deltas Consuming-morphism confirmation in the runner's applyMorphism path (the two modes a source-seq-advance confirm can't observe): - FRACTIONALIZE removes the source + writes one shard per shardId → confirm via the FIRST output shard's existence (confirmPath points at it). - BURN/DECOMPOSE remove the source record → confirm via the source's ABSENCE (it existed pre-send, so exists→404 means the morphism committed); a small resubmit-aware absence poll, never re-burning a gone asset. Non-consuming morphisms (STAKE/Transfer/Wrap) keep the source-seq-advance confirm. example.ts P12: STAKE was already live; now FRACTIONALIZE (mint 900 RVD into carol's wallet → 3×300 shards, asserted) and BURN (mint 200 into frank's wallet → burn → the economy snapshot shows it disappear) run live too. All three approved wallet morphisms are now exercised end-to-end. Economy snapshots: color the deltas — yellow for a changed value / departed instance (`taxesPaid 0→50`, `RVD-tax 50→0`), green for a newly-acquired instance (`+RVD-loan×10000`) — so the eye lands on what moved. ANSI codes are only in the value columns, so label alignment is unaffected. tsc-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt * feat(e2e): parallel-batch for independent setup steps Cut the riverdale-economy wall-clock by running the INDEPENDENT setup steps concurrently. The causal economic chain (lend→buy→repay→tax) stays serial — only steps with no inter-dependency are batched. Runner: a `parallel: true` step flag. The per-step body is extracted into a `processStep(step, i, sw, sl, slog)` closure; a maximal run of consecutive `parallel` steps executes via Promise.allSettled, each writing to its OWN buffered FlowLogger that flushes in step order (concurrent output never interleaves). The SEQUENTIAL path is byte-for-byte unchanged — the else branch calls processStep with the flow's live logger + the same try/catch → {passed:false, failedStep:i+1} — so no other lane is affected (no existing example sets `parallel`). 12 step-level `continue`s became `return`s; tsc-clean guarantees none dangle. example.ts FLOW 1 — 3 batches (12 steps): - P0: the 2 createAssetPolicy + retailer.machine@1.0.0 (distinct registry names, no shared lineage). Same-package later versions (retailer@2.0.0, fed@1.0.0/2.0.0) stay SEQUENTIAL: a lineage is monotonic append-only (VersionLineage.publish → NonMonotonic), so racing 1.0.0/2.0.0 could land 2.0.0 first and reject 1.0.0 (the N3 negative). Causal, not parallel. - P1: all six fiber creates (each mints its own `as` alias — distinct session.fibers key, no shared-mutable race). - P2: the three RVD-leg mints into the consumer fiber (distinct assetIds). GOODS + RVD_LOAN mints stay sequential (split off by their assertAsset). Concurrency-safe: every batched step writes a distinct registry name / `as` alias / assetId, so no shared key is touched by two concurrent steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What this is
Phase 1 of promoting the in-process
RiverdaleEconomyStateMachineSuite(2,843-line unit test) into a real-cluster e2e, on par withrule110/sigma-mixer/staked-oracle-pool. This PR lands the harness enablement + a thin 3-fiber de-risk slice; the full 6-fiber economy is the follow-up.Design doc:
docs/e2e/riverdale-economy-e2e-design.md.Why harness work, not just JSON
The riverdale economy is fundamentally cross-fiber (manufacturer→retailer shipments, broadcasts, tax sweeps) with assets evolving over time. The combiner already does cross-fiber
_triggers/_spawnand real asset custody — but the e2e runner was single-fiber-per-flow and had no way to drive or observe any of it. So ~70% of the work is general-purpose, back-compatible harness extensions (every new step field is optional; existing examples are unchanged).Harness extensions (
376d27a)create … as:"alice"mints + registers a fiber alias; any stepfiber:"alice"targets it.signers:["alice"]: signs with a wallet subset — the proofs ARE the owners/authorizers, so this is the party model (and enables wrong-party negative tests).assertState/assertAsset: observe a fiber changed indirectly by a cross-fiber trigger, or real on-chain asset custody after a_transferAsset(reads/assets/{id}/state-proof).createAssetPolicy/mintAsset/applyMorphism: plain-JSON messages (the chain JAR decodes them; the published SDK doesn't yet export asset builders, andbatchSignsigns any object) — no SDK change needed.The slice (
48b1eab) —e2e-test/examples/riverdale-economy/One causal flow over 3 party-owned fibers (alice=manufacturer, bob=retailer, carol=consumer):
_triggers—fulfill_orderfires the retailer'sreceive_shipment(no declared dependency needed; onlyFiberPolicy.acceptedCallers, unset = any caller)._transferAssetcustody — the GOODS instance moves manufacturer-fiber → retailer-fiber in the same transition, asserted viaassertAsset. Recipient is a bare UUID string (parseRecipient→AssetHolder.Fiber).retailer.machine@1.0.0, upgraded to@2.0.0with a migration addingloyaltyPoints, then a v2-onlyredeem_loyalty.All wire shapes were verified against chain sources (quoted in
example.tsheader + README).Validation
The harness + slice are tsc-clean (the remaining e2e-test type errors are pre-existing SDK-staleness in untouched files; the suite runs via
tsx, no typecheck gate). Local cluster bring-up drifted across retries in my environment (stale GL0 genesis survivingdown --clean), so CI — fresh clone, no accumulated state — is the validation path. New isolated lane:riverdale-economy(concurrency 1, walletsalice,bob,carol, ordinalThreshold 15).Watch in the CI run (first-time-on-cluster unknowns)
createAssetPolicyconfirm: the/registry/{name}response shape for anAssetPolicyPackage(my confirm predicate scans any target variant'sversions.versions).assets/{id}/state-proof.recordcustody reads (holder/amount/sequenceNumber).SupplyPolicywith omitted optionals +morphisms:{}round-tripping the dropNulls signing canonical.Follow-up (not in this PR)
Full economy fan-out: consumer/bank/fed/gov fibers, RVD lending + commerce + tax-sweep, a negative-test flow, auction
_spawn, Fed v1→v2, and extra morphisms (Fractionalize/Stake/Burn).🤖 Generated with Claude Code