A real campaign, and what the site can honestly draw from one — 2026-08-30

Lane CAMPAIGN · step 3 of SIMVIZ's arc (docs/evidence/SIMULATOR_SERIES_VIZ_2026_08_30.md) · Artifacts apps/dclutch-web/public/campaign-series.json, apps/dclutch-web/app/campaign, apps/dclutch-web/scripts/campaign-series.mjs

SIMVIZ ended with a sized handover: the journey tier needed one ActivateFund step (~2h) and three tier1/bindings.json rows (~20min), and then a real campaign could feed the site a moving series. Neither of those was the blocker.

The journey tier is parked, and so is tier 1

run-journey.sh builds its run spec by shelling out to the journey binary's demo-market subcommand. That subcommand is retired at HEAD:

demo-market is retired: a standalone registry address cannot authenticate the
checked local Direct deployment. Supply a market compiled by
dclutch-local-successor-bootstrap local-private-validator-market-v1 to `run`

tools/gauntlet/run.sh dies at the same boundary and says so out loud — "successor local campaign is parked at the retired demo-market boundary" — so this is not a journey-only gap. Direct is deployment-bound now: a market input can only be compiled by DirectMarketCompilerOwnedV1::load_local, which authenticates a checked local mutable plan against a gate on disk and observes a live loopback deployment before anything compiles. The order the journey needs is therefore

prepare the checked-mutable substrate -> boot a validator over it ->
run the administration campaign through activation -> compile the market
against the LIVE deployment -> found -> keep going

which is exactly what tools/gauntlet/relayed-vertical/ already implements. Rebuilding it inside run-journey.sh is the work; ActivateFund and three bindings rows are the rest of the work, not the whole of it.

What was measured before that wall, at 3d259755:

What was driven instead

tools/gauntlet/relayed-vertical is journey-shaped by construction: it links journey/src/ledger.rs by #[path] and threads the same seven-law conservation ledger through every stage boundary, producing the same ObservationV1 census the series artifact already reads. It also closes SIMVIZ's named gap for free — its founding's post-Open readiness suffix drives CreateFund, ActivateFund and VerifyFundReady in one go, which is the step the journey does not have.

The walk refused, three times, and the refusal is the finding

Three independent campaigns were driven on hbox, each with its own freshly prepared checked-mutable substrate, its own stated seed, and its own port block — 31500, 31900, 32500. The three standing validators (26900, 27100, 29300) were read once and otherwise untouched; every validator these runs started is gone.

runwalkseed preimageverdict
1successdclutch/campaign/relayed-vertical/2026-08-30/successrefused
2success…/success-2refused
3failure…/failurerefused

All three at the same instruction, with the same code:

found the Market atomically: Lock, Found, Realize, Claims, Open (DCLTGMF3)
  Instruction 3 -> Core custom program error 0x3003
  = CoreSbfError::Reference, "Realm/Product/result-domain/Market identity
    linkage refused", 460,625 CU

Three things narrow it. The transaction immediately before — DCLTGMF3's own hostile probe, "refuses a substituted Claims request and rolls the whole founding back" — succeeds every time, so the account frame and the five-role release set are sound; it is the real Open leg that refuses. Custody's TransferChecked and CloseAccount inside the same instruction succeed and return data; Core refuses after them. And every CoreSbfError::Reference emission in the tree is in programs/dclutch-core-sbf/src/series_open.rs — the occurrence, ticket, capability-root and product-runtime identity joins.

RELAY-3 got a green from this same walk, gate and revision 75 minutes earlier, and that does not contradict this. The first hypothesis was that their run reused a warm substrate; it did not. run2/substrate/plan.json and run3/substrate/plan.json have different digests and were written 34 minutes apart, so run 3's substrate was as fresh as any of these. Same gate (story2-gate3, 98fe157d…, source 53354005). The one remaining input that differs is the seed, and the seed is what local-mutable-prepare-v1 derives the seven program identities from — which points at a key-varying find_program_address path: one seed missed it and three hit it.

53354005 predates 308c3dff — "the last two key-varying searches take a relayed bump, and the count is zero" — and 44972b78, so this may already be fixed on main. A checked release was therefore built at main HEAD and the identical walk re-run against it, which answers already fixed against live red with one measurement rather than an argument.

The experiment at main HEAD, and the two things it found instead

A complete checked release was built at main HEAD — 13 fresh SBF links, build freshness PASS, gate fb4c3cd1…, source 3d259755, tree 11526552… — and the identical walk driven against it under a fourth stated seed. It answered a different question than the one it was asked, twice.

The relayed vertical does not compile on main. Same tripwire class SIMVIZ documented for the journey, one commit later in the day:

error[E0433]: cannot find `structured_market` in `crate`
  --> local-validator/bootstrap/successor/src/local_mutable.rs:1399:20

local_mutable.rs grew a fourth capability branch (DCLUTCH_MARKET_CAPABILITY=structured) and calls crate::structured_market from it; this tier links seventeen of the successor's modules by #[path] and that was not one of them. It went off silently for the same reason the journey's did — nothing in CI builds this tier either. Fixed in 38b4a73e by linking the module: the closure is finite, so it is one mod and not a cascade, and forking the file would defeat the tripwire it exists to be.

And then it refused before it ever reached a founding. With the binary building, the administration campaign stops at the activation stage:

Error: activation cache progress: ReleaseSetSelectionMismatch

So the 0x3003 question is not answered. At 53354005 the vertical founds and refuses at Open; at 3d259755 it does not get as far as the founding at all. Two different walls, and the second one hides the first.

What the site draws, and whose run it is

No campaign this lane drove produced a series. The record published at apps/dclutch-web/public/campaign-series.json is RELAY-3's run 3 — the relayed graduation market's success walk, on hbox, 2026-08-30, at 533540056d61…, on a loopback validator at 127.0.0.1:27300. It is real local-validator campaign evidence from this repository's own machinery, and it is not this lane's run.

That provenance is not a footnote here; it is carried in the artifact (campaign.label, campaign.source_revision, campaign.rpc_origin, campaign.transcript_file), printed on the page in its own reading sentence, and pinned by a test that asserts the label reaches the rendered markup. The generator's --check re-derives the whole file from that transcript.

What it draws, all of it measured:

figurereading
odds, per cell5000 / 5000 basis points at all four boundaries — the founding's own distribution, unmoved because no fill has landed
claims issued500,000,000 on each of two outcomes
the vaultHoard 500,000,000 against 1,000,000,000 tracked against a 1,000,000,000 Mint supply — exactly collateralised for either answer, which is L4 in a picture
work, per boundary191 → 0 → 7 → 1 transactions; 7,047,284 → 0 → 109,316 → 157,869 CU
spend0 → 0 → 33,412,560 → 33,487,560 lamports off the fee payer
settlementcell 0 selected; one claim on it worth 1 collateral atom, one claim on cell 1 worth nothing
laws7 laws × 4 boundaries, none broken

The flat lines are flat and say so; the moving ones move. That is the actual answer to SIMVIZ's finding: a campaign record's motion is in the work and the phase, not in the market's quantities, until somebody trades.

The series schema, v3

dclutch-simulator-series-v3 is v2 plus the four things a campaign record has and a poller's census does not. Every field is optional; every v1 and v2 document still decodes, as a record that carries none of them.

addedwhy
stage per point"cycle 3" is not what happened there; "resolution funding active" is
transactions, compute_units, fee_lamports, payer_lamports per pointthe only volume a market with no fills has is the work its stages cost
claim_unit_atomsthe price primitive: without it a claim count is a count of nothing in particular
settlementwhich cell the terminal certificate selected — the only price move a market without fills ever makes, and the whole of one

Three refusals are new and all three are the same species — a figure laid under the wrong name:

Two exactness defects this found in its own pipeline

A u64 was silently rounded on the way in. The campaign transcript writes payer_lamports as a JSON number, and the first boundary of a run is 500000009955591120 — larger than 2^53. Plain JSON.parse hands back 500000009955591100: a lamport figure wrong by twenty, in a pipeline whose whole discipline is that quantities cross exactly. campaign-series.mjs now reads each number's raw source text (context.source, Node 21+) and keeps it as a string whenever the double cannot represent it. On a runtime with no context it refuses loudly rather than rounding into the artifact.

The claim unit was nearly guessed. The relayed vertical's transcript has no claim_unit_atoms field, and the first draft derived it as founding_admission.principal_atoms / worst outcome supply — which on this campaign gives exactly the right answer, 1, because the two happen to coincide. That coincidence is what would have made a wrong derivation invisible. The unit is now read from the conservation ledger's own L4 sentence — Hoard H >= worst outcome W x unit U = H, where U comes from the Registry's published ProductBasisV3.payout_scale — and a run whose ledger states more than one unit across its boundaries is refused rather than averaged.

What the page draws, and the rule it enforces

/campaign is a second artifact and a second page on purpose. /pulse draws a devnet census; this draws a local rehearsal. Folding them into one file would put a reader one merge away from believing a local founding happened on devnet.

The demo-vs-product rule is enforced in code, not by convention. campaignSeriesOrRefusalV1 refuses to draw any record whose cluster is not local and any record that names no campaign, and says why on the page. The local caveat is printed under every figure rather than once at the top, because a reader who lands mid-page or screenshots one chart must still be told what chain it came off. Both are pinned by test: the suite counts <figure> elements and asserts the caveat appears at least as many times.

How to reproduce the artifact

node apps/dclutch-web/scripts/campaign-series.mjs \
  --transcript ABS/transcript.json --evidence ABS/session-evidence.json \
  --label "…" --source-revision HEX40 --rpc-origin http://127.0.0.1:PORT/

--check re-derives the artifact from the same transcript and refuses if the committed file disagrees, ignoring only captured_at — a drift check that always fails is a check nobody runs. --out writes somewhere else, so the script can be exercised against a transcript that is not the one published.

What was NOT verified