diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4bd175c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+name: Samaritan CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: samaritan-ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.13.0
+ run_install: false
+
+ - name: Install Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - name: Install locked dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Reject moderate or greater judge-runtime advisories
+ run: pnpm security:audit
+
+ - name: Verify the complete clean-clone judge path
+ run: pnpm judge:check
+
+ - name: Verify tracked public artifacts
+ run: pnpm public:audit
+
+ - name: Install isolated Phase 0 capture dependencies
+ run: pnpm --dir phase0 install --frozen-lockfile
+
+ - name: Typecheck Phase 0 capture tools
+ run: pnpm --dir phase0 exec tsc -p tsconfig.json --noEmit
+
+ - name: Test Phase 0 capture tools
+ run: pnpm --dir phase0 test
diff --git a/.gitignore b/.gitignore
index 4c9e14a..35cd6f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,14 @@ data/
*.sqlite-wal
# Phase 0 local secrets and raw licensed captures.
+.wallet/
phase0/.wallet/
phase0/.tokens/
samples/
+
+# Local design explorations, social renders, and one-off verification helpers
+# are deliberately outside the public bounty submission. The production UI and
+# licence-safe judge artifacts live under apps/ and public/ respectively.
+content/
+design/
+.spain-belgium-verify*.ts
diff --git a/README.md b/README.md
index b6e1215..4d517c0 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,91 @@
# SAMARITAN
-**An autonomous sports-trading system.** Deterministic signal algorithms detect market inefficiencies in real time; Claude agents act as the judgment layer; every decision is scored, ranked, and anchored on-chain.
+**A governed autonomous sports-trading system.** Deterministic signal algorithms detect market inefficiencies; Claude acts only as a bounded judgment layer; deterministic code controls risk and paper execution; decisions are recorded in an append-only local ledger. Decision receipts verify offline and devnet anchoring tooling is implemented, but no anchor transaction has been submitted.
- **Testing ground:** TxODDS "Trading Tools and Agents" bounty — World Cup Hackathon (Superteam Earn / Solana). 16,000 USDT track prize. **Submission deadline: July 19, 2026.**
-- **Real mission:** a durable, money-making trading harness for sports and prediction markets. The hackathon is chapter one, not the book.
+- **Product direction:** durable decision-control and evidence infrastructure for sports and prediction-market teams. The bounty build is paper-only.
## Status
-**PHASE: PHASE 1 COMPLETE — PHASE 2 SIGNAL CORE STARTED.** The strict Node 22 workspace, canonical event contracts, evidence-bearing mapping registry, TXLine SSE clients, Gamma-refreshed Polymarket WebSocket source, streaming replay adapters, and append-only SQLite/DuckDB stores are implemented. The local research database holds 9,025,361 normalized TXLine outcomes and 85,249,582 mapped Polymarket points. Combined three-source live and two-source archive replay smokes pass. Tonight's synchronized capture supplies Phase-3 microstructure evidence while Phase 2 proceeds.
+**PHASE: PHASE 4 PAPER REASONING RUNTIME IMPLEMENTED — BOUNDED EVIDENCE COLLECTION IN PROGRESS.** The strict Node 22 workspace, canonical event contracts, evidence-bearing mapping registry, TXLine SSE clients, Gamma-refreshed Polymarket WebSocket source, streaming replay adapters, append-only SQLite/DuckDB stores, feature engine, detectors, chronological research harness, bounded Claude layer, deterministic paper risk, and paper portfolio are implemented. Live and captured-replay source adapters feed the same `runPaperSession` conductor; the strategy runtime receives canonical events rather than a live/replay mode flag. This is an implemented orchestration boundary, not a claim that Samaritan is already a deployed 24/7 autonomous service. The local research database holds 9,025,361 normalized TXLine outcomes and 85,249,582 mapped Polymarket points. The synchronized Spain-Belgium study found no supporting post-TXLine STALE_QUOTE case, so that detector remains disabled.
+
+**Evidence correction and registration:** Samaritan invalidated its original dynamic-total result after finding a future-informed selector and inflated emission semantics, preserved the artifact, repaired both defects, and reran the unchanged chronological boundary. The corrected Total Goals–only `CONSENSUS_MOVE` family has 38 held-out normalized buy cases across 18 fixtures, mean `+132.7` probability bps after the 100 bps historical proxy, and a fixture-clustered 95% interval of `+14.3` to `+243.9` bps. This remains sampled-price historical signal evidence—not alpha, profitability, or executable fills. On July 18 Deborah registered the exact corrected v2 configuration for forward paper observation only. No qualifying v2 observation existed at registration, and the real-money gate remains closed. See [the registration record](docs/10-paper-study-v2-registration.md), [corrected v4 report](docs/research/historical-gate-study-causal-economic-v4.md), and [v1 invalidation record](docs/research/historical-gate-study-v1-invalidation.md).
+
+The Phase-4 paper spine runs canonical events through deterministic detection, ledgers signals before model I/O, measures Haiku/Opus wall-clock decision latency from local observation time, and waits for both that latency and the current sports-order placement delay before deterministic risk and depth-aware simulation. Haiku and Opus can submit only strict structured judgments; neither can size or place an order. Real invocation metadata can be appended to a separate local hash chain. Receipt generation accepts a reference only after that local chain and exact record verify; the portable offline receipt does not independently prove membership. This is a local integrity audit—not Anthropic/provider attestation, independently signed billing proof, or proof of model quality. The July 12 v1 registration and empty ledgers remain preserved and suspended. Registered v2 uses fresh versioned ledgers; registration removes the human protocol-status block but does not bypass fixture admission. No trading credential is connected and the real-money gate remains closed.
+
+Both semifinal paired captures failed closed and are inadmissible. France–Spain stopped before kickoff. England–Argentina hit a watchdog-detected Polymarket stall and never produced complete terminal manifests, final score state, public resolution, or a verified mapping. Neither fixture may be replayed into v2 or used for a real-Claude claim.
+
+## Public release links
+
+- **Hosted judge MVP:** [samaritan-txline-2026.asaborodaniel.chatgpt.site](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/)
+- **One-call judge evidence:** [public read-only evidence endpoint](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/api/judge/evidence)
+- **Demo video:** `[YOUTUBE_OR_LOOM_URL — pending recording and public playback verification]`
+- **Public repository:** [github.com/Pavilion-devs/samaritan](https://github.com/Pavilion-devs/samaritan)
+
+The hosted MVP has passed signed-out desktop/mobile route checks and strict API-method checks. The video placeholder remains a release blocker and must be replaced only after public playback is watched end to end.
+
+## Judge quickstart
+
+Requirements: Node `22.23.1` and pnpm `11.13.0`.
+
+```bash
+corepack enable
+pnpm install --frozen-lockfile
+pnpm judge
+```
+
+Then open `http://127.0.0.1:4173`. `pnpm judge` first runs the complete clean-clone verification gate, then serves the read-only dashboard. It needs no TXLine credential, Anthropic key, wallet, RPC write, paid subscription, or private archive.
+
+Command also contains an optional live TXLine connectivity pulse. A hosted runtime enables it only when all three server-side values are present: `TXLINE_JWT`, `TXLINE_API_TOKEN`, and `TXLINE_SERVICE_LEVEL_ID=12`; local development may instead use the ignored, validated `phase0/.tokens/mainnet.json`. Missing or invalid credentials produce a truthful degraded card and do not affect the frozen judge evidence. The public pulse exposes only mainnet/SL12, check time, connection status, a rounded latency bucket, aggregate fixture count, and coarse HTTP-response freshness. It never returns fixture identities, names, kickoffs, raw rows, odds, probabilities, credentials, or study observations.
+
+On the exact final public commit, the verification gate must prove:
+
+- all TypeScript checks and the complete release test suite pass; the final count must be recorded only after that commit is frozen;
+- a deterministic synthetic case traverses the shared `runPaperSession` conductor from canonical events through settlement, using deterministic model stubs;
+- the public Decision Receipt verifies offline;
+- the frozen dashboard bundle contains only allowlisted, derived artifacts; and
+- the production dashboard builds from that clean-clone bundle.
+
+The public experience presents two judge-demo cases while keeping historical sampled-price research as a separate third evidence class:
+
+1. **Captured refusal:** the Spain–Belgium replay is real synchronized evidence and correctly ends in no trade. Only bucketed TXLine movement is public.
+2. **Synthetic full lifecycle:** `pnpm demo` sends 20 invented canonical events through the same `runPaperSession` conductor used by the source adapters, then exercises signal → triage → thesis → deterministic risk → intent → paper fill → close → settlement → receipt. Its two model boundaries are deterministic stubs, it makes zero external calls, and it is visibly synthetic and permanently excluded from performance evidence.
+
+Useful standalone commands:
+
+```bash
+pnpm demo
+pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json
+pnpm public:audit
+pnpm judge:check
+```
+
+The downloadable receipt is locally verified but **not Solana-anchored**. Offline prepare, explicitly human-gated devnet submission, and read-only network verification tooling exist; the bounty release intentionally accesses no signer and submits no transaction, so there is no explorer link.
+
+### Judge evidence map
+
+| Bounty criterion | Where to inspect |
+|---|---|
+| Core Functionality & Data Ingestion | **Command** optional derived connectivity pulse; **Matchroom** derived captured replay; [technical overview](docs/submission/technical-overview.md); exact endpoints in [API feedback](docs/submission/txline-api-feedback.md) |
+| Autonomous Operation | `pnpm demo` and **Proof** demonstrate deterministic end-to-end orchestration with model stubs—not an authentic Claude trading case; the forward-paper governance boundary is in [the v2 registration record](docs/10-paper-study-v2-registration.md) |
+| Clean, deterministic, defensible logic/code | Risk verdict and pre-action ledger in the downloadable receipt; detector/risk tests; identical canonical live/replay contracts |
+| Innovation/novelty | Strict Claude thesis boundary plus non-overridable deterministic authority, fail-closed evidence admission, and portable decision receipts |
+| Production readiness | `pnpm judge:check`, restart-safe portfolio reconstruction, public-artifact audit, security headers, explicit failure states, and the signed-out [hosted MVP](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/) |
+
+Every public surface separates **real captured retrospective evidence**, **historical sampled-price research**, and **synthetic engineering proof**. Registration is a governance fact, not a performance result.
-Phase 2 now has rolling velocity/acceleration/EWMA/CUSUM features, feed freshness and score context, research-gated `CONSENSUS_MOVE`/`XMARKET_DIVERGENCE`/`FADER_CANDIDATE`, timestamp-merged DuckDB replay, bounded forward-horizon labeling, streaming threshold-grid/classification/Brier/CLV metrics, and an evidence-weighted dynamic main-total selector. Numeric thresholds and selector weights remain deliberately unset outside explicit tests and smoke studies until the Phase-3 gate.
Decisions locked so far:
| Decision | Choice |
|---|---|
-| Execution modes | **Both** — paper trading (verifiable ledger) AND real-money Polymarket execution |
-| Wallets | Solana mainnet wallet ready (for free real-time data tier); devnet also for testing |
+| Execution modes | **Bounty: paper only.** Real-money Polymarket execution remains a gated post-bounty architecture item |
+| Wallets | No judge wallet and no execution wallet connected; any devnet proof submission is explicitly human-gated |
| Data source | TXLine (TxODDS) — free World Cup tier, Service Level 12 (real-time, mainnet) |
| v1 markets | Match Result + dynamically selected main full-time Over/Under only |
| Signal source reality | Free tier exposes `TXLineStablePriceDemargined` consensus, not named bookmakers; Momentum uses `CONSENSUS_MOVE`, not bookmaker leader-lag |
| AI | Claude — Opus 4.8 (analysis), Haiku 4.5 (triage); custom harness, NOT a prompt template |
| Claude budget | $200 operating target; $300 hard project ceiling |
-| Real-money envelope | $50 bankroll ceiling; $15 maximum aggregate live exposure initially; per-trade/drawdown caps set at the Phase-3 gate |
+| Real-money architecture | Disabled and outside the bounty proof; no production trading credential or adapter is connected |
| Stack | Node 22 + strict TypeScript/pnpm/vitest; SQLite + DuckDB |
## Docs
@@ -33,9 +97,16 @@ Decisions locked so far:
| [docs/03-strategy-playbook.md](docs/03-strategy-playbook.md) | The edges, the math, the risk framework — and the open questions we're discussing before building |
| [docs/04-algorithms.md](docs/04-algorithms.md) | The detector math: trigger conditions, free parameters, sizing, scoring — the spec the gate study fills with numbers |
| [docs/05-harness.md](docs/05-harness.md) | The agent harness machine room: case lifecycle, context assembly, two-speed deliberation, memory stores, hooks, budgets |
+| [docs/06-gate-study.md](docs/06-gate-study.md) | Phase-3 historical/live evidence, detector decisions, cost sensitivity, and the evidence behind the now-registered forward-paper-only v2 protocol |
+| [docs/UI.md](docs/UI.md) | Approved Floodlight UI/UX direction: information architecture, visual system, evidence states, replay behavior, and acceptance criteria |
| [docs/PHASE-1-STATUS.md](docs/PHASE-1-STATUS.md) | Implemented backbone contracts, verification evidence, and the remaining Phase-1 work |
| [docs/PHASE-2-STATUS.md](docs/PHASE-2-STATUS.md) | Current feature/detector/replay implementation, real-archive smoke evidence, and open Phase-2 work |
-| [plan.md](plan.md) | 7 phases to submission (July 18) with exit criteria and the mandatory Phase-3 gate |
+| [docs/PHASE-4-STATUS.md](docs/PHASE-4-STATUS.md) | Strict reasoning contracts, deterministic paper-risk/execution slice, verification, and remaining integration work |
+| [docs/08-winning-submission-plan.md](docs/08-winning-submission-plan.md) | Release-candidate win plan, evidence checkpoint, remaining authority gates, and demo strategy |
+| [docs/10-paper-study-v2-registration.md](docs/10-paper-study-v2-registration.md) | Deborah's exact forward-paper-only v2 registration, frozen limits, stopping rules, and closed real-money boundary |
+| [docs/submission/public-release-scope.md](docs/submission/public-release-scope.md) | Exact public inclusion/exclusion rules, clean-clone gate, and dependency isolation |
+| [docs/submission/third-party-notices.md](docs/submission/third-party-notices.md) | Data-service, font, asset, and software attribution boundaries |
+| [plan.md](plan.md) | 7 phases to the July 19 hard deadline, with exit criteria and the mandatory Phase-3 gate |
| [AGENTS.md](AGENTS.md) | Hard invariants + conventions for any AI agent writing Samaritan code |
## Timeline pressure
diff --git a/apps/dashboard/.openai/hosting.json b/apps/dashboard/.openai/hosting.json
new file mode 100644
index 0000000..8f94828
--- /dev/null
+++ b/apps/dashboard/.openai/hosting.json
@@ -0,0 +1 @@
+{"project_id":"appgprj_6a5b79be488081919908518fe0d45c8a"}
diff --git a/apps/dashboard/index.html b/apps/dashboard/index.html
new file mode 100644
index 0000000..fe246ca
--- /dev/null
+++ b/apps/dashboard/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Samaritan Matchroom
+
+
+
+
+
+
diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
new file mode 100644
index 0000000..87d8f70
--- /dev/null
+++ b/apps/dashboard/src/App.tsx
@@ -0,0 +1,13 @@
+import { CasebookApp } from "./Casebook";
+import { CommandApp } from "./Command";
+import { MatchroomApp } from "./Matchroom";
+import { ProofApp } from "./Proof";
+import { StudyApp } from "./Study";
+
+export function App() {
+ if (window.location.pathname === "/matchroom") return ;
+ if (window.location.pathname === "/casebook") return ;
+ if (window.location.pathname === "/study") return ;
+ if (window.location.pathname === "/proof") return ;
+ return ;
+}
diff --git a/apps/dashboard/src/Casebook.tsx b/apps/dashboard/src/Casebook.tsx
new file mode 100644
index 0000000..27df73c
--- /dev/null
+++ b/apps/dashboard/src/Casebook.tsx
@@ -0,0 +1,317 @@
+import { startTransition, useDeferredValue, useEffect, useState } from "react";
+import type { CasebookCaseSummary, CasebookSnapshot } from "../../../src/dash/public-contract";
+import { loadCasebook } from "./api";
+import { BrandMark, Icon, MobileNavigation, Navigation, ProvenanceBadge, Topbar } from "./Shell";
+
+type Filters = {
+ fixture: string;
+ market: string;
+ detector: string;
+ disposition: string;
+ outcome: string;
+ lane: string;
+ source: string;
+ date: string;
+};
+
+const emptyFilters: Filters = {
+ fixture: "",
+ market: "",
+ detector: "",
+ disposition: "",
+ outcome: "",
+ lane: "",
+ source: "",
+ date: ""
+};
+
+function percentage(value: number, digits = 2) {
+ return `${(value * 100).toFixed(digits)}%`;
+}
+
+function points(value: number) {
+ return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(2)}pp`;
+}
+
+function movementBps(value: number) {
+ return `${value >= 0 ? "+" : ""}${value.toLocaleString("en-US")} bps`;
+}
+
+function compactHash(value: string) {
+ return `${value.slice(0, 12)}…${value.slice(-8)}`;
+}
+
+function caseDate(value: string) {
+ return new Intl.DateTimeFormat("en-GB", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZone: "UTC",
+ timeZoneName: "short"
+ }).format(new Date(value));
+}
+
+function SelectFilter({ label, value, options, onChange }: {
+ label: string;
+ value: string;
+ options: string[];
+ onChange: (value: string) => void;
+}) {
+ return (
+
+ {label}
+ onChange(event.target.value)}>
+ All
+ {options.map((option) => {option} )}
+
+
+
+ );
+}
+
+function CasebookHero({ snapshot }: { snapshot: CasebookSnapshot }) {
+ return (
+
+
+
+
Every measured observation.One honest exemplar.
+
The index contains all {snapshot.corpus.marketEventCases} goal×market observations reported by this one verified capture replay. The detail pane is a deterministic exemplar, not the whole corpus or a performance sample.
+
+
+
Goal×market observations {snapshot.statistics.totalCases.toString().padStart(2, "0")} {snapshot.corpus.goalEvents} goals · one captured fixture
+
Moved before TXLine {snapshot.corpus.movedBeforeTxlineCases} pre-trigger repricing observed
+
No material reprice {snapshot.corpus.noMaterialRepriceCases} inside the 30-second window
+
Executed {snapshot.statistics.executedCases} operational execution not reached
+
Execution runtime Not entered retrospective research only
+
Full reported corpus projected {snapshot.statistics.reconciledCases} observations internally reconciled · capture identity checked separately
+
+
+ );
+}
+
+function CaseFilters({ snapshot, search, onSearch, filters, onFilter, onClear }: {
+ snapshot: CasebookSnapshot;
+ search: string;
+ onSearch: (value: string) => void;
+ filters: Filters;
+ onFilter: (key: keyof Filters, value: string) => void;
+ onClear: () => void;
+}) {
+ const dateOptions = [...new Set(snapshot.cases.map((item) => item.occurredAt.slice(0, 10)))].sort();
+ const hasFilters = Boolean(search || Object.values(filters).some(Boolean));
+ return (
+
+
+
+ Search cases
+ onSearch(event.target.value)} placeholder="Case, fixture, market, reason…" />
+ ⌘ K
+
+
+ onFilter("fixture", value)} />
+ onFilter("market", value)} />
+ onFilter("detector", value)} />
+ onFilter("disposition", value)} />
+ onFilter("outcome", value)} />
+ onFilter("lane", value)} />
+ onFilter("source", value)} />
+ onFilter("date", value)} />
+
+ Clear filters
+
+ );
+}
+
+function CaseIndex({ cases, total, nextEvidence, selectedCaseId }: {
+ cases: CasebookCaseSummary[];
+ total: number;
+ nextEvidence: CasebookSnapshot["nextEvidence"];
+ selectedCaseId: string;
+}) {
+ return (
+
+
+ {cases.length > 0 ? cases.map((item) => (
+
+ {caseDate(item.occurredAt)} {item.selectedExemplar ? "Detail exemplar" : "Corpus row"}
+ {item.homeCode} {item.awayCode} {item.fixtureLabel} {item.marketLabel}
+ {item.detector} {item.evidenceLane}
+ {item.disposition} {item.reason}
+
+ )) : (
+ No captured case matches Clear one or more filters. Casebook will not fabricate a result.
+ )}
+ {nextEvidence.label} {nextEvidence.detail}
+
+ );
+}
+
+function DecisionRail({ selected }: { selected: CasebookSnapshot["selectedCase"] }) {
+ const finalState = selected.decision.ordersPlaced === 0 ? "Execution not entered" : `${selected.decision.ordersPlaced} orders placed`;
+ return (
+
+
+
+ {selected.lifecycle.map((stage, index) => (
+
+ {String(index + 1).padStart(2, "0")}
+
+ {stage.label} {stage.detail}
+ {stage.timingLabel}
+
+ ))}
+
+
+ );
+}
+
+function EvidenceReadout({ selected }: { selected: CasebookSnapshot["selectedCase"] }) {
+ const readout = selected.evidenceReadout;
+ return (
+
+
+
+
TXLine movement {movementBps(readout.consensusMoveFromBaselineBps)} 25-bps bucket · level withheld
+
Best bid {percentage(readout.bestBid)} Executable book
+
Best ask {percentage(readout.bestAsk)} Executable entry
+
Spread {points(readout.spread)} At signal observation
+
Public-data boundary Relative only No exact TXLine level or series
+
Pre-trigger market move {movementBps(readout.preTriggerMarketMoveBps)} Before TXLine first seen
+
+ Timing conclusion {readout.movementConclusion} The public book was already repriced. The timing sequence did not support a tradeable stale quote.
+
+ );
+}
+
+function AnalysisBoundary({ selected }: { selected: CasebookSnapshot["selectedCase"] }) {
+ return (
+
+
+
+ Analyst boundary No operational thesis requested Retrospective feasibility ended before the analyst stage; no Claude thesis ran for this captured case.
Invalidation: {selected.analysis.invalidation}
+
+
+
+ Execution boundary Operational cost not applicable {selected.analysis.costReason}
Research-only path · execution and wallet layers were not entered
+
+
+ );
+}
+
+function EvidenceSequence({ selected }: { selected: CasebookSnapshot["selectedCase"] }) {
+ return (
+
+
+ Moment Source Observation Best ask Assessment
+ {selected.evidence.map((row) => (
+
+ {row.offsetLabel} {row.observedAt.slice(11, 23)} UTC
+ {row.source}
+ {row.observation}
+ {percentage(row.bestAsk)}
+ {row.assessment}
+
+ ))}
+
+ );
+}
+
+function CaseDetail({ snapshot }: { snapshot: CasebookSnapshot }) {
+ const selected = snapshot.selectedCase;
+ return (
+
+
+ Deterministic detail exemplar {selected.summary.caseId} {caseDate(selected.summary.occurredAt)}
+
+ {selected.match.home.code} {selected.match.home.name}
+ {selected.match.clockLabel} {selected.match.scoreAtCursor.home}–{selected.match.scoreAtCursor.away} goal {selected.summary.goalOrdinal} first seen
+ {selected.match.away.name} {selected.match.away.code}
+
+ Retrospective feasibility verdict {selected.summary.reason}. {selected.decision.explanation}
+ Open captured Matchroom replay
+
+
+
+
+
+
+ Underlying capture identity checked Corpus is locally committed, not a member of the replay identity hash
+ Identity hash {compactHash(selected.proof.identityHash)}
+ Corpus SHA-256 {compactHash(selected.proof.corpusCommitment)}
+ Derived evidence only
+
+
+ );
+}
+
+function matchesFilters(item: CasebookCaseSummary, search: string, filters: Filters) {
+ const haystack = [item.caseId, item.fixtureLabel, item.marketLabel, item.detector, item.reason, item.disposition].join(" ").toLowerCase();
+ return (!search || haystack.includes(search.toLowerCase())) &&
+ (!filters.fixture || item.fixtureLabel === filters.fixture) &&
+ (!filters.market || item.marketFamily === filters.market) &&
+ (!filters.detector || item.detector === filters.detector) &&
+ (!filters.disposition || item.disposition === filters.disposition) &&
+ (!filters.outcome || item.executionOutcome === filters.outcome) &&
+ (!filters.lane || item.evidenceLane === filters.lane) &&
+ (!filters.source || item.source === filters.source) &&
+ (!filters.date || item.occurredAt.startsWith(filters.date));
+}
+
+function CasebookView({ snapshot }: { snapshot: CasebookSnapshot }) {
+ const [search, setSearch] = useState("");
+ const deferredSearch = useDeferredValue(search.trim());
+ const [filters, setFilters] = useState(emptyFilters);
+ const visibleCases = snapshot.cases.filter((item) => matchesFilters(item, deferredSearch, filters));
+ const selectedExemplarVisible = visibleCases.some((item) => item.caseId === snapshot.corpus.selectedExemplar.caseId);
+ const updateFilter = (key: keyof Filters, value: string) => setFilters((current) => ({ ...current, [key]: value }));
+ const clear = () => { setSearch(""); setFilters(emptyFilters); };
+ return (
+
+
+
+
+
+
+
+
+
+ {selectedExemplarVisible ?
:
{visibleCases.length === 0 ? "No observation matches" : "Detail exemplar filtered out"} {visibleCases.length === 0 ? "The current filters exclude every reported observation." : "The visible rows remain part of the complete corpus, but only the explicitly marked exemplar has the three-state Matchroom detail."} Clear filters to reopen the selected exemplar.
Clear all filters }
+
+
+
+
+
+ );
+}
+
+function CasebookLoading() {
+ return Samaritan / Casebook Checking the captured corpus
Projecting every reported goal×market observation and reconciling it with the local replay evidence.
;
+}
+
+function CasebookError({ retry }: { retry: () => void }) {
+ return Fail-closed boundary Casebook evidence unavailable The captured replay record changed or could not be reconstructed. Samaritan will not display a partial or fabricated case.
Retry captured record ;
+}
+
+export function CasebookApp() {
+ const [snapshot, setSnapshot] = useState(null);
+ const [failure, setFailure] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailure(false);
+ loadCasebook(controller.signal)
+ .then((nextSnapshot) => startTransition(() => setSnapshot(nextSnapshot)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailure(true);
+ });
+ return () => controller.abort();
+ }, [attempt]);
+
+ if (failure) return { setSnapshot(null); setAttempt((value) => value + 1); }} />;
+ if (!snapshot) return ;
+ return ;
+}
diff --git a/apps/dashboard/src/Command.tsx b/apps/dashboard/src/Command.tsx
new file mode 100644
index 0000000..058a808
--- /dev/null
+++ b/apps/dashboard/src/Command.tsx
@@ -0,0 +1,291 @@
+import { startTransition, useEffect, useState } from "react";
+import type {
+ CommandFixture,
+ CommandSnapshot,
+ PublicBookPoint
+} from "../../../src/dash/public-contract";
+import { loadCommand, loadTxlinePulse, type TxlinePulse } from "./api";
+import { frozenCaptureWindowLabel } from "./command-schedule";
+import { BrandMark, Icon, MobileNavigation, Navigation, ProvenanceBadge, Topbar } from "./Shell";
+
+function percent(value: number, digits = 2) {
+ return `${(value * 100).toFixed(digits)}%`;
+}
+
+function movementBps(value: number) {
+ return `${value >= 0 ? "+" : ""}${value.toLocaleString("en-US")} bps`;
+}
+
+function compactHash(value: string) {
+ return `${value.slice(0, 10)}…${value.slice(-7)}`;
+}
+
+function dateParts(value: string) {
+ const date = new Date(value);
+ return {
+ day: new Intl.DateTimeFormat("en-GB", { day: "2-digit", timeZone: "UTC" }).format(date),
+ month: new Intl.DateTimeFormat("en-GB", { month: "short", timeZone: "UTC" }).format(date).toUpperCase(),
+ time: new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "UTC" }).format(date)
+ };
+}
+
+function MiniProbabilityChart({ featured }: { featured: CommandSnapshot["featuredCase"] }) {
+ const points: PublicBookPoint[] = featured.chart;
+ const frame = { left: 28, right: 624, top: 24, bottom: 194 };
+ const minimumOffset = Math.min(...points.map((point) => point.offsetMs));
+ const maximumOffset = Math.max(...points.map((point) => point.offsetMs));
+ const goalPoint = points.find((point) => point.offsetMs === 0);
+ if (!goalPoint || minimumOffset === maximumOffset) {
+ throw new Error("Featured capture chart is missing its trigger-aligned evidence window");
+ }
+ const x = (offset: number) => frame.left + ((offset - minimumOffset) / (maximumOffset - minimumOffset)) * (frame.right - frame.left);
+ const y = (probability: number) => frame.top + ((0.3 - probability) / 0.15) * (frame.bottom - frame.top);
+ const path = (value: (point: PublicBookPoint) => number) => points.map((point, index) => `${index === 0 ? "M" : "L"}${x(point.offsetMs).toFixed(1)} ${y(value(point)).toFixed(1)}`).join(" ");
+ const bid = path((point) => point.bestBid);
+ const ask = path((point) => point.bestAsk);
+ const bidReverse = [...points].reverse().map((point) => `L${x(point.offsetMs).toFixed(1)} ${y(point.bestBid).toFixed(1)}`).join(" ");
+ const goalX = x(0);
+ return (
+
+ Public executable book Best bid Best ask
+
+ {featured.home.name}–{featured.away.name} public {featured.marketOutcomeLabel} book around the first goal
+ The public executable bid and ask repriced before the goal reached Samaritan. Exact TXLine probability levels are withheld.
+
+
+
+
+
+ GOAL FIRST SEEN
+ T−5s {featured.clockLabel} T+30s
+
+
+ );
+}
+
+function SystemDeck({ snapshot }: { snapshot: CommandSnapshot }) {
+ return (
+
+
+
+ Snapshot posture {snapshot.system.label} {snapshot.system.detail}
+
+
+ {snapshot.system.feeds.map((feed) => (
+
+ {feed.statusLabel}{feed.label} {feed.detail}
+
+ ))}
+
+ Offline snapshot {new Date(snapshot.generatedAt).toISOString().slice(11, 16)} UTC Generated artifact · read only
+
+ );
+}
+
+function LiveTxlinePulse() {
+ const [pulse, setPulse] = useState(null);
+ const [failed, setFailed] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailed(false);
+ loadTxlinePulse(controller.signal)
+ .then((next) => startTransition(() => setPulse(next)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailed(true);
+ });
+ const refresh = window.setTimeout(() => setAttempt((value) => value + 1), 60_000);
+ return () => {
+ controller.abort();
+ window.clearTimeout(refresh);
+ };
+ }, [attempt]);
+
+ const connected = pulse?.status === "connected" && !failed;
+ const status = failed ? "degraded" : pulse?.status ?? "checking";
+ const freshness = failed ? "unknown" : pulse?.freshnessClass ?? "unknown";
+ const checkedAt = pulse?.checkedAt;
+ return (
+
+
+ LIVE DERIVED METADATA · NOT STUDY EVIDENCE
+ {status}
+
+
+
Official server-side check TXLine mainnet · SL12 {connected ? "Fixture snapshot reached through the credential boundary." : "Connectivity is unavailable or degraded; no feed state is inferred."}
+
Latency bucket {pulse?.latencyMsRounded === null || pulse === null ? "—" : `≤ ${pulse.latencyMsRounded} ms`}
+
Aggregate fixtures {pulse?.aggregateFixtureCount ?? "—"}
+
Response freshness {freshness}
+
Checked {checkedAt ? `${checkedAt.slice(11, 19)} UTC` : "Waiting"}
+
+ Connectivity metadata only. No fixture identity, kickoff, raw row, odds, probability, or v2 observation is exposed or admitted. setAttempt((value) => value + 1)}>Refresh
+
+ );
+}
+
+function FeaturedCase({ snapshot }: { snapshot: CommandSnapshot }) {
+ const featured = snapshot.featuredCase;
+ const minutes = Math.floor(featured.clockSeconds / 60);
+ const seconds = featured.clockSeconds % 60;
+ const orderLabel = featured.ordersPlaced === 0 ? "Execution runtime not entered" : `${featured.ordersPlaced} orders placed`;
+ return (
+
+
+
+
+ {featured.home.code} {featured.home.name}
+ {featured.clockLabel} {featured.scoreLabel} first goal seen
+ {featured.away.name} {featured.away.code}
+
+
Retrospective opportunity review
+
The market had already moved.
+
{featured.conclusion}
+
+
TXLine movement {movementBps(featured.consensusMoveFromBaselineBps)} 25-bps bucket · level withheld
+
Executable ask {percent(featured.bestAsk)}
+
Pre-trigger market move {movementBps(featured.preTriggerMarketMoveBps)}
+
+
Retrospective feasibility verdict {orderLabel} · research only
+
Open captured case replay
+
+
+
+
Market repriced first {movementBps(featured.preTriggerMarketMoveBps)} public {featured.marketOutcomeLabel} book move before TXLine first seen
+
+
+ );
+}
+
+function FixtureCard({ fixture, browserNowMs, lead }: { fixture: CommandFixture; browserNowMs: number; lead: boolean }) {
+ const kickoff = dateParts(fixture.kickoffUtc);
+ const capture = dateParts(fixture.captureStartUtc);
+ return (
+
+ {kickoff.day} {kickoff.month}
+
+
At export: {fixture.statusLabel}{frozenCaptureWindowLabel(fixture.captureStartUtc, fixture.captureEndUtc, browserNowMs)}
+
{fixture.home.code} {fixture.home.name} vs {fixture.away.name} {fixture.away.code}
+
Capture begins {capture.time} UTC Kickoff {kickoff.time} UTC
+
Capture only {fixture.identityStatus === "exact_match_confirmed" ? "Identity confirmed at export" : "Historical config reviewed at export"} · non-tradeable
+
+
+ );
+}
+
+function CaptureSchedule({ snapshot }: { snapshot: CommandSnapshot }) {
+ const [browserNowMs, setBrowserNowMs] = useState(() => Date.now());
+ useEffect(() => {
+ const refresh = window.setInterval(() => setBrowserNowMs(Date.now()), 60_000);
+ return () => window.clearInterval(refresh);
+ }, []);
+ return (
+
+
+ Frozen at export {new Date(snapshot.generatedAt).toISOString()}. Browser-clock timing describes configured windows, not live recorder state.
+
+ {snapshot.fixtureSchedule.map((fixture, index) => )}
+
+ Admission stays fail-closed Capture does not authorize a trade. Replay and paper-study gates still apply.
+
+ );
+}
+
+function RecentCases({ snapshot }: { snapshot: CommandSnapshot }) {
+ return (
+
+ );
+}
+
+function StudyPanel({ snapshot }: { snapshot: CommandSnapshot }) {
+ const study = snapshot.study;
+ const counts = study.qualifyingCounts;
+ const observationCopy = counts.signals === 0
+ ? "Zero fresh observations qualify."
+ : `${counts.signals} fresh signal${counts.signals === 1 ? " qualifies" : "s qualify"} from the registered v2 ledger.`;
+ return (
+
+
+ V2 is registered for forward paper only. {observationCopy} Invalidated v1 remains separate audit history; the real-money gate is closed.
+
+
{counts.filledMatches} / {study.requiredFilledMatches}fresh filled matches
+
{counts.fills} / {study.requiredFills}fresh fills
+
+ Protocol status Active forward paper Config hash {compactHash(study.configHash)}
+
+ );
+}
+
+function CommandProof({ snapshot }: { snapshot: CommandSnapshot }) {
+ const validLedgerChains = [snapshot.proof.bountyLedgerValid, snapshot.proof.longRunLedgerValid].filter(Boolean).length;
+ return (
+
+
+ {snapshot.proof.replayIdentityParity ? "PASS" : "FAIL"} Replay identity parity {snapshot.proof.replayIdentityParity ? "Deterministic replay matched twice" : "Local replay check failed"}
+ Canonical events {snapshot.proof.canonicalEvents.toLocaleString("en-US")} Evidence fixtures {snapshot.proof.evidenceFixtures} V2 study chains {validLedgerChains} valid Paired replays {snapshot.proof.pairedBookReplays}
+ Replay identity {compactHash(snapshot.proof.replayIdentityHash)}
+ Public observer boundary Derived evidence only · no wallet or raw feed access
+
+ );
+}
+
+function CommandView({ snapshot }: { snapshot: CommandSnapshot }) {
+ return (
+
+ );
+}
+
+function CommandLoading() {
+ return Samaritan / Command Assembling the evidence desk
Revalidating configured fixtures, offline ledger state, and the captured replay before anything is shown.
;
+}
+
+function CommandError({ retry }: { retry: () => void }) {
+ return Fail-closed boundary Command evidence unavailable A fixture or evidence identity could not be verified. Samaritan will not substitute stale or fabricated artifact state.
Retry offline load ;
+}
+
+export function CommandApp() {
+ const [snapshot, setSnapshot] = useState(null);
+ const [failure, setFailure] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailure(false);
+ loadCommand(controller.signal)
+ .then((nextSnapshot) => startTransition(() => setSnapshot(nextSnapshot)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailure(true);
+ });
+ return () => controller.abort();
+ }, [attempt]);
+
+ if (failure) return { setSnapshot(null); setAttempt((value) => value + 1); }} />;
+ if (!snapshot) return ;
+ return ;
+}
diff --git a/apps/dashboard/src/Matchroom.tsx b/apps/dashboard/src/Matchroom.tsx
new file mode 100644
index 0000000..60934a0
--- /dev/null
+++ b/apps/dashboard/src/Matchroom.tsx
@@ -0,0 +1,370 @@
+import {
+ startTransition,
+ useEffect,
+ useEffectEvent,
+ useState
+} from "react";
+import type {
+ MatchroomSnapshot,
+ PublicBookPoint,
+ ReplayState,
+ ReplayStepId
+} from "../../../src/dash/public-contract";
+import { loadMatchroom } from "./api";
+import { BrandMark, Icon, MobileNavigation, Navigation, ProvenanceBadge, Topbar } from "./Shell";
+
+const replayOrder: ReplayStepId[] = ["pre", "goal", "post"];
+
+function percent(value: number, digits = 2) {
+ return `${(value * 100).toFixed(digits)}%`;
+}
+
+function points(value: number) {
+ const amount = value * 100;
+ return `${amount >= 0 ? "+" : ""}${amount.toFixed(2)}pp`;
+}
+
+function movementBps(value: number) {
+ return `${value >= 0 ? "+" : ""}${value.toLocaleString("en-US")} bps`;
+}
+
+function utcTime(value: string) {
+ return `${new Date(value).toISOString().slice(11, 23)} UTC`;
+}
+
+function originalDate(value: string) {
+ return new Intl.DateTimeFormat("en-GB", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ timeZone: "UTC"
+ }).format(new Date(`${value}T00:00:00Z`));
+}
+
+function compactHash(value: string) {
+ return `${value.slice(0, 18)}…${value.slice(-7)}`;
+}
+
+function formatDuration(ms: number) {
+ return ms >= 1000 ? `${(ms / 1000).toFixed(3)}s` : `${ms}ms`;
+}
+
+function ContextBar({ snapshot }: { snapshot: MatchroomSnapshot }) {
+ return (
+
+
+
+ {snapshot.match.competition} · {snapshot.match.stage} {snapshot.match.home.name} vs {snapshot.match.away.name}
+
+
+
+ Overview
+ Markets
+ Decision
+ Evidence
+
+
+
+ Original match date {originalDate(snapshot.match.originalMatchDate)}
+
+
+ );
+}
+
+function MatchMasthead({ snapshot }: { snapshot: MatchroomSnapshot }) {
+ return (
+
+
+
+
Captured replay · public reference
+
+
+ {snapshot.match.home.code}
+ {snapshot.match.home.name} {snapshot.match.home.code}
+
+
+
{snapshot.match.clockLabel}
+
{snapshot.match.scoreAtCursor.home} : {snapshot.match.scoreAtCursor.away}
+
Goal {snapshot.match.goalOrdinal} first seen
+
+
+ {snapshot.match.away.name} {snapshot.match.away.code}
+ {snapshot.match.away.code}
+
+
+ Exact market
+ {snapshot.market.label}
+ {snapshot.market.period} · research-only mapping
+
+
+ );
+}
+
+const chartFrame = { left: 62, right: 830, top: 27, bottom: 282 };
+const chartMinimum = 0.15;
+const chartMaximum = 0.3;
+
+function chartX(offset: number, minimum: number, maximum: number) {
+ return chartFrame.left + ((offset - minimum) / (maximum - minimum)) * (chartFrame.right - chartFrame.left);
+}
+
+function chartY(probability: number) {
+ return chartFrame.top + ((chartMaximum - probability) / (chartMaximum - chartMinimum)) * (chartFrame.bottom - chartFrame.top);
+}
+
+function linePath(pointsToDraw: PublicBookPoint[], value: (point: PublicBookPoint) => number, minimum: number, maximum: number) {
+ return pointsToDraw.map((point, index) => `${index === 0 ? "M" : "L"}${chartX(point.offsetMs, minimum, maximum).toFixed(1)} ${chartY(value(point)).toFixed(1)}`).join(" ");
+}
+
+function chartSegments(snapshot: MatchroomSnapshot) {
+ const pointsToDraw = snapshot.replay.chart;
+ if (snapshot.replay.availabilityGaps.length === 0) return [pointsToDraw];
+ const firstSeen = Date.parse(snapshot.replay.firstSeenAt);
+ const gapOffsets = snapshot.replay.availabilityGaps.map((gap) => ({
+ start: Date.parse(gap.startedAt) - firstSeen,
+ end: Date.parse(gap.endedAt) - firstSeen
+ }));
+ const segments: PublicBookPoint[][] = [];
+ let current: PublicBookPoint[] = [];
+ for (const point of pointsToDraw) {
+ const previous = current.at(-1);
+ if (previous && gapOffsets.some((gap) => previous.offsetMs <= gap.end && point.offsetMs >= gap.start)) {
+ if (current.length > 1) segments.push(current);
+ current = [];
+ }
+ current.push(point);
+ }
+ if (current.length > 1) segments.push(current);
+ return segments;
+}
+
+function ProbabilityChart({ snapshot, state }: { snapshot: MatchroomSnapshot; state: ReplayState }) {
+ const minimum = Math.min(...snapshot.replay.chart.map((point) => point.offsetMs));
+ const maximum = Math.max(...snapshot.replay.chart.map((point) => point.offsetMs));
+ const segments = chartSegments(snapshot);
+ const cursorX = chartX(state.offsetMs, minimum, maximum);
+ const goalX = chartX(0, minimum, maximum);
+
+ return (
+
+
+ Public {snapshot.market.outcome} order book around selected goal {snapshot.match.goalOrdinal}
+ The public executable bid and ask repriced before the goal reached Samaritan. Exact TXLine probability levels are withheld.
+
+
+ {[27, 78, 129, 180, 231, 282].map((y) => )}
+
+
+ {[30, 27, 24, 21, 18, 15].map((value, index) => {value}% )}
+ T−5s Goal +5s +15s +25s +30s
+
+ {segments.map((segment, index) => {
+ const ask = linePath(segment, (point) => point.bestAsk, minimum, maximum);
+ const bidReverse = [...segment].reverse().map((point) => `L${chartX(point.offsetMs, minimum, maximum).toFixed(1)} ${chartY(point.bestBid).toFixed(1)}`).join(" ");
+ return ;
+ })}
+ {segments.map((segment, index) => point.bestBid, minimum, maximum)} />)}
+ {segments.map((segment, index) => point.bestAsk, minimum, maximum)} />)}
+ GOAL · {snapshot.match.clockLabel}
+
+
+ {utcTime(state.observedAt)} Derived summaries only · unavailable feed intervals render as gaps
+
+ );
+}
+
+function ProbabilityPanel({ snapshot, state, activeId, playing, onSelect, onTogglePlay }: {
+ snapshot: MatchroomSnapshot;
+ state: ReplayState;
+ activeId: ReplayStepId;
+ playing: boolean;
+ onSelect: (id: ReplayStepId) => void;
+ onTogglePlay: () => void;
+}) {
+ return (
+
+
+
+
TXLine movement {movementBps(state.consensusMoveFromBaselineBps)} 25-bps bucket from case baseline · absolute level withheld
+
Executable bid / ask {(state.bestBid * 100).toFixed(2)} / {(state.bestAsk * 100).toFixed(2)} %{points(state.spread)} measured spread
+
Pre-trigger market move {movementBps(snapshot.replay.preTriggerMarketMoveBps)} Public executable market · no TXLine level disclosed
+
+
+
+
{state.conclusionTitle} {state.conclusionBody}
+
Retrospective pass
+
+
+
+
+
+ {playing ? "Pause replay" : "Play replay"}
+
+
+ {snapshot.replay.states.map((item) => onSelect(item.id)}>{item.label} )}
+
+
1×
+
+
+ );
+}
+
+function DecisionRail({ snapshot, state }: { snapshot: MatchroomSnapshot; state: ReplayState }) {
+ const executionLabel = snapshot.decision.ordersPlaced === 0 ? "Execution not entered" : `${snapshot.decision.ordersPlaced} orders placed`;
+ return (
+
+
+
+
+
Feasibility verdict {snapshot.decision.label} {state.decisionExplanation}
+
+ Primary reason {snapshot.decision.primaryReason} Pre-trigger {snapshot.market.outcome} repricing: {movementBps(snapshot.replay.preTriggerMarketMoveBps)}
+
+ {snapshot.decision.stages.map((stage) => (
+
+
+ {stage.label} {stage.detail} {stage.timingLabel}
+
+ ))}
+
+
+
Execution runtime Not entered
+
Order result Not applicable
+
Wallet path Unavailable to this research lane
+
+ Real money disabled in bounty build Paper-only architecture · no order credential connected
+
+ );
+}
+
+function EvidencePanel({ snapshot, activeId }: { snapshot: MatchroomSnapshot; activeId: ReplayStepId }) {
+ const sourceClass = { Polymarket: "poly", TXLine: "tx", Samaritan: "sam" } as const;
+ const sourceMark = { Polymarket: "P", TXLine: "TX", Samaritan: "S" } as const;
+ return (
+
+
+
+
+ Moment Source Captured observation Executable ask Assessment
+
+ {snapshot.evidence.map((row) => (
+
+ {new Date(row.observedAt).toISOString().slice(11, 23)} {row.offsetLabel}
+ {sourceMark[row.source]} {row.source}
+ {row.observation}
+ {percent(row.bestAsk)} Best ask
+ {row.assessment}
+
+ ))}
+
+
+
+
+ );
+}
+
+function ProofPanel({ snapshot }: { snapshot: MatchroomSnapshot }) {
+ return (
+
+
+ {snapshot.proof.identityParity ? "PASS" : "FAIL"} Replay identity parity {snapshot.proof.identityParity ? "Replay matched twice" : "Verification failed"}
+
+
Canonical events {snapshot.proof.canonicalEvents.toLocaleString("en-US")}
+
First-seen latency {snapshot.replay.firstSeenLatencyMs} ms
+
Unavailable feed {snapshot.proof.feedOutageCount} gaps · {formatDuration(snapshot.proof.feedDowntimeMs)}
+
+ Replay identity hash {compactHash(snapshot.proof.identityHash)}
+ Public surface Derived summaries only No credentials, wallet controls, or raw feed redistribution
+
+ );
+}
+
+function LoadingScreen() {
+ return Samaritan / Matchroom Loading captured replay
Assembling licence-safe derived evidence. The retrospective case is shown only after local replay checks pass.
;
+}
+
+function ErrorScreen({ retry }: { retry: () => void }) {
+ return Fail-closed boundary Evidence unavailable The captured replay could not be checked, so Samaritan is not presenting partial or fabricated match evidence.
Retry captured replay ;
+}
+
+function Matchroom({ snapshot }: { snapshot: MatchroomSnapshot }) {
+ const [activeId, setActiveId] = useState(snapshot.replay.activeStateId);
+ const [playing, setPlaying] = useState(false);
+ const state = snapshot.replay.states.find((item) => item.id === activeId) ?? snapshot.replay.states[1]!;
+
+ const advanceReplay = useEffectEvent(() => {
+ const currentIndex = replayOrder.indexOf(activeId);
+ if (currentIndex >= replayOrder.length - 1) {
+ setPlaying(false);
+ return;
+ }
+ startTransition(() => setActiveId(replayOrder[currentIndex + 1]!));
+ });
+
+ useEffect(() => {
+ if (!playing) return;
+ const timer = window.setInterval(advanceReplay, 1400);
+ return () => window.clearInterval(timer);
+ }, [playing]);
+
+ function selectState(id: ReplayStepId) {
+ setPlaying(false);
+ startTransition(() => setActiveId(id));
+ }
+
+ function togglePlay() {
+ if (playing) {
+ setPlaying(false);
+ return;
+ }
+ if (activeId === "post") setActiveId("pre");
+ setPlaying(true);
+ }
+
+ return (
+
+ );
+}
+
+export function MatchroomApp() {
+ const [snapshot, setSnapshot] = useState(null);
+ const [failure, setFailure] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailure(false);
+ loadMatchroom(controller.signal)
+ .then((nextSnapshot) => startTransition(() => setSnapshot(nextSnapshot)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailure(true);
+ });
+ return () => controller.abort();
+ }, [attempt]);
+
+ if (failure) return { setSnapshot(null); setAttempt((value) => value + 1); }} />;
+ if (!snapshot) return ;
+ return ;
+}
diff --git a/apps/dashboard/src/Proof.tsx b/apps/dashboard/src/Proof.tsx
new file mode 100644
index 0000000..9eeaaa7
--- /dev/null
+++ b/apps/dashboard/src/Proof.tsx
@@ -0,0 +1,391 @@
+import { startTransition, useEffect, useState } from "react";
+import {
+ BrandMark,
+ Icon,
+ type IconName,
+ MobileNavigation,
+ Navigation,
+ ProvenanceBadge,
+ Topbar,
+ formatUsdMicros
+} from "./Shell";
+
+const RECEIPT_PATH = "/artifacts/dashboard/synthetic-decision-receipt.json";
+const VERIFY_COMMAND = "pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json";
+const RECEIPT_HASH_DOMAIN = "samaritan.decision-receipt/v1";
+
+type ProofLedgerEntry = {
+ atTsMs: number;
+ entryHash: string;
+ kind: string;
+ previousHash: string;
+ sequence: number;
+};
+
+type ProofReceipt = {
+ receiptType: string;
+ receiptId: string;
+ generatedAtTsMs: number;
+ provenance: {
+ evidenceClass: string;
+ label: string;
+ performanceUse: string;
+ synthetic: boolean;
+ };
+ agents: {
+ runs: Array<{
+ invocationClass: string;
+ model: string;
+ stage: string;
+ status: string;
+ actualCostNanoUsd: number;
+ }>;
+ totalActualCostNanoUsd: number;
+ };
+ lifecycle: {
+ finalStatus: string;
+ orderedEventKinds: string[];
+ triage: { decision: string; priority: string };
+ thesis: { recommendation: string } | null;
+ risk: {
+ decision: string;
+ realMoneyGate: string;
+ stakeMicroUsd: number | null;
+ } | null;
+ execution: {
+ adapter: string;
+ status: string;
+ } | null;
+ settlement: {
+ won: boolean;
+ } | null;
+ };
+ ledger: {
+ caseEntries: ProofLedgerEntry[];
+ finalHeadHash: string;
+ rowsAtGeneration: number;
+ verificationAtGeneration: string;
+ };
+ integrity: {
+ algorithm: string;
+ canonicalization: string;
+ domain: string;
+ receiptHash: string;
+ };
+ solanaAnchor: unknown | null;
+};
+
+export type BrowserProofVerification = {
+ valid: boolean;
+ canonicalHashValid: boolean;
+ disclosedChainValid: boolean;
+ lifecycleOrderValid: boolean;
+ syntheticBoundaryValid: boolean;
+ anchorAbsent: boolean;
+ issues: string[];
+};
+
+type ProofLoad = {
+ receipt: ProofReceipt;
+ verification: BrowserProofVerification;
+};
+
+function isRecord(value: unknown): value is Record {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+}
+
+function parseProofReceipt(value: unknown): ProofReceipt {
+ if (!isRecord(value)) throw new Error("Receipt is not an object");
+ if (!isRecord(value.provenance) || !isRecord(value.agents) || !isRecord(value.lifecycle)) {
+ throw new Error("Receipt is missing its provenance or lifecycle envelope");
+ }
+ if (!isRecord(value.ledger) || !isRecord(value.integrity) || !Array.isArray(value.ledger.caseEntries)) {
+ throw new Error("Receipt is missing its integrity or ledger envelope");
+ }
+ if (!Array.isArray(value.agents.runs) || !Array.isArray(value.lifecycle.orderedEventKinds)) {
+ throw new Error("Receipt is missing its disclosed agent or event sequence");
+ }
+ return value as unknown as ProofReceipt;
+}
+
+function stableJson(value: unknown): string {
+ const sort = (child: unknown): unknown => {
+ if (Array.isArray(child)) return child.map(sort);
+ if (isRecord(child)) {
+ return Object.fromEntries(
+ Object.entries(child)
+ .sort(([left], [right]) => left.localeCompare(right))
+ .map(([key, nested]) => [key, sort(nested)])
+ );
+ }
+ return child;
+ };
+ return JSON.stringify(sort(value));
+}
+
+async function sha256Hex(value: string): Promise {
+ if (!globalThis.crypto?.subtle) throw new Error("Browser SHA-256 is unavailable");
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
+}
+
+/**
+ * Recomputes the portable receipt commitment and checks only the relationships
+ * disclosed in the public artifact. The CLI below remains the strict schema and
+ * semantic verifier; neither verifier replays private source payloads.
+ */
+export async function verifyBrowserProof(value: unknown): Promise {
+ const receipt = parseProofReceipt(value);
+ const issues: string[] = [];
+ const entries = receipt.ledger.caseEntries;
+ const body = Object.fromEntries(Object.entries(receipt).filter(([key]) => key !== "integrity"));
+ const expectedReceiptHash = await sha256Hex(`${RECEIPT_HASH_DOMAIN}\n${stableJson(body)}`);
+ const canonicalHashValid =
+ receipt.integrity.algorithm === "sha256" &&
+ receipt.integrity.domain === RECEIPT_HASH_DOMAIN &&
+ receipt.integrity.canonicalization === "samaritan-stable-json-v1" &&
+ receipt.integrity.receiptHash === expectedReceiptHash;
+ if (!canonicalHashValid) issues.push("Canonical receipt hash mismatch");
+
+ const disclosedChainValid = entries.length > 0 && entries.every((entry, index) => {
+ if (index === 0) return Number.isSafeInteger(entry.sequence) && entry.sequence > 0;
+ const previous = entries[index - 1];
+ return previous !== undefined &&
+ entry.sequence === previous.sequence + 1 &&
+ entry.previousHash === previous.entryHash;
+ }) && entries.at(-1)?.entryHash === receipt.ledger.finalHeadHash;
+ if (!disclosedChainValid) issues.push("Disclosed ledger links or head do not reconcile");
+
+ const lifecycleOrderValid =
+ receipt.lifecycle.orderedEventKinds.length === entries.length &&
+ receipt.lifecycle.orderedEventKinds.every((kind, index) => entries[index]?.kind === kind);
+ if (!lifecycleOrderValid) issues.push("Lifecycle ordering does not match ledger ordering");
+
+ const syntheticBoundaryValid =
+ receipt.provenance.synthetic === true &&
+ receipt.provenance.evidenceClass === "synthetic_proving_fixture" &&
+ receipt.provenance.performanceUse === "excluded_synthetic" &&
+ receipt.agents.totalActualCostNanoUsd === 0 &&
+ receipt.agents.runs.every((run) => run.invocationClass === "synthetic_stub" && run.actualCostNanoUsd === 0);
+ if (!syntheticBoundaryValid) issues.push("Synthetic or zero-external-model-call boundary changed");
+
+ const anchorAbsent = receipt.solanaAnchor === null;
+ if (!anchorAbsent) issues.push("This page only accepts the frozen unanchored proving receipt");
+
+ return {
+ valid: issues.length === 0,
+ canonicalHashValid,
+ disclosedChainValid,
+ lifecycleOrderValid,
+ syntheticBoundaryValid,
+ anchorAbsent,
+ issues
+ };
+}
+
+function compactHash(value: string) {
+ return `${value.slice(0, 14)}…${value.slice(-10)}`;
+}
+
+function formatTime(value: number) {
+ return new Intl.DateTimeFormat("en-GB", {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ fractionalSecondDigits: 3,
+ timeZone: "UTC"
+ }).format(new Date(value));
+}
+
+const eventPresentation: Record = {
+ signal_received: { label: "Signal detected", eyebrow: "Deterministic detector", icon: "pulse" },
+ triage_decision: { label: "Triage escalated", eyebrow: "Haiku-shaped boundary", icon: "spark" },
+ thesis_submitted: { label: "Thesis submitted", eyebrow: "Opus-shaped boundary", icon: "case" },
+ analysis_completed: { label: "Analysis locked", eyebrow: "Strict schema exit", icon: "check" },
+ risk_verdict: { label: "Paper allowed. Money refused.", eyebrow: "Deterministic risk authority", icon: "shield" },
+ execution_intent: { label: "Paper intent ledgered", eyebrow: "Before action", icon: "lock" },
+ paper_execution: { label: "Paper adapter acted", eyebrow: "No venue order", icon: "play" },
+ case_terminal: { label: "Case terminal", eyebrow: "Runtime outcome", icon: "check" },
+ position_opened: { label: "Paper position opened", eyebrow: "Accounting", icon: "chart" },
+ position_closed: { label: "Paper position marked", eyebrow: "Pre-kickoff close", icon: "clock" },
+ position_settled: { label: "Paper position settled", eyebrow: "Synthetic resolution", icon: "proof" }
+};
+
+function eventDetail(receipt: ProofReceipt, kind: string) {
+ if (kind === "triage_decision") return `${receipt.lifecycle.triage.decision} · ${receipt.lifecycle.triage.priority}`;
+ if (kind === "thesis_submitted") return receipt.lifecycle.thesis?.recommendation ?? "No thesis";
+ if (kind === "risk_verdict") {
+ const risk = receipt.lifecycle.risk;
+ if (!risk) return "No risk verdict";
+ const stake = risk.stakeMicroUsd === null ? "no stake" : `${formatUsdMicros(risk.stakeMicroUsd)} paper stake`;
+ return `${risk.decision} paper · ${stake} · real money ${risk.realMoneyGate}`;
+ }
+ if (kind === "paper_execution") {
+ return `${receipt.lifecycle.execution?.status ?? "unknown"} · ${receipt.lifecycle.execution?.adapter ?? "paper"} adapter`;
+ }
+ if (kind === "case_terminal") return receipt.lifecycle.finalStatus.replaceAll("_", " ");
+ if (kind === "position_settled") return receipt.lifecycle.settlement?.won ? "synthetic outcome: won" : "synthetic outcome: lost";
+ return kind.replaceAll("_", " ");
+}
+
+function ProofHero({ receipt }: { receipt: ProofReceipt }) {
+ const risk = receipt.lifecycle.risk;
+ return (
+
+
+
+
A decision boundary, made inspectable
+
THE MONEY PATHSAMARITAN REFUSED.
+
What was refused: every path from an agent recommendation to real money. A deterministic Opus-shaped stub recommended a paper trade; code-owned risk kept the money gate closed and exercised only the synthetic paper lifecycle.
+
+
+
Decision boundaryZero real orders
+
+
01 · Agent-shaped thesis {receipt.lifecycle.thesis?.recommendation.replaceAll("_", " ") ?? "No thesis"} May recommend. Cannot size or execute.
+
+
02 · Deterministic authority {risk?.decision ?? "closed"} paper {risk?.stakeMicroUsd ? formatUsdMicros(risk.stakeMicroUsd) : "$0.00"} fixed · real money {risk?.realMoneyGate ?? "closed"}
+
+
03 · Money boundary REFUSED Paper adapter only · no wallet or venue order
+
+
An LLM never touches money here. The synthetic case continues after the refusal solely to prove paper execution, close, settlement, and receipt wiring.
+
+
+ );
+}
+
+function ProofTruthStrip({ receipt }: { receipt: ProofReceipt }) {
+ return (
+
+ Evidence class Synthetic proving fixture No real match or licensed feed record
+ Models {receipt.agents.runs.length} deterministic stubs Zero Anthropic API cost or calls
+ Performance use Excluded Not alpha, P&L, or fill evidence
+ Execution Paper only Zero real orders · gate closed
+ Solana Not submitted No anchor or network verification
+
+ );
+}
+
+function ProofLedger({ receipt }: { receipt: ProofReceipt }) {
+ return (
+
+
+ Seq. Boundary Committed event UTC Entry hash
+
+ {receipt.ledger.caseEntries.map((entry) => {
+ const presentation = eventPresentation[entry.kind] ?? { label: entry.kind, eyebrow: "Committed event", icon: "proof" as const };
+ return (
+
+ {String(entry.sequence).padStart(2, "0")}
+
+ {presentation.eyebrow} {presentation.label} {eventDetail(receipt, entry.kind)}
+ {formatTime(entry.atTsMs)}
+ {compactHash(entry.entryHash)}
+
+ );
+ })}
+
+ {receipt.ledger.rowsAtGeneration} total ledger rows at generation The case events above follow the study-initialization commitment; each row links to the previous disclosed hash.
+
+ );
+}
+
+function VerificationInspector({ receipt, verification }: ProofLoad) {
+ const checks = [
+ { label: "Canonical receipt hash", detail: "Recomputed in this browser", pass: verification.canonicalHashValid },
+ { label: "Disclosed ledger links", detail: "Sequences, links, and final head reconcile", pass: verification.disclosedChainValid },
+ { label: "Lifecycle ordering", detail: "Event kinds match committed row order", pass: verification.lifecycleOrderValid },
+ { label: "Synthetic boundary", detail: "Stubs, zero model cost, performance excluded", pass: verification.syntheticBoundaryValid },
+ { label: "External anchor", detail: "Absent; no network verification claimed", pass: verification.anchorAbsent }
+ ];
+ return (
+
+
+
+
+ {checks.map((check) =>
{check.label} {check.detail}
)}
+
+ Ledger verification at generation {receipt.ledger.verificationAtGeneration.replaceAll("_", " ")}
+
+
+
+ Receipt hash {receipt.integrity.receiptHash}
+ Committed ledger head {receipt.ledger.finalHeadHash}
+ Receipt ID {receipt.receiptId}
+ SHA-256 {receipt.integrity.canonicalization} Local not externally timestamped
+
+
+ Licence-safe public artifact Download receipt JSON
+
+
+ );
+}
+
+function ReproduceProof() {
+ return (
+
+ Do not trust the screenshot Verify the frozen receipt offline. The repository verifier enforces the strict schema, recomputes the canonical receipt hash, and checks disclosed lifecycle relationships. It does not query TXLine, Polymarket, Anthropic, a wallet, or Solana.
+
+ Assurance boundary: this verifies portable commitments and disclosed relationships. It does not replay private source payloads, prove a real model invocation, establish profitability, or claim an external timestamp.
+
+ );
+}
+
+function ProofView({ receipt, verification }: ProofLoad) {
+ return (
+
+
+
+
+
+
+
+
+
+
Synthetic engineering proof. Permanently excluded from every performance claim. No external model call · no real order · no wallet · no submitted Solana anchor.
+
+
+
+
+ );
+}
+
+function ProofLoading() {
+ return Samaritan / Proof Recomputing the receipt
Checking the frozen canonical commitment, disclosed ledger links, event order, provenance, and anchor boundary.
;
+}
+
+function ProofError({ retry }: { retry: () => void }) {
+ return Fail-closed evidence boundary Proof did not reconcile The frozen receipt could not be loaded or its public commitments changed. Samaritan will not show a partial verification result.
Retry proof load ;
+}
+
+export function ProofApp() {
+ const [loaded, setLoaded] = useState(null);
+ const [failure, setFailure] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailure(false);
+ fetch(RECEIPT_PATH, { headers: { accept: "application/json" }, signal: controller.signal })
+ .then(async (response) => {
+ if (!response.ok) throw new Error(`Receipt request failed with status ${response.status}`);
+ const value = await response.json() as unknown;
+ const receipt = parseProofReceipt(value);
+ const verification = await verifyBrowserProof(receipt);
+ if (!verification.valid) throw new Error(verification.issues.join("; "));
+ return { receipt, verification };
+ })
+ .then((next) => startTransition(() => setLoaded(next)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailure(true);
+ });
+ return () => controller.abort();
+ }, [attempt]);
+
+ if (failure) return { setLoaded(null); setAttempt((value) => value + 1); }} />;
+ if (!loaded) return ;
+ return ;
+}
diff --git a/apps/dashboard/src/Shell.tsx b/apps/dashboard/src/Shell.tsx
new file mode 100644
index 0000000..2b76699
--- /dev/null
+++ b/apps/dashboard/src/Shell.tsx
@@ -0,0 +1,124 @@
+import type { ReactNode } from "react";
+
+export type ProductRoute = "command" | "matchroom" | "casebook" | "study" | "proof";
+
+export type IconName =
+ | "arrow"
+ | "case"
+ | "chart"
+ | "check"
+ | "chevron"
+ | "clock"
+ | "command"
+ | "cup"
+ | "lock"
+ | "minus"
+ | "pause"
+ | "play"
+ | "proof"
+ | "pulse"
+ | "replay"
+ | "shield"
+ | "spark"
+ | "system";
+
+export function Icon({ name, className }: { name: IconName; className?: string }) {
+ const paths: Record = {
+ arrow: <> >,
+ case: <> >,
+ chart: ,
+ check: ,
+ chevron: ,
+ clock: <> >,
+ command: <> >,
+ cup: ,
+ lock: <> >,
+ minus: ,
+ pause: ,
+ play: ,
+ proof: <> >,
+ pulse: ,
+ replay: <> >,
+ shield: <> >,
+ spark: <> >,
+ system: <> >
+ };
+ return {paths[name]} ;
+}
+
+export function BrandMark() {
+ return ;
+}
+
+export type ProvenanceTone = "capture" | "configured" | "historical" | "offline" | "synthetic";
+
+export function ProvenanceBadge({ label, tone }: { label: string; tone: ProvenanceTone }) {
+ return {label} ;
+}
+
+export function formatUsdMicros(value: number, minimumFractionDigits = 2) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits,
+ maximumFractionDigits: 2
+ }).format(value / 1_000_000);
+}
+
+export function Navigation({ active, caseCount }: { active: ProductRoute; caseCount?: number }) {
+ return (
+
+ );
+}
+
+export function MobileNavigation({ active }: { active: ProductRoute }) {
+ return (
+
+ Command
+ Match
+ Cases
+ Study
+ Proof
+
+ );
+}
+
+export function Topbar({ title, modeLabel, modeClass }: {
+ title: string;
+ modeLabel: string;
+ modeClass: "replay" | "offline";
+}) {
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/Study.tsx b/apps/dashboard/src/Study.tsx
new file mode 100644
index 0000000..6e78ce5
--- /dev/null
+++ b/apps/dashboard/src/Study.tsx
@@ -0,0 +1,321 @@
+import { startTransition, useEffect, useState } from "react";
+import type { StudyGuardrails, StudySnapshot } from "../../../src/dash/public-contract";
+import { loadStudy } from "./api";
+import { BrandMark, Icon, MobileNavigation, Navigation, ProvenanceBadge, Topbar } from "./Shell";
+
+function money(microUsd: number) {
+ return `$${(microUsd / 1_000_000).toFixed(2)}`;
+}
+
+function percentage(value: number, digits = 0) {
+ return `${(value * 100).toFixed(digits)}%`;
+}
+
+function bps(value: number | null) {
+ return value === null ? "—" : `${value >= 0 ? "+" : ""}${value.toFixed(1)} bps`;
+}
+
+function compactHash(value: string) {
+ return `${value.slice(0, 12)}…${value.slice(-8)}`;
+}
+
+function ProgressMeter({ value, target, label }: { value: number; target: number; label: string }) {
+ const progress = Math.min(100, target === 0 ? 0 : value / target * 100);
+ return (
+
+
{value} / {target}{label}
+
+
+ );
+}
+
+function StudyHero({ snapshot }: { snapshot: StudySnapshot }) {
+ const protocol = snapshot.protocol;
+ const evaluation = protocol.evaluation;
+ const counts = protocol.qualifyingCounts;
+ return (
+
+
+
+
V2 is registered.Evidence starts fresh.
+
Deborah activated the corrected protocol for forward paper observation only. Historical, retrospective, and synthetic cases remain excluded; zero observations currently qualify.
+
Registered primary endpoint {evaluation.primaryEndpoint}
+
+
+
Registered · active forward paperSince {new Date(protocol.registeredAt).toISOString().slice(0, 10)}
+
+
+
Fresh observations {counts.filledMatches} / {evaluation.minimumFilledMatches}qualifying matches
+
Fresh fills {counts.fills} / {evaluation.minimumFills}qualifying fills
+
+
+
Status registered Evidence fresh only Real money closed
+
+
+ );
+}
+
+function LaneStrip({ snapshot }: { snapshot: StudySnapshot }) {
+ const bounty = snapshot.historicalV1.lanes.bounty;
+ const longRun = snapshot.historicalV1.lanes.longRun;
+ return (
+
+ Historical decision lane {longRun.label} {longRun.reason} {longRun.statusLabel}
+ Historical demo lane {bounty.label} {bounty.reason} {bounty.statusLabel}
+ V1 remains invalidated audit history Both zero-observation chains are preserved and can never satisfy registered v2.
+
+ );
+}
+
+function CorrectedHistoricalEvidence({ snapshot }: { snapshot: StudySnapshot }) {
+ const candidate = snapshot.correctedHistoricalCandidate;
+ return (
+
+
+
+
Held-out mean after {candidate.costProxyBps} bps proxy {bps(candidate.meanNetAfterCostProxyBps)} 95% fixture-clustered CI {bps(candidate.matchClustered95Bps.low)} to {bps(candidate.matchClustered95Bps.high)}
+
Training cases {candidate.trainingNormalizedCases} Normalized before held-out review
+
Held-out cases {candidate.heldoutNormalizedCases} Across {candidate.heldoutFixtures} fixtures
+
Evidence class Sampled prices No historical bid, ask, depth, or fill proof
+
+ Historical source status preserved {candidate.claimBoundary}
+ Protocol {candidate.protocolId} Configuration {compactHash(candidate.configurationHash)}
+
+ );
+}
+
+function SyntheticReceiptProof({ snapshot }: { snapshot: StudySnapshot }) {
+ const proof = snapshot.syntheticProof;
+ const steps = ["Signal", "Triage stub", "Thesis stub", "Hard risk", "Paper fill", "Close", "Settlement"];
+ return (
+
+
+
+
+
+
Closed-world proving fixture {proof.label} {proof.explanation} Deterministic agent stubs replace external model calls.
+
+
+ Lifecycle Filled + settled
+ External calls {proof.externalCalls}
+ Performance use Excluded
+ Solana anchor Not submitted
+
+
+
+ {steps.map((step, index) => {step} {index < steps.length - 1 ? : null} )}
+
+
+
+ );
+}
+
+function SealedEndpoint({ snapshot }: { snapshot: StudySnapshot }) {
+ const evaluation = snapshot.protocol.evaluation;
+ const counts = snapshot.protocol.qualifyingCounts;
+ const countLabel = `${counts.signals} qualifying signal${counts.signals === 1 ? "" : "s"}`;
+ return (
+
+
+
+
Reject zone 0 bps Accept zone
+
+
No current endpoint {counts.filledMatches} / {evaluation.minimumFilledMatches} filled matches · {counts.fills} / {evaluation.minimumFills} fills Only fresh post-registration evidence may open the registered endpoint; old research, v1 rows, and synthetic proof are excluded.
+
Bootstrap {evaluation.bootstrapIterations.toLocaleString("en-US")} iterations Resampling block Whole matches Control {evaluation.randomDirectionControl}
+
+
+ {snapshot.decisionRules.accept.map((rule, index) =>
{String(index + 1).padStart(2, "0")} {rule}
)}
+
+
+ );
+}
+
+function OpenEndpoint({ snapshot }: { snapshot: StudySnapshot }) {
+ if (snapshot.results.visibility !== "open") return null;
+ const endpoints = snapshot.results.endpoints;
+ return (
+
+
+ {endpoints ?
+
Mean net executable CLV {bps(endpoints.meanNetClvBps)} 95% CI [{bps(endpoints.netClvInterval.low)}, {bps(endpoints.netClvInterval.high)}]
+
Settlement P&L {money(endpoints.meanSettlementPnlMicroUsd)} Mean resolved result
+
Random control {bps(endpoints.randomDirectionControlClvBps)} Matched observed costs
+
Positive matches {percentage(endpoints.fractionSettledMatchesNetPositive)} Settled match fraction
+
: Endpoint evidence incomplete The minimum sample opened the rows, but complete close or settlement evidence is still required.
}
+
+ );
+}
+
+function guardrailState(guardrails: StudyGuardrails | null, key: keyof StudyGuardrails): "pass" | "fail" | "waiting" {
+ if (!guardrails) return "waiting";
+ const value = guardrails[key];
+ return value === true ? "pass" : "fail";
+}
+
+function GuardrailsPanel({ snapshot }: { snapshot: StudySnapshot }) {
+ const thresholds = snapshot.protocol.guardrailThresholds;
+ const observed = snapshot.results.visibility === "open" ? snapshot.results.guardrails : null;
+ const items = [
+ { label: "Fill rate", threshold: `≥ ${percentage(thresholds.minimumFillRate)}`, value: observed ? percentage(observed.fillRate, 1) : "Awaiting fills", state: guardrailState(observed, "fillRatePassed") },
+ { label: "Mean slippage", threshold: `≤ ${thresholds.maximumMeanSlippageBps} bps`, value: observed ? bps(observed.meanSlippageBps) : "Awaiting fills", state: guardrailState(observed, "slippagePassed") },
+ { label: "Max drawdown", threshold: `≤ ${money(thresholds.maximumDrawdownMicroUsd)}`, value: observed ? money(observed.maxDrawdownMicroUsd) : "No observations", state: guardrailState(observed, "drawdownPassed") },
+ { label: "Selected depth", threshold: "Every trade", value: observed ? (observed.selectedDepthComplete ? "Complete" : "Incomplete") : "Awaiting fills", state: guardrailState(observed, "selectedDepthComplete") }
+ ];
+ return (
+
+
+ Positive CLV cannot rescue a study that fails execution quality or risk control.
+
+ {items.map((item) =>
{item.state === "waiting" ? : } {item.label} {item.value} {item.threshold}
)}
+
+ Runtime market metadata required Missing depth, fee, tick, or close evidence fails closed.
+
+ );
+}
+
+function CandidatePanel({ snapshot }: { snapshot: StudySnapshot }) {
+ const candidate = snapshot.protocol.candidate;
+ return (
+
+
+ Enabled detector {candidate.detector} {candidate.marketFamily}
+
+ Move Z {candidate.moveAbsZ.toFixed(1)}
+ CUSUM {candidate.cusumThresholdBps.toFixed(0)} bps
+ Minimum gap {candidate.minimumGapBps.toFixed(0)} bps
+ Updates {candidate.minimumUpdates}
+
+ Dynamic total selector {candidate.selector} ≥ {candidate.minimumCoveragePoints.toLocaleString("en-US")} points · within {percentage(candidate.maximumDistanceFromEven)} of even
+ Registered at {snapshot.protocol.registeredAt}
+ Configuration identity {compactHash(snapshot.protocol.configHash)}
+
+ );
+}
+
+function RiskPanel({ snapshot }: { snapshot: StudySnapshot }) {
+ const risk = snapshot.protocol.risk;
+ return (
+
+
+
+
Bankroll {money(risk.bankrollMicroUsd)} Paper notional
+
+
+ Per trade {money(risk.perTradeStakeMicroUsd)} Max exposure {money(risk.aggregateExposureMicroUsd)} Drawdown stop {money(risk.drawdownStopMicroUsd)}
+ Deterministic limits No agent can increase stake or exposure, or override the drawdown stop.
+
+ );
+}
+
+function EvidenceQueue({ snapshot }: { snapshot: StudySnapshot }) {
+ const universe = snapshot.fixtureUniverse;
+ return (
+
+
+
+
{universe.evidenceFixtures} Historical evidence fixtures
{universe.pairedBookReplays} Retrospective paired replay
{snapshot.protocol.qualifyingCounts.matches} V2 qualifying
+
+ {universe.signalResearchOnly} sampled-history fixtures remain research-only They lack synchronized executable depth and cannot count toward the long-run gate.
+ Inspect fresh capture status
+
+ );
+}
+
+function DecisionRules({ snapshot }: { snapshot: StudySnapshot }) {
+ const groups = [
+ { label: "Accept", className: "accept", icon: "check" as const, rules: snapshot.decisionRules.accept },
+ { label: "Reject", className: "reject", icon: "minus" as const, rules: snapshot.decisionRules.reject },
+ { label: "Inconclusive", className: "inconclusive", icon: "clock" as const, rules: snapshot.decisionRules.inconclusive }
+ ];
+ return (
+
+
+ {groups.map((group) =>
{group.rules.map((rule) =>
{rule}
)}
)}
+ Registration authorizes forward paper observation only. No study result can unlock real money without a separate future decision.
+
+ );
+}
+
+function StudyProof({ snapshot }: { snapshot: StudySnapshot }) {
+ return (
+
+ Both preserved v1 chains verify Local append-only audit history · not an external anchor
+ V1 long-run head {compactHash(snapshot.historicalV1.lanes.longRun.chain.headHash)}
+ V1 bounty head {compactHash(snapshot.historicalV1.lanes.bounty.chain.headHash)}
+ Current protocol {snapshot.protocol.version}
+ Derived evidence only
+
+ );
+}
+
+function StudyRows({ snapshot }: { snapshot: StudySnapshot }) {
+ if (snapshot.results.visibility !== "open") return null;
+ return (
+
+
+ Fixture Total Signals Fills Fill rate Slippage Net CLV P&L
{snapshot.results.rows.map((row) =>
{row.fixtureRef} {row.kickoffUtc.slice(0, 10)} {row.selectedLine.toFixed(1)} {row.signals} {row.fills} {percentage(row.fillRate, 1)} {bps(row.meanSlippageBps)} {bps(row.netClvBps)} {row.settlementPnlMicroUsd === null ? "—" : money(row.settlementPnlMicroUsd)}
)}
+
+ );
+}
+
+function StudyView({ snapshot }: { snapshot: StudySnapshot }) {
+ return (
+
+
+
+
+
+
+
+
+
+
{snapshot.results.visibility === "sealed" ? : }
+
+
+
+
+
+
+
+
+ );
+}
+
+function StudyLoading() {
+ return Samaritan / Study Reconstructing study governance
Verifying current v2 registration and the separate preserved zero-observation v1 audit.
;
+}
+
+function StudyError({ retry }: { retry: () => void }) {
+ return Fail-closed boundary Study evidence unavailable The protocol identity, ledger chain, or sealing state could not be reconciled. Samaritan will not expose a partial profitability result.
Retry verified load ;
+}
+
+export function StudyApp() {
+ const [snapshot, setSnapshot] = useState(null);
+ const [failure, setFailure] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setFailure(false);
+ loadStudy(controller.signal)
+ .then((nextSnapshot) => startTransition(() => setSnapshot(nextSnapshot)))
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ setFailure(true);
+ });
+ return () => controller.abort();
+ }, [attempt]);
+
+ if (failure) return { setSnapshot(null); setAttempt((value) => value + 1); }} />;
+ if (!snapshot) return ;
+ return ;
+}
diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts
new file mode 100644
index 0000000..d784f97
--- /dev/null
+++ b/apps/dashboard/src/api.ts
@@ -0,0 +1,94 @@
+import type {
+ CasebookApiResponse,
+ CasebookSnapshot,
+ CommandApiResponse,
+ CommandSnapshot,
+ DashboardApiResponse,
+ MatchroomSnapshot,
+ StudyApiResponse,
+ StudySnapshot
+} from "../../../src/dash/public-contract";
+
+export const MATCHROOM_API_PATH = "/api/v1/matchroom/paired-spain-belgium-2026-07-10";
+export const COMMAND_API_PATH = "/api/v1/command";
+export const CASEBOOK_API_PATH = "/api/v1/casebook";
+export const STUDY_API_PATH = "/api/v1/study";
+export const TXLINE_PULSE_API_PATH = "/api/v1/txline/pulse";
+
+export type TxlinePulse = {
+ network: "mainnet";
+ serviceLevel: "SL12";
+ checkedAt: string;
+ status: "connected" | "degraded";
+ latencyMsRounded: number | null;
+ aggregateFixtureCount: number | null;
+ freshnessClass: "current" | "stale" | "unknown";
+};
+
+function parseTxlinePulse(value: unknown): TxlinePulse {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Pulse payload is invalid");
+ const record = value as Record;
+ const expectedKeys = ["aggregateFixtureCount", "checkedAt", "freshnessClass", "latencyMsRounded", "network", "serviceLevel", "status"];
+ if (Object.keys(record).sort().join("|") !== expectedKeys.join("|")) throw new Error("Pulse payload crossed its public allowlist");
+ if (
+ record.network !== "mainnet" ||
+ record.serviceLevel !== "SL12" ||
+ (record.status !== "connected" && record.status !== "degraded") ||
+ typeof record.checkedAt !== "string" ||
+ !Number.isFinite(Date.parse(record.checkedAt)) ||
+ (record.freshnessClass !== "current" && record.freshnessClass !== "stale" && record.freshnessClass !== "unknown") ||
+ (record.latencyMsRounded !== null && (typeof record.latencyMsRounded !== "number" || !Number.isSafeInteger(record.latencyMsRounded) || record.latencyMsRounded < 0)) ||
+ (record.aggregateFixtureCount !== null && (typeof record.aggregateFixtureCount !== "number" || !Number.isSafeInteger(record.aggregateFixtureCount) || record.aggregateFixtureCount < 0))
+ ) throw new Error("Pulse payload values are invalid");
+ return record as TxlinePulse;
+}
+
+export async function loadTxlinePulse(signal: AbortSignal): Promise {
+ const response = await fetch(TXLINE_PULSE_API_PATH, {
+ headers: { accept: "application/json" },
+ cache: "no-store",
+ signal
+ });
+ if (!response.ok) throw new Error(`TXLine pulse request failed with status ${response.status}`);
+ return parseTxlinePulse(await response.json() as unknown);
+}
+
+export async function loadStudy(signal: AbortSignal): Promise {
+ const response = await fetch(STUDY_API_PATH, {
+ headers: { accept: "application/json" },
+ signal
+ });
+ if (!response.ok) throw new Error(`Study request failed with status ${response.status}`);
+ const payload = (await response.json()) as StudyApiResponse;
+ return payload.data;
+}
+
+export async function loadCasebook(signal: AbortSignal): Promise {
+ const response = await fetch(CASEBOOK_API_PATH, {
+ headers: { accept: "application/json" },
+ signal
+ });
+ if (!response.ok) throw new Error(`Casebook request failed with status ${response.status}`);
+ const payload = (await response.json()) as CasebookApiResponse;
+ return payload.data;
+}
+
+export async function loadCommand(signal: AbortSignal): Promise {
+ const response = await fetch(COMMAND_API_PATH, {
+ headers: { accept: "application/json" },
+ signal
+ });
+ if (!response.ok) throw new Error(`Command request failed with status ${response.status}`);
+ const payload = (await response.json()) as CommandApiResponse;
+ return payload.data;
+}
+
+export async function loadMatchroom(signal: AbortSignal): Promise {
+ const response = await fetch(MATCHROOM_API_PATH, {
+ headers: { accept: "application/json" },
+ signal
+ });
+ if (!response.ok) throw new Error(`Matchroom request failed with status ${response.status}`);
+ const payload = (await response.json()) as DashboardApiResponse;
+ return payload.data;
+}
diff --git a/apps/dashboard/src/command-schedule.ts b/apps/dashboard/src/command-schedule.ts
new file mode 100644
index 0000000..98c4bc1
--- /dev/null
+++ b/apps/dashboard/src/command-schedule.ts
@@ -0,0 +1,33 @@
+function windowDistance(milliseconds: number): string {
+ const totalMinutes = Math.max(1, Math.ceil(milliseconds / 60_000));
+ const days = Math.floor(totalMinutes / (24 * 60));
+ const hours = Math.floor(totalMinutes / 60) % 24;
+ const minutes = totalMinutes % 60;
+ if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
+ if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
+ return `${minutes}m`;
+}
+
+/**
+ * Describes clock position relative to a configured capture window without
+ * claiming that the frozen export reflects a live recorder process.
+ */
+export function frozenCaptureWindowLabel(
+ captureStartUtc: string,
+ captureEndUtc: string,
+ browserNowMs: number
+): string {
+ const startMs = Date.parse(captureStartUtc);
+ const endMs = Date.parse(captureEndUtc);
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs || !Number.isFinite(browserNowMs)) {
+ return "Frozen export · configured timing unavailable";
+ }
+ if (browserNowMs < startMs) {
+ return `Frozen export · browser clock: window in ${windowDistance(startMs - browserNowMs)}`;
+ }
+ if (browserNowMs < endMs) {
+ return "Frozen export · browser clock is inside configured window";
+ }
+ if (browserNowMs === endMs) return "Frozen export · browser clock: window just ended";
+ return `Frozen export · browser clock: window ended ${windowDistance(browserNowMs - endMs)} ago`;
+}
diff --git a/apps/dashboard/src/main.tsx b/apps/dashboard/src/main.tsx
new file mode 100644
index 0000000..33837d7
--- /dev/null
+++ b/apps/dashboard/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App";
+import "./styles.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+);
diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css
new file mode 100644
index 0000000..9708cca
--- /dev/null
+++ b/apps/dashboard/src/styles.css
@@ -0,0 +1,8947 @@
+@import "@fontsource/manrope/latin-400.css";
+@import "@fontsource/manrope/latin-500.css";
+@import "@fontsource/manrope/latin-600.css";
+@import "@fontsource/manrope/latin-700.css";
+@import "@fontsource/manrope/latin-800.css";
+@import "@fontsource/ibm-plex-mono/latin-400.css";
+@import "@fontsource/ibm-plex-mono/latin-500.css";
+
+:root {
+ --bg: #080b19;
+ --sidebar: #060817;
+ --surface: #10142a;
+ --surface-raised: #141a34;
+ --surface-soft: #0c1023;
+ --border: rgba(157, 169, 218, 0.15);
+ --border-strong: rgba(157, 169, 218, 0.25);
+ --text: #f7f8fc;
+ --text-soft: #cbd0e2;
+ --muted: #929bb9;
+ --muted-strong: #aeb6d0;
+ --lime: #9ff850;
+ --lime-deep: #75d52c;
+ --blue: #6489f6;
+ --violet: #9183e5;
+ --amber: #e9b968;
+ --amber-soft: rgba(233, 185, 104, 0.1);
+ --danger: #f16e7a;
+ --danger-soft: rgba(241, 110, 122, 0.1);
+ --shadow: 0 24px 70px rgba(0, 0, 0, 0.2);
+ --sans: "Manrope", sans-serif;
+ --mono: "IBM Plex Mono", monospace;
+}
+
+html.light {
+ --bg: #e8ebf3;
+ --sidebar: #f8f9fb;
+ --surface: #f9fafc;
+ --surface-raised: #ffffff;
+ --surface-soft: #f0f2f7;
+ --border: rgba(55, 61, 83, 0.12);
+ --border-strong: rgba(55, 61, 83, 0.2);
+ --text: #25262d;
+ --text-soft: #4a4d59;
+ --muted: #737887;
+ --muted-strong: #5e6371;
+ --lime: #54bf26;
+ --lime-deep: #3a9d18;
+ --blue: #607ee7;
+ --violet: #8574d7;
+ --amber: #a86f16;
+ --amber-soft: rgba(198, 135, 34, 0.1);
+ --danger: #c94f5b;
+ --danger-soft: rgba(201, 79, 91, 0.09);
+ --shadow: 0 24px 60px rgba(46, 49, 65, 0.12);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ color-scheme: dark;
+ scroll-behavior: smooth;
+}
+
+html.light {
+ color-scheme: light;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+ overflow-x: hidden;
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--sans);
+ font-size: 14px;
+ text-rendering: optimizeLegibility;
+}
+
+button,
+input {
+ color: inherit;
+ font: inherit;
+}
+
+.load-screen {
+ min-height: 100vh;
+ display: grid;
+ align-content: center;
+ justify-items: center;
+ padding: 32px;
+ background:
+ radial-gradient(circle at 50% 42%, rgba(100, 137, 246, 0.14), transparent 22rem),
+ linear-gradient(145deg, #070914, var(--bg));
+ text-align: center;
+}
+
+.load-screen .brand-mark {
+ width: 42px;
+ height: 42px;
+ margin-bottom: 24px;
+}
+
+.load-kicker {
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.load-screen h1 {
+ margin: 10px 0 9px;
+ font-size: clamp(25px, 4vw, 42px);
+ letter-spacing: -0.05em;
+}
+
+.load-screen p {
+ max-width: 480px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.7;
+}
+
+.load-line {
+ width: min(280px, 70vw);
+ height: 2px;
+ margin: 23px 0;
+ overflow: hidden;
+ border-radius: 2px;
+ background: var(--border);
+}
+
+.load-line i {
+ display: block;
+ width: 40%;
+ height: 100%;
+ border-radius: inherit;
+ background: var(--lime);
+ animation: verified-load 1.2s ease-in-out infinite alternate;
+}
+
+@keyframes verified-load {
+ from { transform: translateX(-20%); }
+ to { transform: translateX(170%); }
+}
+
+.error-screen .error-mark {
+ width: 48px;
+ height: 48px;
+ display: grid;
+ margin-bottom: 22px;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--blue), transparent 65%);
+ border-radius: 14px;
+ background: color-mix(in srgb, var(--blue), transparent 90%);
+ color: var(--blue);
+}
+
+.error-mark svg {
+ width: 24px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.7;
+}
+
+.error-screen button {
+ margin-top: 24px;
+ padding: 11px 16px;
+ border: 0;
+ border-radius: 10px;
+ background: var(--lime);
+ color: #081018;
+ font-size: 11px;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+
+button:focus-visible,
+a:focus-visible {
+ outline: 2px solid var(--lime);
+ outline-offset: 3px;
+}
+
+.theme-frame {
+ display: none;
+}
+
+html.light .theme-frame {
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-bottom: 1px solid rgba(55, 61, 83, 0.1);
+ background: #2e2e35;
+ color: #f1f2f5;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+.app-shell {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: 204px minmax(0, 1fr);
+ background:
+ radial-gradient(circle at 78% -10%, rgba(86, 105, 204, 0.12), transparent 35rem),
+ var(--bg);
+}
+
+html.light .app-shell {
+ min-height: calc(100vh - 38px);
+ background: var(--bg);
+}
+
+.sidebar {
+ position: sticky;
+ top: 0;
+ z-index: 12;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ padding: 24px 15px 18px;
+ border-right: 1px solid var(--border);
+ background: var(--sidebar);
+}
+
+html.light .sidebar {
+ top: 38px;
+ height: calc(100vh - 38px);
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 9px;
+ color: var(--text);
+ font-size: 16px;
+ font-weight: 800;
+ letter-spacing: -0.04em;
+ text-decoration: none;
+}
+
+.brand-mark {
+ position: relative;
+ width: 29px;
+ height: 29px;
+ flex: 0 0 auto;
+ overflow: hidden;
+ border-radius: 9px;
+ background: var(--lime);
+ box-shadow: 0 8px 18px rgba(128, 224, 47, 0.16);
+}
+
+.brand-mark i {
+ position: absolute;
+ width: 23px;
+ height: 5px;
+ border-radius: 10px;
+ background: var(--sidebar);
+ transform: rotate(-38deg);
+}
+
+.brand-mark i:first-child {
+ top: 8px;
+ left: -1px;
+}
+
+.brand-mark i:last-child {
+ right: -1px;
+ bottom: 8px;
+}
+
+.side-nav {
+ flex: 1;
+ margin-top: 38px;
+}
+
+.nav-group {
+ display: block;
+ margin: 0 11px 12px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.system-group {
+ margin-top: 32px;
+}
+
+.side-nav a {
+ min-height: 46px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 4px 0;
+ padding: 0 12px;
+ border-radius: 11px;
+ color: var(--muted-strong);
+ font-size: 12px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: background 180ms ease, color 180ms ease, transform 180ms ease;
+}
+
+.side-nav a:hover {
+ background: color-mix(in srgb, var(--surface-raised), transparent 30%);
+ color: var(--text);
+ transform: translateX(2px);
+}
+
+.side-nav a.active {
+ background: var(--lime);
+ color: #081018;
+ box-shadow: 0 10px 24px rgba(116, 208, 40, 0.14);
+}
+
+.side-nav svg,
+.mobile-nav svg,
+.state-chip svg,
+.shield-lock svg,
+.context-bar svg,
+.plain-conclusion svg,
+.replay-bar svg,
+.outcome-icon svg,
+.stage-icon svg {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.7;
+}
+
+.side-nav em {
+ min-width: 24px;
+ margin-left: auto;
+ padding: 3px 6px;
+ border-radius: 7px;
+ background: color-mix(in srgb, var(--surface-raised), transparent 35%);
+ color: var(--muted);
+ font-size: 9px;
+ font-style: normal;
+ text-align: center;
+}
+
+.side-nav a.active em {
+ background: rgba(8, 16, 24, 0.12);
+ color: #081018;
+}
+
+.feed-dot {
+ width: 7px;
+ height: 7px;
+ margin-left: auto;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lime), transparent 88%);
+}
+
+.sidebar-bottom {
+ display: grid;
+ gap: 12px;
+}
+
+.gate-card,
+.owner {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 11px;
+ border-radius: 12px;
+}
+
+.gate-card {
+ border: 1px solid color-mix(in srgb, var(--blue), transparent 80%);
+ background: color-mix(in srgb, var(--blue), transparent 94%);
+}
+
+.shield-lock {
+ width: 31px;
+ height: 31px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 9px;
+ background: color-mix(in srgb, var(--blue), transparent 88%);
+ color: var(--blue);
+}
+
+.shield-lock svg {
+ width: 17px;
+ height: 17px;
+}
+
+.gate-card > span:last-child,
+.owner > span:last-child {
+ min-width: 0;
+ display: grid;
+ gap: 2px;
+}
+
+.gate-card b,
+.owner b {
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.gate-card small,
+.owner small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.owner {
+ padding: 7px 10px;
+}
+
+.owner-avatar {
+ width: 33px;
+ height: 33px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid var(--border-strong);
+ border-radius: 50%;
+ background: var(--surface-raised);
+ color: var(--lime-deep);
+ font-size: 12px;
+ font-weight: 800;
+}
+
+.workspace {
+ min-width: 0;
+ padding-bottom: 36px;
+}
+
+.topbar {
+ min-height: 84px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ padding: 15px 28px;
+ border-bottom: 1px solid var(--border);
+}
+
+.mobile-brand {
+ display: none;
+}
+
+.page-title > span {
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.page-title h1 {
+ margin: 3px 0 0;
+ font-size: 25px;
+ font-weight: 700;
+ letter-spacing: -0.045em;
+}
+
+.system-state {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.state-chip,
+.paper-label,
+.verified-label,
+.discipline-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.provenance-badge {
+ display: inline-flex;
+ width: fit-content;
+ align-items: center;
+ gap: 7px;
+ padding: 6px 9px;
+ border: 1px solid currentColor;
+ border-radius: 999px;
+ background: color-mix(in srgb, currentColor, transparent 92%);
+ font-family: var(--mono);
+ font-size: 8px;
+ font-weight: 700;
+ letter-spacing: 0.07em;
+ line-height: 1;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.provenance-badge i {
+ width: 6px;
+ height: 6px;
+ flex: 0 0 6px;
+ border-radius: 50%;
+ background: currentColor;
+ box-shadow: 0 0 0 4px color-mix(in srgb, currentColor, transparent 88%);
+}
+
+.provenance-badge.capture { color: var(--lime); }
+.provenance-badge.configured { color: var(--violet); }
+.provenance-badge.historical { color: var(--blue); }
+.provenance-badge.offline { color: var(--muted-strong); }
+.provenance-badge.synthetic { color: var(--amber); }
+
+.state-chip {
+ min-height: 36px;
+ padding: 0 12px;
+ border: 1px solid var(--border);
+ background: color-mix(in srgb, var(--surface-raised), transparent 15%);
+ color: var(--text-soft);
+}
+
+.state-chip.replay {
+ border-color: color-mix(in srgb, var(--lime), transparent 78%);
+ color: color-mix(in srgb, var(--lime), var(--text) 30%);
+}
+
+.state-chip.replay i,
+.verified-label i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lime), transparent 88%);
+}
+
+.state-chip.paper {
+ color: var(--blue);
+}
+
+.state-chip.gate {
+ border-color: color-mix(in srgb, var(--blue), transparent 80%);
+ background: color-mix(in srgb, var(--blue), transparent 94%);
+ color: color-mix(in srgb, var(--blue), var(--text) 28%);
+}
+
+.state-chip.gate svg {
+ width: 15px;
+ height: 15px;
+}
+
+.context-bar {
+ min-height: 72px;
+ display: grid;
+ grid-template-columns: minmax(250px, 1fr) auto minmax(200px, 1fr);
+ align-items: center;
+ gap: 18px;
+ padding: 10px 28px;
+ border-bottom: 1px solid var(--border);
+ background: color-mix(in srgb, var(--surface-soft), transparent 34%);
+}
+
+.fixture-select {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ justify-self: start;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: default;
+ text-align: left;
+}
+
+.cup-mark {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--blue), transparent 86%);
+ color: var(--lime-deep);
+}
+
+.cup-mark svg {
+ width: 17px;
+ height: 17px;
+}
+
+.fixture-select > span:nth-child(2),
+.original-date > span {
+ display: grid;
+ gap: 2px;
+}
+
+.fixture-select small,
+.original-date small {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.fixture-select b,
+.original-date b {
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.fixture-select .chevron {
+ width: 14px;
+ color: var(--muted);
+}
+
+.view-tabs {
+ height: 42px;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding: 4px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: var(--surface-soft);
+}
+
+.view-tabs a {
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 15px;
+ border-radius: 8px;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.view-tabs a.active {
+ background: var(--surface-raised);
+ color: var(--text);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+}
+
+.original-date {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ justify-self: end;
+}
+
+.original-date > svg {
+ width: 18px;
+ color: var(--muted);
+}
+
+.content {
+ width: min(1560px, 100%);
+ margin: 0 auto;
+ padding: 24px 28px 0;
+}
+
+.surface {
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ background: var(--surface);
+ box-shadow: var(--shadow);
+}
+
+.match-masthead {
+ min-height: 156px;
+ display: grid;
+ grid-template-columns: minmax(145px, 0.75fr) minmax(165px, 1fr) auto minmax(165px, 1fr) minmax(205px, 0.95fr);
+ align-items: center;
+ gap: 22px;
+ padding: 22px 24px;
+ background:
+ radial-gradient(circle at 50% 135%, color-mix(in srgb, var(--blue), transparent 87%), transparent 45%),
+ var(--surface);
+}
+
+.match-meta {
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.live-label {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ color: var(--muted-strong);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.live-label i {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lime), transparent 88%);
+}
+
+.fixture-number {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 10px;
+}
+
+.team {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+}
+
+.team > span:last-child,
+.away-team > span:first-child {
+ display: grid;
+ gap: 3px;
+}
+
+.team small {
+ color: var(--text);
+ font-size: 17px;
+ font-weight: 700;
+ letter-spacing: -0.035em;
+}
+
+.team b {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.away-team {
+ justify-content: flex-end;
+ text-align: right;
+}
+
+.crest {
+ width: 54px;
+ height: 54px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ overflow: hidden;
+ border: 3px solid color-mix(in srgb, var(--text), transparent 20%);
+ border-radius: 50%;
+ color: #ffffff;
+ font-size: 10px;
+ font-weight: 800;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
+ box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
+}
+
+.crest.esp {
+ background: linear-gradient(#b91f2f 0 27%, #e9c43a 27% 73%, #b91f2f 73%);
+}
+
+.crest.bel {
+ background: linear-gradient(90deg, #171717 0 33%, #e9c83b 33% 66%, #c92534 66%);
+}
+
+.score-block {
+ min-width: 130px;
+ display: grid;
+ justify-items: center;
+}
+
+.clock {
+ padding: 4px 10px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--lime), transparent 88%);
+ color: var(--lime-deep);
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.score-block > div {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 1px;
+}
+
+.score-block strong {
+ font-size: 38px;
+ font-weight: 700;
+ letter-spacing: -0.06em;
+}
+
+.score-block i {
+ color: var(--muted);
+ font-size: 20px;
+ font-style: normal;
+}
+
+.score-block small {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.market-meta {
+ min-height: 86px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-left: 22px;
+ border-left: 1px solid var(--border);
+}
+
+.market-meta small {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.market-meta b {
+ margin-top: 5px;
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.market-meta span {
+ margin-top: 4px;
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.45;
+}
+
+.analysis-grid,
+.support-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 2.05fr) minmax(320px, 0.95fr);
+ gap: 22px;
+ margin-top: 22px;
+}
+
+.probability-panel,
+.decision-rail,
+.evidence-panel,
+.proof-panel {
+ min-width: 0;
+ padding: 24px;
+}
+
+.probability-panel,
+.decision-rail {
+ min-height: 680px;
+}
+
+.panel-heading {
+ min-height: 42px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+
+.panel-heading > div > span,
+.panel-heading > span:first-child {
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.075em;
+ text-transform: uppercase;
+}
+
+.panel-heading h2 {
+ margin: 5px 0 0;
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: -0.03em;
+}
+
+.legend {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ color: var(--muted-strong);
+ font-size: 10px;
+}
+
+.legend span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ white-space: nowrap;
+}
+
+.legend i {
+ width: 16px;
+ display: inline-block;
+}
+
+.legend-bid {
+ height: 3px;
+ border-radius: 2px;
+ background: var(--lime);
+}
+
+.legend-ask {
+ height: 3px;
+ border-radius: 2px;
+ background: var(--violet);
+}
+
+.legend-spread {
+ height: 8px;
+ border: 1px solid color-mix(in srgb, var(--violet), transparent 55%);
+ background: color-mix(in srgb, var(--violet), transparent 83%);
+}
+
+.comparison-strip {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ margin-top: 22px;
+ padding: 18px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.comparison-strip > div {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+ padding: 0 20px;
+}
+
+.comparison-strip > div:first-child {
+ padding-left: 0;
+}
+
+.comparison-strip > div:not(:last-child) {
+ border-right: 1px solid var(--border);
+}
+
+.comparison-strip > div > span {
+ color: var(--muted-strong);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.comparison-strip strong {
+ overflow: hidden;
+ font-size: clamp(23px, 2vw, 32px);
+ font-weight: 700;
+ letter-spacing: -0.055em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.comparison-strip strong i {
+ padding: 0 5px;
+ color: var(--muted);
+ font-style: normal;
+}
+
+.comparison-strip small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.plain-conclusion {
+ min-height: 74px;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 14px;
+ margin-top: 18px;
+ padding: 14px 16px;
+ border: 1px solid color-mix(in srgb, var(--amber), transparent 76%);
+ border-radius: 14px;
+ background: var(--amber-soft);
+}
+
+.conclusion-mark {
+ width: 36px;
+ height: 36px;
+ display: grid;
+ place-items: center;
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--amber), transparent 84%);
+ color: var(--amber);
+}
+
+.conclusion-mark svg {
+ width: 19px;
+ height: 19px;
+}
+
+.plain-conclusion p {
+ margin: 0;
+ color: var(--muted-strong);
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.plain-conclusion p strong {
+ color: var(--text);
+ font-weight: 700;
+}
+
+.discipline-chip {
+ padding: 7px 10px;
+ background: color-mix(in srgb, var(--amber), transparent 84%);
+ color: var(--amber);
+}
+
+.chart-wrap {
+ margin: 18px 0 0;
+}
+
+.chart-wrap > svg {
+ width: 100%;
+ height: 300px;
+ display: block;
+ overflow: visible;
+}
+
+.chart-grid line {
+ stroke: color-mix(in srgb, var(--muted), transparent 83%);
+ stroke-dasharray: 2 7;
+}
+
+.chart-axis {
+ fill: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 10px;
+}
+
+.spread-area {
+ fill: url(#spread-fill);
+}
+
+.bid-path,
+.ask-path {
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.bid-path {
+ stroke: color-mix(in srgb, var(--violet), transparent 60%);
+ stroke-width: 1.5;
+ stroke-dasharray: 4 4;
+}
+
+.ask-path {
+ stroke: var(--violet);
+ stroke-width: 2.8;
+}
+
+.goal-event line {
+ stroke: color-mix(in srgb, var(--text), transparent 77%);
+ stroke-dasharray: 3 6;
+}
+
+.goal-event rect {
+ fill: var(--surface-raised);
+ stroke: color-mix(in srgb, var(--lime), transparent 62%);
+}
+
+.goal-event text {
+ fill: var(--lime-deep);
+ font-family: var(--mono);
+ font-size: 9px;
+ font-weight: 500;
+}
+
+.cursor {
+ transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.cursor line {
+ stroke: color-mix(in srgb, var(--text), transparent 74%);
+ stroke-dasharray: 2 5;
+}
+
+.bid-dot,
+.ask-dot {
+ stroke: var(--surface);
+ stroke-width: 3px;
+ transition: cy 420ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.bid-dot {
+ fill: var(--lime);
+}
+
+.ask-dot {
+ fill: var(--violet);
+}
+
+.chart-wrap figcaption {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ margin-top: 0;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 10px;
+}
+
+.replay-bar {
+ min-height: 58px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+}
+
+.play-button {
+ height: 38px;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 14px;
+ border: 0;
+ border-radius: 10px;
+ background: var(--lime);
+ color: #081018;
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 800;
+}
+
+.play-button svg {
+ width: 15px;
+ height: 15px;
+}
+
+.play-button .play-icon {
+ fill: currentColor;
+ stroke: none;
+}
+
+.pause-icon,
+.play-button[aria-pressed="true"] .play-icon {
+ display: none;
+}
+
+.play-button[aria-pressed="true"] .pause-icon {
+ display: block;
+}
+
+.replay-steps {
+ height: 38px;
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ padding: 3px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--surface-soft);
+}
+
+.replay-steps button {
+ height: 30px;
+ padding: 0 12px;
+ border: 0;
+ border-radius: 7px;
+ background: transparent;
+ color: var(--muted-strong);
+ cursor: pointer;
+ font-size: 10px;
+ font-weight: 700;
+}
+
+.replay-steps button.active {
+ background: var(--surface-raised);
+ color: var(--text);
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+}
+
+.speed {
+ margin-left: auto;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 10px;
+}
+
+.decision-rail {
+ display: flex;
+ flex-direction: column;
+}
+
+.paper-label {
+ padding: 6px 9px;
+ background: color-mix(in srgb, var(--blue), transparent 88%);
+ color: var(--blue);
+}
+
+.decision-outcome {
+ display: grid;
+ justify-items: center;
+ padding: 28px 16px 23px;
+ border-bottom: 1px solid var(--border);
+ text-align: center;
+}
+
+.outcome-icon {
+ width: 60px;
+ height: 60px;
+ display: grid;
+ place-items: center;
+ margin-bottom: 12px;
+ border: 1px solid color-mix(in srgb, var(--amber), transparent 72%);
+ border-radius: 18px;
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.outcome-icon svg {
+ width: 29px;
+ height: 29px;
+}
+
+.decision-outcome > span:not(.outcome-icon) {
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.decision-outcome > strong {
+ margin-top: 4px;
+ color: var(--amber);
+ font-size: 31px;
+ font-weight: 700;
+ letter-spacing: -0.05em;
+}
+
+.decision-outcome p {
+ max-width: 320px;
+ min-height: 38px;
+ margin: 8px 0 0;
+ color: var(--muted-strong);
+ font-size: 11px;
+ line-height: 1.55;
+}
+
+.reason-block {
+ margin-top: 18px;
+ padding: 15px 16px;
+ border-left: 3px solid var(--amber);
+ border-radius: 0 12px 12px 0;
+ background: var(--amber-soft);
+}
+
+.reason-block > span {
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.reason-block b {
+ display: block;
+ margin-top: 5px;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.reason-block p {
+ margin: 5px 0 0;
+ color: var(--muted-strong);
+ font-size: 10px;
+}
+
+.reason-block p strong {
+ color: var(--text);
+}
+
+.decision-stages {
+ flex: 1;
+ margin: 14px 0 0;
+ padding: 0;
+ list-style: none;
+}
+
+.decision-stages li {
+ position: relative;
+ min-height: 55px;
+ display: grid;
+ grid-template-columns: 26px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px;
+}
+
+.decision-stages li:not(:last-child)::after {
+ position: absolute;
+ top: 37px;
+ bottom: -11px;
+ left: 13px;
+ width: 1px;
+ background: var(--border-strong);
+ content: "";
+}
+
+.stage-icon {
+ position: relative;
+ z-index: 1;
+ width: 26px;
+ height: 26px;
+ display: grid;
+ place-items: center;
+ border-radius: 50%;
+}
+
+.stage-icon svg {
+ width: 13px;
+ height: 13px;
+ stroke-width: 2;
+}
+
+.complete .stage-icon {
+ background: color-mix(in srgb, var(--lime), transparent 88%);
+ color: var(--lime-deep);
+}
+
+.passed .stage-icon {
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.locked .stage-icon {
+ background: color-mix(in srgb, var(--blue), transparent 90%);
+ color: var(--blue);
+}
+
+.decision-stages li > span:nth-child(2) {
+ display: grid;
+ gap: 2px;
+}
+
+.decision-stages b {
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.decision-stages small,
+.decision-stages time {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.decision-stages time {
+ font-family: var(--mono);
+}
+
+.boundary-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ margin-top: 14px;
+ padding: 13px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.boundary-grid div {
+ display: grid;
+ gap: 4px;
+ padding: 0 10px;
+}
+
+.boundary-grid div:not(:last-child) {
+ border-right: 1px solid var(--border);
+}
+
+.boundary-grid span {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.boundary-grid b {
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.protective-gate {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 14px;
+ padding: 11px;
+ border: 1px solid color-mix(in srgb, var(--blue), transparent 80%);
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--blue), transparent 94%);
+}
+
+.protective-gate > span:last-child {
+ display: grid;
+ gap: 2px;
+}
+
+.protective-gate b {
+ color: color-mix(in srgb, var(--blue), var(--text) 28%);
+ font-size: 11px;
+}
+
+.protective-gate small {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.support-grid {
+ align-items: stretch;
+}
+
+.evidence-panel,
+.proof-panel {
+ min-height: 312px;
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.13);
+}
+
+.case-id {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 10px;
+}
+
+.evidence-table-wrap {
+ overflow-x: auto;
+ margin-top: 20px;
+ scrollbar-color: var(--border-strong) transparent;
+ scrollbar-width: thin;
+}
+
+.evidence-table {
+ width: 100%;
+ min-width: 690px;
+ border-collapse: collapse;
+ color: var(--text-soft);
+ font-size: 11px;
+}
+
+.evidence-table th {
+ padding: 0 12px 12px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 600;
+ text-align: left;
+}
+
+.evidence-table td {
+ height: 64px;
+ padding: 10px 12px;
+ border-top: 1px solid var(--border);
+ transition: background 180ms ease;
+}
+
+.evidence-table tr.active td {
+ background: color-mix(in srgb, var(--blue), transparent 95%);
+}
+
+.evidence-table tr.active td:first-child {
+ box-shadow: inset 2px 0 var(--blue);
+}
+
+.evidence-table time,
+.evidence-table td:nth-child(4) b {
+ display: block;
+ color: var(--text);
+ font-family: var(--mono);
+ font-size: 10px;
+ font-weight: 500;
+}
+
+.evidence-table td small {
+ display: block;
+ margin-top: 3px;
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.source {
+ width: 28px;
+ height: 28px;
+ display: inline-grid;
+ place-items: center;
+ margin-right: 8px;
+ border-radius: 8px;
+ font-size: 8px;
+ font-weight: 800;
+ vertical-align: middle;
+}
+
+.source.poly {
+ background: color-mix(in srgb, var(--blue), transparent 87%);
+ color: var(--blue);
+}
+
+.source.tx {
+ background: color-mix(in srgb, var(--lime), transparent 87%);
+ color: var(--lime-deep);
+}
+
+.source.sam {
+ background: color-mix(in srgb, var(--violet), transparent 87%);
+ color: var(--violet);
+}
+
+.assessment {
+ display: inline-flex;
+ padding: 6px 9px;
+ border-radius: 8px;
+ font-size: 9px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.assessment.watch,
+.assessment.pass {
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.verified-label {
+ padding: 6px 9px;
+ background: color-mix(in srgb, var(--lime), transparent 89%);
+ color: var(--lime-deep);
+}
+
+.proof-primary {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin-top: 20px;
+ padding-bottom: 18px;
+ border-bottom: 1px solid var(--border);
+}
+
+.proof-ring {
+ width: 61px;
+ height: 61px;
+ display: grid;
+ place-items: center;
+ border: 4px solid var(--lime);
+ border-radius: 50%;
+ font-size: 17px;
+ font-weight: 700;
+ letter-spacing: -0.04em;
+}
+
+.proof-ring small {
+ color: var(--lime-deep);
+ font-size: 9px;
+}
+
+.proof-primary > span:last-child {
+ display: grid;
+ gap: 3px;
+}
+
+.proof-primary b {
+ font-size: 12px;
+}
+
+.proof-primary small {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.proof-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10px;
+ margin: 16px 0;
+}
+
+.proof-stats div {
+ min-width: 0;
+}
+
+.proof-stats dt {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.proof-stats dd {
+ overflow: hidden;
+ margin: 4px 0 0;
+ color: var(--text);
+ font-size: 11px;
+ font-weight: 700;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.proof-stats .outage dd {
+ color: var(--amber);
+}
+
+.hash {
+ display: grid;
+ gap: 4px;
+ padding: 12px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--surface-soft);
+}
+
+.hash span {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.hash code {
+ overflow: hidden;
+ color: var(--text-soft);
+ font-family: var(--mono);
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.public-policy {
+ display: grid;
+ gap: 3px;
+ margin-top: 10px;
+ padding: 11px 12px;
+ border: 1px solid color-mix(in srgb, var(--blue), transparent 83%);
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--blue), transparent 95%);
+}
+
+.public-policy span,
+.public-policy small {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.public-policy b {
+ color: var(--text-soft);
+ font-size: 10px;
+}
+
+.public-policy small {
+ line-height: 1.45;
+}
+
+.mobile-nav {
+ display: none;
+}
+
+.reveal {
+ opacity: 0;
+ transform: translateY(9px);
+ animation: reveal 480ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+.r1 { animation-delay: 40ms; }
+.r2 { animation-delay: 80ms; }
+.r3 { animation-delay: 120ms; }
+.r4 { animation-delay: 160ms; }
+.r5 { animation-delay: 200ms; }
+
+@keyframes reveal {
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Command uses the same visual language as Matchroom, with one dominant case. */
+.state-chip.offline {
+ border-color: color-mix(in srgb, var(--violet), transparent 76%);
+ background: color-mix(in srgb, var(--violet), transparent 94%);
+ color: color-mix(in srgb, var(--violet), var(--text) 28%);
+}
+
+.state-chip.offline i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--violet);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--violet), transparent 88%);
+}
+
+.command-shell .workspace {
+ background:
+ radial-gradient(circle at 56% 8%, rgba(100, 137, 246, 0.07), transparent 31rem),
+ transparent;
+}
+
+.command-system-deck {
+ min-height: 104px;
+ display: grid;
+ grid-template-columns: minmax(250px, 0.9fr) minmax(520px, 2fr) 120px;
+ align-items: stretch;
+ border-bottom: 1px solid var(--border);
+ background: color-mix(in srgb, var(--surface-soft), transparent 31%);
+}
+
+.system-posture {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 18px 24px 18px 28px;
+ border-right: 1px solid var(--border);
+}
+
+.posture-icon {
+ width: 38px;
+ height: 38px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--lime), transparent 75%);
+ border-radius: 11px;
+ background: color-mix(in srgb, var(--lime), transparent 91%);
+ color: var(--lime);
+}
+
+.posture-icon svg,
+.command-primary-action svg,
+.feature-verdict svg,
+.schedule-rule svg,
+.fixture-boundary svg,
+.command-panel-head svg,
+.case-empty svg,
+.case-outcome svg,
+.sealed-label svg,
+.study-lock svg,
+.proof-policy svg {
+ width: 18px;
+ height: 18px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.7;
+}
+
+.system-posture > span:last-child {
+ min-width: 0;
+ display: grid;
+ gap: 2px;
+}
+
+.system-posture small,
+.deck-time small,
+.deck-time span {
+ color: var(--muted);
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.system-posture b {
+ overflow: hidden;
+ font-size: 13px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.system-posture em {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 10px;
+ font-style: normal;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.feed-deck {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: repeat(4, minmax(110px, 1fr));
+ align-items: stretch;
+}
+
+.feed-state {
+ min-width: 0;
+ display: grid;
+ align-content: center;
+ gap: 4px;
+ padding: 15px 16px;
+ border-right: 1px solid var(--border);
+}
+
+.feed-state > span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.feed-state > span i {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--blue);
+}
+
+.feed-state.verified > span i {
+ background: var(--lime);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--lime), transparent 89%);
+}
+
+.feed-state.initialized > span i {
+ background: var(--violet);
+}
+
+.feed-state b {
+ font-size: 11px;
+}
+
+.feed-state small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.deck-time {
+ display: grid;
+ align-content: center;
+ justify-items: end;
+ gap: 3px;
+ padding: 15px 25px 15px 12px;
+ text-align: right;
+}
+
+.deck-time time {
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.deck-time span {
+ font-size: 8px;
+ letter-spacing: 0;
+ text-transform: none;
+}
+
+.command-content {
+ width: min(1560px, 100%);
+ margin: 0 auto;
+ padding: 24px 28px 0;
+}
+
+.command-live-pulse {
+ margin-bottom: 14px;
+ overflow: hidden;
+ border: 1px solid rgba(100, 137, 246, 0.22);
+ border-radius: 13px;
+ background: linear-gradient(100deg, rgba(100, 137, 246, 0.07), rgba(9, 13, 29, 0.96) 38%);
+}
+
+.command-live-pulse.connected {
+ border-color: rgba(159, 248, 80, 0.2);
+ background: linear-gradient(100deg, rgba(159, 248, 80, 0.055), rgba(9, 13, 29, 0.96) 38%);
+}
+
+.command-live-pulse > header,
+.command-live-pulse > footer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 15px;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+}
+
+.command-live-pulse > header {
+ justify-content: space-between;
+ text-transform: uppercase;
+}
+
+.command-live-pulse > header > span {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.command-live-pulse > header i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--blue);
+ box-shadow: 0 0 0 4px rgba(100, 137, 246, 0.1);
+}
+
+.command-live-pulse.connected > header i {
+ background: var(--lime);
+ box-shadow: 0 0 0 4px rgba(159, 248, 80, 0.1);
+}
+
+.command-live-pulse > header em {
+ color: var(--blue);
+ font-style: normal;
+}
+
+.command-live-pulse.connected > header em {
+ color: var(--lime);
+}
+
+.command-live-pulse-grid {
+ display: grid;
+ grid-template-columns: minmax(290px, 1.55fr) repeat(4, minmax(105px, 0.55fr));
+}
+
+.command-live-pulse-grid > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 4px;
+ padding: 13px 16px;
+ border-right: 1px solid var(--border);
+}
+
+.command-live-pulse-grid > div:last-child {
+ border-right: 0;
+}
+
+.command-live-pulse-grid small,
+.command-live-pulse-grid em {
+ color: var(--muted);
+ font-size: 7px;
+ font-style: normal;
+}
+
+.command-live-pulse-grid small {
+ font-family: var(--mono);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.command-live-pulse-grid b {
+ color: var(--text-soft);
+ font-size: 10px;
+ text-transform: capitalize;
+}
+
+.command-live-pulse-grid .pulse-identity {
+ flex-direction: row;
+ align-items: center;
+ gap: 11px;
+}
+
+.pulse-identity > span:first-child {
+ width: 33px;
+ height: 33px;
+ display: grid;
+ flex: 0 0 33px;
+ place-items: center;
+ border: 1px solid rgba(100, 137, 246, 0.2);
+ border-radius: 9px;
+ background: rgba(100, 137, 246, 0.08);
+ color: var(--blue);
+}
+
+.connected .pulse-identity > span:first-child {
+ border-color: rgba(159, 248, 80, 0.2);
+ background: rgba(159, 248, 80, 0.06);
+ color: var(--lime);
+}
+
+.pulse-identity svg,
+.command-live-pulse > footer svg {
+ width: 16px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.pulse-identity > span:last-child {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.command-live-pulse > footer {
+ border-top: 1px solid var(--border);
+ border-bottom: 0;
+ letter-spacing: 0;
+}
+
+.command-live-pulse > footer svg {
+ width: 13px;
+ flex: 0 0 13px;
+}
+
+.command-live-pulse > footer b {
+ color: var(--muted-strong);
+}
+
+.command-live-pulse > footer button {
+ margin-left: auto;
+ padding: 5px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--surface-raised);
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 7px;
+ cursor: pointer;
+}
+
+.command-lead-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 350px;
+ gap: 18px;
+}
+
+.command-feature {
+ position: relative;
+ min-width: 0;
+ min-height: 548px;
+ display: grid;
+ grid-template-columns: minmax(310px, 0.85fr) minmax(370px, 1.15fr);
+ overflow: hidden;
+ background:
+ linear-gradient(115deg, rgba(20, 26, 52, 0.96) 0 44%, rgba(10, 14, 31, 0.96) 44% 100%),
+ var(--surface);
+}
+
+.command-feature::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 2px;
+ content: "";
+ background: linear-gradient(90deg, var(--lime) 0 24%, var(--violet) 24% 72%, transparent 96%);
+}
+
+.command-feature::after {
+ position: absolute;
+ right: -170px;
+ bottom: -190px;
+ width: 440px;
+ height: 440px;
+ border: 1px solid rgba(145, 131, 229, 0.08);
+ border-radius: 50%;
+ box-shadow: 0 0 0 58px rgba(145, 131, 229, 0.025), 0 0 0 116px rgba(145, 131, 229, 0.018);
+ content: "";
+ pointer-events: none;
+}
+
+.feature-copy,
+.feature-visual {
+ position: relative;
+ z-index: 1;
+ min-width: 0;
+}
+
+.feature-copy {
+ display: flex;
+ flex-direction: column;
+ padding: 30px 30px 28px;
+}
+
+.feature-eyebrow,
+.fixture-status,
+.feature-eyebrow > span {
+ display: flex;
+ align-items: center;
+}
+
+.feature-eyebrow {
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.feature-eyebrow > span {
+ gap: 7px;
+ color: var(--lime);
+ font-size: 9px;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.feature-eyebrow > span i,
+.fixture-status > span i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lime), transparent 89%);
+}
+
+.fixture-card.failed .fixture-status > span,
+.feed-state.failed > span {
+ color: var(--danger);
+}
+
+.fixture-card.failed .fixture-status > span i,
+.feed-state.failed > span i {
+ background: var(--danger);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger), transparent 88%);
+}
+
+.fixture-card.complete .fixture-status > span i,
+.feed-state.complete > span i {
+ background: var(--lime);
+}
+
+.fixture-card.unknown .fixture-status > span,
+.feed-state.unknown > span {
+ color: var(--muted);
+}
+
+.feature-eyebrow em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 9px;
+ font-style: normal;
+}
+
+.feature-scoreline {
+ display: grid;
+ grid-template-columns: 1fr 82px 1fr;
+ align-items: center;
+ gap: 11px;
+ margin: 26px 0 32px;
+}
+
+.feature-team {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.feature-team.away {
+ justify-content: flex-end;
+}
+
+.feature-team b {
+ overflow: hidden;
+ font-size: 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.command-crest {
+ width: 41px;
+ height: 41px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ overflow: hidden;
+ border: 2px solid rgba(255, 255, 255, 0.4);
+ border-radius: 50%;
+ color: #fff;
+ font-size: 9px;
+ font-style: normal;
+ font-weight: 800;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
+}
+
+.command-crest.esp {
+ background: linear-gradient(#b82835 0 28%, #e6bd55 28% 72%, #b82835 72%);
+}
+
+.command-crest.bel {
+ background: linear-gradient(90deg, #16171d 0 33%, #e0c348 33% 66%, #b82d39 66%);
+}
+
+.feature-score {
+ display: grid;
+ justify-items: center;
+ gap: 1px;
+}
+
+.feature-score small,
+.feature-score em {
+ color: var(--muted);
+ font-size: 8px;
+ font-style: normal;
+ text-transform: uppercase;
+}
+
+.feature-score strong {
+ font-family: var(--mono);
+ font-size: 24px;
+ letter-spacing: -0.08em;
+}
+
+.feature-kicker {
+ color: var(--amber);
+ font-family: var(--mono);
+ font-size: 9px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.feature-copy h2 {
+ max-width: 420px;
+ margin: 8px 0 10px;
+ font-size: clamp(28px, 2.45vw, 42px);
+ line-height: 1.02;
+ letter-spacing: -0.06em;
+}
+
+.feature-copy > p {
+ max-width: 440px;
+ margin: 0;
+ color: var(--text-soft);
+ font-size: 11px;
+ line-height: 1.65;
+}
+
+.feature-metrics {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0;
+ margin-top: 24px;
+ padding: 14px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.feature-metrics > div {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+ padding: 0 12px;
+ border-right: 1px solid var(--border);
+}
+
+.feature-metrics > div:first-child {
+ padding-left: 0;
+}
+
+.feature-metrics > div:last-child {
+ padding-right: 0;
+ border-right: 0;
+}
+
+.feature-metrics span {
+ color: var(--muted);
+ font-size: 8px;
+ white-space: nowrap;
+}
+
+.feature-metrics b {
+ font-family: var(--mono);
+ font-size: 16px;
+}
+
+.feature-metrics .gap b {
+ color: var(--amber);
+}
+
+.feature-verdict {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 17px;
+}
+
+.feature-verdict > span:first-child {
+ width: 32px;
+ height: 32px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 9px;
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.feature-verdict > span:last-child {
+ display: grid;
+ gap: 2px;
+}
+
+.feature-verdict small {
+ color: var(--muted);
+ font-size: 8px;
+ text-transform: uppercase;
+}
+
+.feature-verdict b {
+ color: var(--amber);
+ font-size: 11px;
+}
+
+.command-primary-action {
+ width: fit-content;
+ min-height: 42px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: auto;
+ padding: 0 14px;
+ border-radius: 11px;
+ background: var(--lime);
+ color: #081018;
+ font-size: 10px;
+ font-weight: 800;
+ text-decoration: none;
+ box-shadow: 0 12px 30px rgba(117, 213, 44, 0.12);
+}
+
+.command-primary-action svg:last-child {
+ width: 14px;
+ margin-left: 6px;
+}
+
+.feature-visual {
+ display: grid;
+ align-content: center;
+ padding: 36px 27px 28px;
+ border-left: 1px solid var(--border);
+ background:
+ linear-gradient(rgba(100, 137, 246, 0.025) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(100, 137, 246, 0.025) 1px, transparent 1px);
+ background-size: 34px 34px;
+}
+
+.command-chart {
+ min-width: 0;
+ margin: 0;
+}
+
+.command-chart-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 19px;
+ color: var(--muted);
+ font-size: 8px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.command-chart-head > span:last-child {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ letter-spacing: 0;
+ text-transform: none;
+}
+
+.chart-bid-dot,
+.chart-ask-dot {
+ width: 15px;
+ height: 2px;
+ display: inline-block;
+ margin-left: 4px;
+ border-radius: 2px;
+ background: var(--lime);
+}
+
+.chart-ask-dot {
+ background: var(--violet);
+}
+
+.command-chart svg {
+ width: 100%;
+ height: auto;
+ overflow: visible;
+}
+
+.command-chart-grid line {
+ stroke: var(--border);
+ stroke-dasharray: 2 7;
+}
+
+.command-spread-area {
+ fill: url(#command-spread);
+}
+
+.command-bid-line,
+.command-ask-line {
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 3;
+}
+
+.command-bid-line {
+ stroke: var(--lime);
+}
+
+.command-ask-line {
+ stroke: var(--violet);
+}
+
+.command-goal line {
+ stroke: color-mix(in srgb, var(--lime), transparent 45%);
+ stroke-dasharray: 3 6;
+}
+
+.command-goal circle {
+ fill: var(--surface-soft);
+ stroke: var(--violet);
+ stroke-width: 3;
+}
+
+.command-goal text,
+.command-chart-axis {
+ fill: var(--muted);
+ font-family: var(--mono);
+ font-size: 9px;
+}
+
+.command-goal text {
+ fill: var(--lime);
+ font-size: 8px;
+ font-weight: 700;
+}
+
+.chart-conclusion {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ gap: 3px 18px;
+ margin: 20px 7px 0;
+ padding: 14px 16px;
+ border: 1px solid color-mix(in srgb, var(--amber), transparent 70%);
+ border-radius: 12px;
+ background: var(--amber-soft);
+}
+
+.chart-conclusion span,
+.chart-conclusion small {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.chart-conclusion b {
+ grid-column: 2;
+ grid-row: 1 / span 2;
+ color: var(--amber);
+ font-family: var(--mono);
+ font-size: 18px;
+}
+
+.capture-schedule,
+.command-cases,
+.command-study,
+.command-proof {
+ min-width: 0;
+ padding: 22px;
+}
+
+.command-panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.command-panel-head > div {
+ min-width: 0;
+ display: grid;
+ gap: 3px;
+}
+
+.command-panel-head > div > span {
+ color: var(--muted);
+ font-size: 8px;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.command-panel-head h2 {
+ margin: 0;
+ font-size: 17px;
+ letter-spacing: -0.04em;
+}
+
+.command-panel-head > a {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--lime-deep);
+ font-size: 9px;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.command-panel-head > a svg {
+ width: 13px;
+}
+
+.schedule-count,
+.sealed-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--blue), transparent 90%);
+ color: var(--blue);
+ font-size: 8px;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+.schedule-intro {
+ margin: 13px 0 17px;
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.55;
+}
+
+.fixture-stack {
+ display: grid;
+ gap: 10px;
+}
+
+.fixture-card {
+ display: grid;
+ grid-template-columns: 42px minmax(0, 1fr);
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 13px;
+ background: var(--surface-soft);
+}
+
+.fixture-card.lead {
+ border-color: color-mix(in srgb, var(--lime), transparent 75%);
+ box-shadow: inset 2px 0 var(--lime);
+}
+
+.fixture-date {
+ display: grid;
+ align-content: start;
+ justify-items: center;
+ gap: 0;
+ padding: 14px 6px;
+ border-right: 1px solid var(--border);
+ color: var(--muted);
+}
+
+.fixture-date b {
+ color: var(--text);
+ font-family: var(--mono);
+ font-size: 17px;
+}
+
+.fixture-date span {
+ font-size: 8px;
+ font-weight: 800;
+}
+
+.fixture-card-main {
+ min-width: 0;
+ padding: 13px 12px;
+}
+
+.fixture-status {
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.fixture-status > span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--lime-deep);
+ font-size: 8px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.fixture-status em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+ font-style: normal;
+}
+
+.fixture-versus {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1fr);
+ align-items: center;
+ gap: 5px;
+ margin-top: 13px;
+}
+
+.fixture-versus > span {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.fixture-versus > span:last-child {
+ justify-content: flex-end;
+}
+
+.fixture-versus b {
+ overflow: hidden;
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.fixture-versus > em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+ font-style: normal;
+ text-align: center;
+}
+
+.mini-crest {
+ width: 27px;
+ height: 27px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid rgba(255, 255, 255, 0.23);
+ border-radius: 50%;
+ color: #fff;
+ font-size: 6px;
+ font-style: normal;
+ font-weight: 800;
+ text-shadow: 0 1px 2px #000;
+}
+
+.mini-crest.fra { background: linear-gradient(90deg, #1f3b84 0 33%, #eceff5 33% 66%, #b22e3c 66%); }
+.mini-crest.esp { background: linear-gradient(#b82835 0 28%, #e6bd55 28% 72%, #b82835 72%); }
+.mini-crest.eng { background: linear-gradient(90deg, #e6e8ed 0 44%, #b82e3b 44% 56%, #e6e8ed 56%); color: #202332; text-shadow: none; }
+.mini-crest.arg { background: linear-gradient(#72b7de 0 33%, #f3f2e9 33% 66%, #72b7de 66%); color: #27345e; text-shadow: none; }
+
+.fixture-times {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ margin-top: 13px;
+ padding: 10px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.fixture-times > span {
+ display: grid;
+ gap: 2px;
+}
+
+.fixture-times small,
+.fixture-boundary small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.fixture-times b {
+ font-family: var(--mono);
+ font-size: 9px;
+}
+
+.fixture-boundary,
+.schedule-rule,
+.proof-policy {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.fixture-boundary {
+ margin-top: 10px;
+ color: var(--blue);
+}
+
+.fixture-boundary svg {
+ width: 14px;
+ flex: 0 0 auto;
+}
+
+.fixture-boundary > span,
+.schedule-rule > span,
+.proof-policy > span {
+ display: grid;
+ gap: 1px;
+}
+
+.fixture-boundary b {
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.schedule-rule {
+ align-items: flex-start;
+ margin-top: 15px;
+ padding: 12px;
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--blue), transparent 94%);
+ color: var(--blue);
+}
+
+.schedule-rule b,
+.proof-policy b {
+ color: var(--text-soft);
+ font-size: 9px;
+}
+
+.schedule-rule small,
+.proof-policy small {
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.45;
+}
+
+.command-evidence-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.45fr) minmax(330px, 0.75fr);
+ align-items: start;
+ gap: 18px;
+ margin-top: 18px;
+}
+
+.command-cases {
+ min-height: 475px;
+}
+
+.case-table-head,
+.command-case-row {
+ display: grid;
+ grid-template-columns: 1.25fr 0.95fr 0.82fr 1fr;
+ align-items: center;
+ gap: 16px;
+}
+
+.case-table-head {
+ margin-top: 22px;
+ padding: 0 14px 9px;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-size: 8px;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.command-case-row {
+ min-height: 84px;
+ margin-top: 8px;
+ padding: 12px 14px;
+ border: 1px solid color-mix(in srgb, var(--lime), transparent 81%);
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--lime), transparent 96%);
+ color: inherit;
+ text-decoration: none;
+ transition: border-color 180ms ease, transform 180ms ease;
+}
+
+.command-case-row:hover {
+ border-color: color-mix(in srgb, var(--lime), transparent 62%);
+ transform: translateY(-2px);
+}
+
+.case-identity,
+.case-evidence,
+.case-outcome {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.case-identity > i {
+ width: 31px;
+ height: 31px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 9px;
+ background: var(--lime);
+ color: #081018;
+ font-size: 7px;
+ font-style: normal;
+ font-weight: 900;
+}
+
+.case-identity > span,
+.case-evidence > span,
+.case-market,
+.case-outcome {
+ min-width: 0;
+ display: grid;
+ gap: 3px;
+}
+
+.command-case-row b {
+ overflow: hidden;
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.command-case-row small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 8px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.case-evidence {
+ display: flex;
+}
+
+.case-evidence > i {
+ width: 7px;
+ height: 7px;
+ flex: 0 0 auto;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lime), transparent 89%);
+}
+
+.case-evidence b {
+ color: var(--lime-deep);
+}
+
+.case-outcome {
+ position: relative;
+ padding-right: 23px;
+}
+
+.case-outcome b {
+ color: var(--amber);
+}
+
+.case-outcome svg {
+ position: absolute;
+ right: 0;
+ width: 14px;
+ color: var(--muted);
+}
+
+.case-empty {
+ min-height: 104px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 13px;
+ padding: 18px;
+ border: 1px dashed var(--border-strong);
+ border-radius: 12px;
+ background: var(--surface-soft);
+}
+
+.case-empty > span:first-child {
+ width: 36px;
+ height: 36px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--violet), transparent 90%);
+ color: var(--violet);
+}
+
+.case-empty > span:last-child {
+ display: grid;
+ gap: 3px;
+}
+
+.case-empty b {
+ font-size: 10px;
+}
+
+.case-empty small {
+ color: var(--muted);
+ font-size: 9px;
+ line-height: 1.5;
+}
+
+.command-side-stack {
+ display: grid;
+ gap: 18px;
+}
+
+.sealed-label {
+ background: color-mix(in srgb, var(--violet), transparent 90%);
+ color: var(--violet);
+}
+
+.sealed-label svg {
+ width: 12px;
+}
+
+.study-message {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ margin-top: 18px;
+ padding: 13px;
+ border: 1px solid color-mix(in srgb, var(--violet), transparent 80%);
+ border-radius: 11px;
+ background: color-mix(in srgb, var(--violet), transparent 95%);
+}
+
+.study-lock {
+ width: 32px;
+ height: 32px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 9px;
+ background: color-mix(in srgb, var(--violet), transparent 86%);
+ color: var(--violet);
+}
+
+.study-message > span:last-child {
+ display: grid;
+ gap: 3px;
+}
+
+.study-message b {
+ font-size: 9px;
+}
+
+.study-message small {
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.4;
+}
+
+.study-progress {
+ display: grid;
+ gap: 15px;
+ margin-top: 19px;
+}
+
+.study-progress > div {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 5px;
+}
+
+.study-progress span {
+ font-family: var(--mono);
+ font-size: 11px;
+}
+
+.study-progress span b {
+ font-size: 17px;
+}
+
+.study-progress small {
+ align-self: end;
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-progress > div > i {
+ grid-column: 1 / -1;
+ height: 5px;
+ overflow: hidden;
+ border-radius: 5px;
+ background: var(--surface-soft);
+}
+
+.study-progress em {
+ height: 100%;
+ display: block;
+ min-width: 3px;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--lime), var(--blue));
+}
+
+.study-meta {
+ display: grid;
+ grid-template-columns: 0.75fr 1.25fr;
+ gap: 8px;
+ margin-top: 19px;
+ padding-top: 14px;
+ border-top: 1px solid var(--border);
+}
+
+.study-meta > span {
+ min-width: 0;
+ display: grid;
+ gap: 3px;
+}
+
+.study-meta small,
+.command-proof-grid small,
+.command-hash span {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-meta b,
+.study-meta code {
+ overflow: hidden;
+ font-family: var(--mono);
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.study-meta b {
+ color: var(--amber);
+}
+
+.command-proof-hero {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ margin-top: 18px;
+}
+
+.command-proof-hero > span {
+ width: 54px;
+ height: 54px;
+ display: grid;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--lime), transparent 63%);
+ border-radius: 50%;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 16px;
+ box-shadow: inset 0 0 0 5px color-mix(in srgb, var(--lime), transparent 93%);
+}
+
+.command-proof-hero > span small {
+ margin-left: -14px;
+ font-size: 8px;
+ transform: translate(16px, 4px);
+}
+
+.command-proof-hero > div {
+ display: grid;
+ gap: 3px;
+}
+
+.command-proof-hero b {
+ font-size: 10px;
+}
+
+.command-proof-hero small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.command-proof-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1px;
+ margin-top: 17px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--border);
+}
+
+.command-proof-grid > span {
+ min-width: 0;
+ display: grid;
+ gap: 3px;
+ padding: 10px;
+ background: var(--surface-soft);
+}
+
+.command-proof-grid b {
+ overflow: hidden;
+ font-family: var(--mono);
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.command-hash {
+ display: grid;
+ gap: 4px;
+ margin-top: 10px;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 9px;
+ background: var(--surface-soft);
+}
+
+.command-hash code {
+ overflow: hidden;
+ color: var(--text-soft);
+ font-family: var(--mono);
+ font-size: 9px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.proof-policy {
+ align-items: flex-start;
+ margin-top: 10px;
+ padding: 10px;
+ border-radius: 9px;
+ background: color-mix(in srgb, var(--blue), transparent 94%);
+ color: var(--blue);
+}
+
+.proof-policy svg {
+ width: 14px;
+ flex: 0 0 auto;
+}
+
+@media (max-width: 1180px) {
+ .app-shell {
+ grid-template-columns: 80px minmax(0, 1fr);
+ }
+
+ .sidebar {
+ align-items: center;
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+
+ .brand {
+ padding: 0;
+ }
+
+ .brand > span:last-child,
+ .nav-group,
+ .side-nav a > span,
+ .side-nav a em,
+ .feed-dot,
+ .gate-card > span:last-child,
+ .owner > span:last-child {
+ display: none;
+ }
+
+ .side-nav {
+ width: 100%;
+ }
+
+ .side-nav a {
+ justify-content: center;
+ padding: 0;
+ }
+
+ .system-group {
+ display: block;
+ height: 1px;
+ margin: 26px 7px 13px;
+ overflow: hidden;
+ background: var(--border);
+ color: transparent;
+ }
+
+ .gate-card,
+ .owner {
+ justify-content: center;
+ padding: 7px;
+ }
+
+ .match-masthead {
+ grid-template-columns: 120px minmax(140px, 1fr) auto minmax(140px, 1fr) 190px;
+ gap: 15px;
+ }
+
+ .analysis-grid,
+ .support-grid {
+ grid-template-columns: minmax(0, 1.8fr) minmax(300px, 1fr);
+ }
+
+ .comparison-strip > div {
+ padding: 0 14px;
+ }
+
+ .comparison-strip strong {
+ font-size: 23px;
+ }
+
+ .command-system-deck {
+ grid-template-columns: 240px minmax(440px, 1fr) 100px;
+ }
+
+ .system-posture {
+ padding-left: 20px;
+ }
+
+ .feed-state {
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+
+ .command-lead-grid {
+ grid-template-columns: minmax(0, 1fr) 330px;
+ }
+
+ .command-live-pulse-grid {
+ grid-template-columns: minmax(260px, 1.3fr) repeat(2, 1fr);
+ }
+
+ .command-live-pulse-grid > div:nth-child(3) {
+ border-right: 0;
+ }
+
+ .command-live-pulse-grid > div:nth-child(-n + 3) {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .command-feature {
+ grid-template-columns: minmax(275px, 0.82fr) minmax(330px, 1.18fr);
+ }
+
+ .feature-copy {
+ padding-right: 23px;
+ padding-left: 23px;
+ }
+
+ .feature-copy h2 {
+ font-size: 30px;
+ }
+
+ .case-table-head,
+ .command-case-row {
+ grid-template-columns: 1.2fr 0.9fr 0.8fr 1fr;
+ gap: 10px;
+ }
+}
+
+@media (max-width: 980px) {
+ .context-bar {
+ grid-template-columns: 1fr auto;
+ }
+
+ .view-tabs {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ justify-content: center;
+ }
+
+ .match-masthead {
+ grid-template-columns: 1fr auto 1fr;
+ }
+
+ .match-meta {
+ grid-column: 1 / -1;
+ grid-row: 1;
+ flex-direction: row;
+ }
+
+ .market-meta {
+ grid-column: 1 / -1;
+ min-height: auto;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 10px;
+ padding: 14px 0 0;
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .market-meta b,
+ .market-meta span {
+ margin: 0;
+ }
+
+ .analysis-grid,
+ .support-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .probability-panel,
+ .decision-rail {
+ min-height: auto;
+ }
+
+ .decision-stages {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0 20px;
+ }
+
+ .decision-stages li:not(:last-child)::after {
+ display: none;
+ }
+
+ .command-system-deck {
+ grid-template-columns: 1fr 112px;
+ }
+
+ .system-posture {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .feed-deck {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ }
+
+ .deck-time {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .command-lead-grid,
+ .command-evidence-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .command-live-pulse-grid {
+ grid-template-columns: minmax(240px, 1.3fr) 1fr 1fr;
+ }
+
+ .command-feature {
+ grid-template-columns: minmax(300px, 0.8fr) minmax(380px, 1.2fr);
+ }
+
+ .capture-schedule {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0 18px;
+ }
+
+ .capture-schedule .command-panel-head,
+ .capture-schedule .schedule-intro,
+ .capture-schedule .schedule-rule {
+ grid-column: 1 / -1;
+ }
+
+ .fixture-stack {
+ grid-column: 1 / -1;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .command-cases {
+ min-height: auto;
+ }
+
+ .command-side-stack {
+ grid-template-columns: 1fr 1fr;
+ align-items: stretch;
+ }
+}
+
+@media (max-width: 720px) {
+ .theme-frame {
+ display: none !important;
+ }
+
+ .app-shell {
+ display: block;
+ }
+
+ .sidebar {
+ display: none;
+ }
+
+ .workspace {
+ padding-bottom: 92px;
+ }
+
+ .topbar {
+ min-height: 112px;
+ align-content: center;
+ flex-wrap: wrap;
+ padding: 12px 15px;
+ }
+
+ .mobile-brand {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ width: 100%;
+ font-size: 16px;
+ letter-spacing: -0.04em;
+ }
+
+ .mobile-brand .brand-mark {
+ width: 28px;
+ height: 28px;
+ }
+
+ .page-title {
+ display: none;
+ }
+
+ .system-state {
+ width: 100%;
+ justify-content: flex-start;
+ gap: 6px;
+ }
+
+ .state-chip {
+ min-height: 32px;
+ padding: 0 9px;
+ font-size: 10px;
+ }
+
+ .state-chip.gate {
+ width: auto;
+ padding: 0 9px;
+ font-size: 10px;
+ }
+
+ .context-bar {
+ min-height: 118px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding: 11px 15px;
+ }
+
+ .fixture-select {
+ flex: 1;
+ }
+
+ .fixture-select small,
+ .original-date small {
+ font-size: 9px;
+ }
+
+ .fixture-select b,
+ .original-date b {
+ font-size: 11px;
+ }
+
+ .original-date {
+ margin-left: auto;
+ }
+
+ .view-tabs {
+ width: 100%;
+ height: 40px;
+ overflow-x: auto;
+ justify-content: flex-start;
+ scrollbar-width: none;
+ }
+
+ .view-tabs::-webkit-scrollbar {
+ display: none;
+ }
+
+ .view-tabs a {
+ flex: 1 0 auto;
+ height: 30px;
+ padding: 0 12px;
+ font-size: 10px;
+ }
+
+ .content {
+ padding: 14px 12px 0;
+ }
+
+ .surface {
+ border-radius: 15px;
+ }
+
+ .command-system-deck {
+ display: block;
+ }
+
+ .system-posture {
+ min-height: 88px;
+ padding: 15px;
+ border-right: 0;
+ }
+
+ .system-posture b {
+ font-size: 12px;
+ }
+
+ .system-posture em {
+ white-space: normal;
+ }
+
+ .deck-time {
+ display: none;
+ }
+
+ .feed-deck {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .feed-state {
+ min-height: 72px;
+ padding: 11px 15px;
+ border-top: 1px solid var(--border);
+ }
+
+ .feed-state:nth-child(2n) {
+ border-right: 0;
+ }
+
+ .feed-state small {
+ font-size: 8px;
+ }
+
+ .command-content {
+ padding: 14px 12px 0;
+ }
+
+ .command-live-pulse-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .command-live-pulse-grid .pulse-identity {
+ grid-column: 1 / -1;
+ border-right: 0;
+ }
+
+ .command-live-pulse-grid > div:nth-child(odd),
+ .command-live-pulse-grid > div:nth-child(3) {
+ border-right: 1px solid var(--border);
+ }
+
+ .command-live-pulse-grid > div:nth-child(even),
+ .command-live-pulse-grid > div:last-child {
+ border-right: 0;
+ }
+
+ .command-live-pulse-grid .pulse-identity {
+ border-right: 0;
+ }
+
+ .command-live-pulse > footer {
+ align-items: flex-start;
+ flex-wrap: wrap;
+ }
+
+ .command-lead-grid,
+ .command-evidence-grid {
+ gap: 14px;
+ }
+
+ .command-evidence-grid {
+ margin-top: 14px;
+ }
+
+ .command-feature {
+ min-height: 0;
+ grid-template-columns: 1fr;
+ }
+
+ .feature-copy {
+ padding: 21px 18px 19px;
+ }
+
+ .feature-scoreline {
+ margin: 21px 0 26px;
+ }
+
+ .feature-copy h2 {
+ font-size: 31px;
+ }
+
+ .feature-copy > p {
+ font-size: 10px;
+ }
+
+ .feature-metrics b {
+ font-size: 14px;
+ }
+
+ .command-primary-action {
+ width: 100%;
+ justify-content: center;
+ margin-top: 22px;
+ }
+
+ .feature-visual {
+ padding: 24px 15px 19px;
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .command-chart-head {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .chart-conclusion {
+ margin-right: 0;
+ margin-left: 0;
+ }
+
+ .capture-schedule,
+ .command-cases,
+ .command-study,
+ .command-proof {
+ padding: 18px 15px;
+ }
+
+ .capture-schedule {
+ display: block;
+ }
+
+ .fixture-stack {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .case-table-head {
+ display: none;
+ }
+
+ .command-case-row {
+ grid-template-columns: 1fr 1fr;
+ gap: 13px;
+ margin-top: 18px;
+ padding: 14px;
+ }
+
+ .case-identity,
+ .case-market {
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .case-market {
+ align-self: stretch;
+ }
+
+ .case-evidence,
+ .case-outcome {
+ min-height: 35px;
+ }
+
+ .case-empty {
+ min-height: 94px;
+ }
+
+ .command-side-stack {
+ grid-template-columns: 1fr;
+ gap: 14px;
+ }
+
+ .match-masthead {
+ min-height: 236px;
+ grid-template-columns: 1fr 92px 1fr;
+ gap: 8px;
+ padding: 17px 15px;
+ }
+
+ .match-meta {
+ font-size: 10px;
+ }
+
+ .live-label,
+ .fixture-number {
+ font-size: 9px;
+ }
+
+ .team {
+ flex-direction: column;
+ gap: 7px;
+ text-align: center;
+ }
+
+ .away-team {
+ flex-direction: column-reverse;
+ }
+
+ .team > span:last-child,
+ .away-team > span:first-child {
+ justify-items: center;
+ }
+
+ .team small {
+ font-size: 14px;
+ }
+
+ .team b {
+ font-size: 10px;
+ }
+
+ .crest {
+ width: 45px;
+ height: 45px;
+ }
+
+ .score-block {
+ min-width: 80px;
+ }
+
+ .score-block strong {
+ font-size: 33px;
+ }
+
+ .score-block small,
+ .clock {
+ font-size: 9px;
+ }
+
+ .market-meta {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ padding-top: 12px;
+ text-align: center;
+ }
+
+ .market-meta small,
+ .market-meta span {
+ font-size: 10px;
+ }
+
+ .market-meta b {
+ font-size: 13px;
+ }
+
+ .analysis-grid,
+ .support-grid {
+ gap: 14px;
+ margin-top: 14px;
+ }
+
+ .probability-panel,
+ .decision-rail,
+ .evidence-panel,
+ .proof-panel {
+ padding: 18px 15px;
+ }
+
+ .panel-heading {
+ align-items: flex-start;
+ }
+
+ .panel-heading > div > span,
+ .panel-heading > span:first-child {
+ font-size: 10px;
+ }
+
+ .panel-heading h2 {
+ font-size: 17px;
+ }
+
+ .panel-heading {
+ flex-wrap: wrap;
+ }
+
+ .legend {
+ width: 100%;
+ display: flex;
+ gap: 11px;
+ margin-top: 7px;
+ font-size: 12px;
+ }
+
+ .comparison-strip {
+ grid-template-columns: 1fr;
+ gap: 0;
+ margin-top: 18px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 13px;
+ background: var(--surface-soft);
+ }
+
+ .comparison-strip > div,
+ .comparison-strip > div:first-child {
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 3px 14px;
+ padding: 14px;
+ border-right: 0;
+ }
+
+ .comparison-strip > div:not(:last-child) {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .comparison-strip > div > span {
+ align-self: end;
+ font-size: 12px;
+ }
+
+ .comparison-strip strong {
+ grid-column: 2;
+ grid-row: 1 / span 2;
+ font-size: 22px;
+ }
+
+ .comparison-strip small {
+ grid-column: 1;
+ font-size: 10px;
+ }
+
+ .plain-conclusion {
+ grid-template-columns: auto 1fr;
+ align-items: start;
+ padding: 14px;
+ }
+
+ .plain-conclusion p {
+ font-size: 12px;
+ }
+
+ .discipline-chip {
+ grid-column: 2;
+ justify-self: start;
+ padding: 6px 9px;
+ font-size: 10px;
+ }
+
+ .chart-wrap {
+ margin-top: 16px;
+ }
+
+ .chart-wrap > svg {
+ height: auto;
+ }
+
+ .chart-axis {
+ font-size: 12px;
+ }
+
+ .goal-event text {
+ font-size: 11px;
+ }
+
+ .chart-wrap figcaption {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 5px;
+ font-size: 10px;
+ line-height: 1.45;
+ }
+
+ .replay-bar {
+ flex-wrap: wrap;
+ }
+
+ .play-button {
+ flex: 1;
+ justify-content: center;
+ font-size: 11px;
+ }
+
+ .speed {
+ margin-left: 0;
+ font-size: 11px;
+ }
+
+ .replay-steps {
+ order: 3;
+ width: 100%;
+ height: 42px;
+ }
+
+ .replay-steps button {
+ flex: 1;
+ height: 34px;
+ padding: 0 6px;
+ font-size: 10px;
+ }
+
+ .decision-outcome {
+ padding: 24px 10px 20px;
+ }
+
+ .decision-outcome > strong {
+ font-size: 29px;
+ }
+
+ .decision-outcome p {
+ font-size: 12px;
+ }
+
+ .reason-block > span,
+ .reason-block p {
+ font-size: 10px;
+ }
+
+ .reason-block b {
+ font-size: 13px;
+ }
+
+ .decision-stages {
+ display: block;
+ }
+
+ .decision-stages li:not(:last-child)::after {
+ display: block;
+ }
+
+ .decision-stages b {
+ font-size: 12px;
+ }
+
+ .decision-stages small,
+ .decision-stages time {
+ font-size: 10px;
+ }
+
+ .boundary-grid span,
+ .protective-gate small {
+ font-size: 9px;
+ }
+
+ .protective-gate b {
+ font-size: 12px;
+ }
+
+ .evidence-table-wrap {
+ overflow: visible;
+ }
+
+ .evidence-table,
+ .evidence-table tbody,
+ .evidence-table tr,
+ .evidence-table td {
+ width: 100%;
+ display: block;
+ }
+
+ .evidence-table {
+ min-width: 0;
+ }
+
+ .evidence-table thead {
+ display: none;
+ }
+
+ .evidence-table tr {
+ display: grid;
+ grid-template-columns: minmax(115px, 0.9fr) minmax(0, 1.1fr);
+ gap: 9px 12px;
+ margin-top: 10px;
+ padding: 13px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: var(--surface-soft);
+ }
+
+ .evidence-table tr.active {
+ border-color: color-mix(in srgb, var(--blue), transparent 66%);
+ background: color-mix(in srgb, var(--blue), transparent 95%);
+ box-shadow: inset 2px 0 var(--blue);
+ }
+
+ .evidence-table td,
+ .evidence-table tr.active td,
+ .evidence-table tr.active td:first-child {
+ width: auto;
+ height: auto;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ }
+
+ .evidence-table td:nth-child(3) {
+ grid-column: 1 / -1;
+ padding: 9px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ font-size: 12px;
+ }
+
+ .evidence-table td:nth-child(5) {
+ align-self: center;
+ justify-self: end;
+ }
+
+ .evidence-table time,
+ .evidence-table td:nth-child(4) b {
+ font-size: 11px;
+ }
+
+ .evidence-table td small,
+ .assessment {
+ font-size: 10px;
+ }
+
+ .proof-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .proof-stats div {
+ padding: 4px 0;
+ }
+
+ .proof-stats dt,
+ .hash span {
+ font-size: 10px;
+ }
+
+ .proof-stats dd,
+ .hash code {
+ font-size: 11px;
+ }
+
+ .mobile-nav {
+ position: fixed;
+ right: 9px;
+ bottom: 9px;
+ left: 9px;
+ z-index: 30;
+ height: 68px;
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ padding: 6px;
+ border: 1px solid var(--border-strong);
+ border-radius: 17px;
+ background: color-mix(in srgb, var(--surface), transparent 7%);
+ box-shadow: 0 22px 55px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(18px);
+ }
+
+ .mobile-nav a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 4px;
+ border-radius: 10px;
+ color: var(--muted);
+ font-size: 9px;
+ font-weight: 700;
+ text-decoration: none;
+ }
+
+ .mobile-nav a.active {
+ background: color-mix(in srgb, var(--lime), transparent 89%);
+ color: var(--lime-deep);
+ }
+
+ .mobile-nav svg {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+@media (max-width: 380px) {
+ .original-date {
+ display: none;
+ }
+
+ .match-masthead {
+ grid-template-columns: 1fr 82px 1fr;
+ }
+
+ .fixture-number {
+ display: none;
+ }
+
+ .comparison-strip strong {
+ font-size: 20px;
+ }
+
+ .boundary-grid div {
+ padding: 0 7px;
+ }
+
+ .mobile-nav a {
+ font-size: 8px;
+ }
+
+ .feature-scoreline {
+ grid-template-columns: 1fr 70px 1fr;
+ }
+
+ .feature-team {
+ flex-direction: column;
+ }
+
+ .feature-team.away {
+ flex-direction: column-reverse;
+ }
+
+ .feature-metrics > div {
+ padding-right: 8px;
+ padding-left: 8px;
+ }
+
+ .feature-metrics b {
+ font-size: 12px;
+ }
+
+ .command-case-row {
+ grid-template-columns: 1fr;
+ }
+
+ .case-identity,
+ .case-market,
+ .case-evidence {
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+/* Casebook: captured retrospective case record */
+.casebook-shell {
+ background:
+ radial-gradient(circle at 70% -8%, rgba(159, 248, 80, 0.055), transparent 31rem),
+ radial-gradient(circle at 97% 42%, rgba(100, 137, 246, 0.06), transparent 32rem),
+ var(--bg);
+}
+
+.casebook-content {
+ width: min(1480px, 100%);
+ margin: 0 auto;
+ padding: 26px 28px 76px;
+}
+
+.casebook-hero {
+ position: relative;
+ min-height: 252px;
+ display: grid;
+ grid-template-columns: minmax(360px, 0.82fr) minmax(580px, 1.18fr);
+ gap: 34px;
+ align-items: stretch;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 18px 18px 4px 4px;
+ background:
+ linear-gradient(90deg, rgba(159, 248, 80, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
+ linear-gradient(rgba(159, 248, 80, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
+ linear-gradient(128deg, #10162c 0%, #0c1021 56%, #12162b 100%);
+ box-shadow: var(--shadow);
+}
+
+.casebook-hero::before {
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 3px;
+ background: var(--lime);
+ content: "";
+}
+
+.casebook-hero::after {
+ position: absolute;
+ top: -148px;
+ left: 34%;
+ width: 310px;
+ height: 310px;
+ border: 1px solid rgba(159, 248, 80, 0.1);
+ border-radius: 50%;
+ content: "";
+}
+
+.casebook-hero-copy {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 34px 0 34px 40px;
+}
+
+.casebook-kicker {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 9px;
+ font-weight: 500;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.casebook-kicker i {
+ width: 20px;
+ height: 1px;
+ background: currentColor;
+}
+
+.casebook-hero h2 {
+ max-width: 580px;
+ margin: 14px 0 14px;
+ font-size: clamp(32px, 3.3vw, 54px);
+ font-weight: 800;
+ letter-spacing: -0.065em;
+ line-height: 0.98;
+}
+
+.casebook-hero h2 em {
+ color: var(--muted-strong);
+ font-style: normal;
+ font-weight: 500;
+}
+
+.casebook-hero-copy > p {
+ max-width: 570px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.72;
+}
+
+.casebook-stat-deck {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: 1.08fr 1fr 1fr 1.15fr;
+ grid-template-rows: minmax(132px, 1fr) 66px;
+ align-self: stretch;
+ margin-left: 6px;
+ border-left: 1px solid var(--border);
+ background: rgba(4, 7, 17, 0.28);
+}
+
+.casebook-stat-deck > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: 23px 20px;
+ border-right: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+.casebook-stat-deck > div > span,
+.casebook-stat-deck > div > small {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.casebook-stat-deck > div > span {
+ order: -2;
+ margin-bottom: auto;
+ font-family: var(--mono);
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+}
+
+.casebook-stat-deck > div > b {
+ overflow: hidden;
+ margin: 15px 0 4px;
+ font-size: clamp(22px, 2.25vw, 37px);
+ letter-spacing: -0.055em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.casebook-stat-deck .casebook-primary-stat b {
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: clamp(34px, 3.5vw, 58px);
+ font-weight: 500;
+ line-height: 0.9;
+}
+
+.casebook-verified-stat {
+ grid-column: 1 / -1;
+ flex-direction: row !important;
+ align-items: center;
+ justify-content: flex-start !important;
+ gap: 13px;
+ padding: 15px 20px !important;
+ border-bottom: 0 !important;
+ background: linear-gradient(90deg, rgba(159, 248, 80, 0.08), transparent 75%);
+}
+
+.casebook-verified-stat svg {
+ width: 22px;
+ fill: none;
+ stroke: var(--lime);
+ stroke-width: 1.5;
+}
+
+.casebook-verified-stat span {
+ order: initial !important;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin: 0 !important;
+ letter-spacing: 0 !important;
+ text-transform: none !important;
+}
+
+.casebook-verified-stat b {
+ color: var(--text);
+ font-size: 11px !important;
+ letter-spacing: -0.01em !important;
+}
+
+.casebook-verified-stat small {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.casebook-filters {
+ position: relative;
+ z-index: 3;
+ display: grid;
+ grid-template-columns: minmax(200px, 1.35fr) minmax(600px, 3.65fr) auto;
+ gap: 8px;
+ align-items: end;
+ margin-top: 12px;
+ padding: 10px;
+ border-radius: 4px 4px 12px 12px;
+}
+
+.casebook-search {
+ height: 51px;
+ display: grid;
+ grid-template-columns: 18px minmax(0, 1fr) auto;
+ grid-template-rows: 15px 19px;
+ column-gap: 10px;
+ align-content: center;
+ padding: 0 13px;
+ border: 1px solid var(--border-strong);
+ border-radius: 9px;
+ background: var(--surface-soft);
+}
+
+.casebook-search > svg {
+ grid-row: 1 / 3;
+ align-self: center;
+ width: 17px;
+ fill: none;
+ stroke: var(--muted-strong);
+ stroke-width: 1.5;
+}
+
+.casebook-search > span,
+.casebook-filter-select > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.casebook-search input {
+ width: 100%;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ color: var(--text);
+ font-size: 10px;
+}
+
+.casebook-search input::placeholder {
+ color: var(--muted);
+}
+
+.casebook-search kbd {
+ grid-column: 3;
+ grid-row: 1 / 3;
+ align-self: center;
+ padding: 3px 5px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.casebook-filter-grid {
+ display: grid;
+ grid-template-columns: repeat(8, minmax(0, 1fr));
+ gap: 5px;
+}
+
+.casebook-filter-select {
+ position: relative;
+ height: 51px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 3px;
+ min-width: 0;
+ padding: 0 24px 0 9px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: rgba(7, 10, 24, 0.5);
+}
+
+.casebook-filter-select select {
+ width: 100%;
+ overflow: hidden;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ appearance: none;
+ background: transparent;
+ color: var(--text-soft);
+ font-family: var(--sans);
+ font-size: 9px;
+ font-weight: 700;
+ text-overflow: ellipsis;
+}
+
+.casebook-filter-select > svg {
+ position: absolute;
+ right: 8px;
+ bottom: 12px;
+ width: 12px;
+ fill: none;
+ stroke: var(--muted);
+ stroke-width: 1.7;
+}
+
+.casebook-clear {
+ height: 51px;
+ padding: 0 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: transparent;
+ color: var(--text-soft);
+ font-size: 9px;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.casebook-clear:disabled {
+ opacity: 0.35;
+ cursor: default;
+}
+
+.casebook-workbench {
+ display: grid;
+ grid-template-columns: 340px minmax(0, 1fr);
+ gap: 12px;
+ align-items: start;
+ margin-top: 12px;
+}
+
+.casebook-index {
+ position: sticky;
+ top: 87px;
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.casebook-panel-head,
+.casebook-section-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.casebook-panel-head {
+ padding: 20px 20px 15px;
+ border-bottom: 1px solid var(--border);
+}
+
+.casebook-panel-head > div > span,
+.casebook-section-head > div > span {
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.11em;
+ text-transform: uppercase;
+}
+
+.casebook-panel-head h2,
+.casebook-section-head h3 {
+ margin: 4px 0 0;
+ font-size: 15px;
+ letter-spacing: -0.035em;
+}
+
+.casebook-panel-head > em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+ font-style: normal;
+}
+
+.casebook-index-row {
+ position: relative;
+ padding: 18px 18px 17px 20px;
+ border-bottom: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.01);
+}
+
+.casebook-index-row::before {
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 2px;
+ background: transparent;
+ content: "";
+}
+
+.casebook-index-row.selected {
+ background: linear-gradient(110deg, rgba(159, 248, 80, 0.08), rgba(159, 248, 80, 0.01) 72%);
+}
+
+.casebook-index-row.selected::before {
+ background: var(--lime);
+}
+
+.casebook-index-time,
+.casebook-index-match,
+.casebook-index-outcome {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.casebook-index-time time {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.casebook-index-time > span {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--lime);
+ font-size: 8px;
+ font-weight: 700;
+}
+
+.casebook-index-time > span i {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 10px rgba(159, 248, 80, 0.45);
+}
+
+.casebook-index-match {
+ justify-content: flex-start;
+ margin-top: 17px;
+}
+
+.casebook-pair {
+ position: relative;
+ width: 56px;
+ height: 36px;
+ flex: 0 0 56px;
+}
+
+.casebook-pair .command-crest {
+ position: absolute;
+ top: 0;
+ width: 36px;
+ height: 36px;
+ font-size: 7px;
+}
+
+.casebook-pair .command-crest:last-child {
+ left: 22px;
+ box-shadow: 0 0 0 3px var(--surface);
+}
+
+.casebook-index-match > span:last-child,
+.casebook-index-outcome > span:nth-child(2) {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-index-match b,
+.casebook-index-outcome b {
+ overflow: hidden;
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.casebook-index-match small,
+.casebook-index-outcome small,
+.casebook-index-foot small {
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.casebook-index-tags {
+ display: flex;
+ gap: 5px;
+ margin: 16px 0 12px;
+}
+
+.casebook-index-tags span {
+ padding: 4px 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 6px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.casebook-index-outcome {
+ justify-content: flex-start;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.casebook-index-outcome > span:first-child {
+ width: 28px;
+ height: 28px;
+ display: grid;
+ flex: 0 0 28px;
+ place-items: center;
+ border-radius: 7px;
+ background: rgba(145, 131, 229, 0.12);
+ color: var(--violet);
+}
+
+.casebook-index-outcome svg,
+.casebook-index-foot svg,
+.casebook-no-results svg {
+ width: 14px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.casebook-index-outcome > svg {
+ width: 15px;
+ margin-left: auto;
+ color: var(--muted);
+}
+
+.casebook-index-foot {
+ display: flex;
+ gap: 11px;
+ padding: 17px 18px;
+ color: var(--muted-strong);
+ background: var(--surface-soft);
+}
+
+.casebook-index-foot > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-index-foot b {
+ color: var(--text-soft);
+ font-size: 9px;
+}
+
+.casebook-no-results {
+ min-height: 260px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 30px;
+ text-align: center;
+}
+
+.casebook-no-results > span,
+.casebook-detail-empty > span {
+ width: 42px;
+ height: 42px;
+ display: grid;
+ margin-bottom: 14px;
+ place-items: center;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ color: var(--muted);
+}
+
+.casebook-no-results small {
+ max-width: 230px;
+ margin-top: 6px;
+ color: var(--muted);
+ font-size: 9px;
+ line-height: 1.55;
+}
+
+.casebook-detail {
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.casebook-detail-masthead {
+ position: relative;
+ min-height: 285px;
+ display: grid;
+ grid-template-columns: 130px minmax(240px, 0.85fr) minmax(260px, 1.15fr);
+ grid-template-rows: 1fr auto;
+ gap: 24px;
+ align-items: center;
+ padding: 30px 32px 26px;
+ overflow: hidden;
+ border-bottom: 1px solid var(--border);
+ background:
+ linear-gradient(90deg, transparent 49.8%, rgba(159, 248, 80, 0.045) 50%, transparent 50.2%) 0 0 / 84px 84px,
+ linear-gradient(transparent 49.8%, rgba(159, 248, 80, 0.045) 50%, transparent 50.2%) 0 0 / 84px 84px,
+ radial-gradient(circle at 56% 40%, rgba(145, 131, 229, 0.14), transparent 22rem),
+ #0b0f21;
+}
+
+.casebook-detail-masthead::after {
+ position: absolute;
+ right: -86px;
+ bottom: -150px;
+ width: 330px;
+ height: 330px;
+ border: 1px solid rgba(159, 248, 80, 0.09);
+ border-radius: 50%;
+ content: "";
+}
+
+.casebook-detail-id {
+ align-self: stretch;
+ display: flex;
+ flex-direction: column;
+ padding-right: 22px;
+ border-right: 1px solid var(--border);
+}
+
+.casebook-detail-id > span,
+.casebook-detail-id > small,
+.casebook-detail-verdict small,
+.casebook-market-verdict small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.casebook-detail-id > b {
+ margin: 9px 0 auto;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 17px;
+ font-weight: 500;
+ letter-spacing: -0.04em;
+}
+
+.casebook-detail-id > small {
+ line-height: 1.55;
+}
+
+.casebook-detail-match {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ gap: 18px;
+ align-items: center;
+}
+
+.casebook-detail-match > span {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 9px;
+ font-size: 11px;
+}
+
+.casebook-detail-match .command-crest {
+ width: 58px;
+ height: 58px;
+ font-size: 9px;
+}
+
+.casebook-detail-match > em {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: var(--text);
+ font-style: normal;
+}
+
+.casebook-detail-match > em small,
+.casebook-detail-match > em i {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 6px;
+ font-style: normal;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.casebook-detail-match > em strong {
+ margin: 3px 0;
+ font-size: 30px;
+ letter-spacing: -0.07em;
+}
+
+.casebook-detail-verdict {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ gap: 14px;
+}
+
+.casebook-detail-verdict > span:first-child {
+ width: 40px;
+ height: 40px;
+ display: grid;
+ flex: 0 0 40px;
+ place-items: center;
+ border: 1px solid rgba(159, 248, 80, 0.18);
+ border-radius: 11px;
+ background: rgba(159, 248, 80, 0.07);
+ color: var(--lime);
+}
+
+.casebook-detail-verdict svg {
+ width: 20px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.6;
+}
+
+.casebook-detail-verdict h2 {
+ margin: 7px 0 7px;
+ font-size: clamp(22px, 2.3vw, 35px);
+ letter-spacing: -0.055em;
+ line-height: 1.02;
+}
+
+.casebook-detail-verdict p {
+ max-width: 500px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.65;
+}
+
+.casebook-matchroom-link {
+ position: relative;
+ z-index: 1;
+ grid-column: 2 / 4;
+ width: fit-content;
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+ padding: 10px 13px;
+ border: 1px solid rgba(159, 248, 80, 0.22);
+ border-radius: 8px;
+ background: rgba(159, 248, 80, 0.08);
+ color: var(--text);
+ font-size: 9px;
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.casebook-matchroom-link svg {
+ width: 14px;
+ fill: none;
+ stroke: var(--lime);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.8;
+}
+
+.casebook-decision-rail,
+.casebook-readout,
+.casebook-evidence-sequence {
+ padding: 26px 30px;
+ border-bottom: 1px solid var(--border);
+}
+
+.casebook-final-state,
+.casebook-move-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 9px;
+ border: 1px solid rgba(145, 131, 229, 0.23);
+ border-radius: 6px;
+ background: rgba(145, 131, 229, 0.08);
+ color: var(--violet);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.casebook-final-state svg {
+ width: 12px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.casebook-rail-stages {
+ position: relative;
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ margin-top: 20px;
+}
+
+.casebook-rail-stages::before {
+ position: absolute;
+ top: 31px;
+ right: 9%;
+ left: 9%;
+ height: 1px;
+ background: linear-gradient(90deg, var(--lime), var(--violet));
+ content: "";
+}
+
+.casebook-rail-stage {
+ position: relative;
+ z-index: 1;
+ min-height: 112px;
+ display: grid;
+ grid-template-columns: 28px 1fr auto;
+ grid-template-rows: 34px 1fr;
+ gap: 8px 10px;
+ padding: 12px;
+ border: 1px solid var(--border);
+ border-radius: 9px;
+ background: var(--surface-soft);
+}
+
+.rail-index {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.casebook-rail-stage > i {
+ grid-column: 2;
+ grid-row: 1;
+ width: 25px;
+ height: 25px;
+ display: grid;
+ align-self: start;
+ place-items: center;
+ border-radius: 50%;
+ background: var(--lime);
+ color: #0c1420;
+ font-style: normal;
+}
+
+.casebook-rail-stage.locked > i {
+ background: var(--violet);
+}
+
+.casebook-rail-stage > i svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2.1;
+}
+
+.casebook-rail-stage > span:nth-of-type(2) {
+ grid-column: 1 / 4;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-rail-stage b {
+ font-size: 9px;
+}
+
+.casebook-rail-stage small {
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.4;
+}
+
+.casebook-rail-stage > em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ font-style: normal;
+}
+
+.casebook-move-label {
+ border-color: rgba(233, 185, 104, 0.25);
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.casebook-move-label i {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.casebook-readout-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ margin-top: 20px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+}
+
+.casebook-readout-grid > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 18px 14px;
+ border-right: 1px solid var(--border);
+ background: rgba(7, 10, 23, 0.35);
+}
+
+.casebook-readout-grid > div:last-child {
+ border-right: 0;
+}
+
+.casebook-readout-grid span,
+.casebook-readout-grid small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.casebook-readout-grid b {
+ margin: 13px 0 4px;
+ font-size: clamp(17px, 1.7vw, 25px);
+ letter-spacing: -0.05em;
+}
+
+.casebook-readout-grid .consensus-move {
+ background: rgba(100, 137, 246, 0.08);
+}
+
+.casebook-readout-grid .consensus-move b {
+ color: var(--blue);
+}
+
+.casebook-readout-grid .pre-move {
+ background: rgba(233, 185, 104, 0.08);
+}
+
+.casebook-readout-grid .pre-move b {
+ color: var(--amber);
+}
+
+.casebook-market-verdict {
+ display: grid;
+ grid-template-columns: 38px minmax(180px, auto) 1fr;
+ gap: 13px;
+ align-items: center;
+ margin-top: 10px;
+ padding: 12px 14px;
+ border: 1px solid rgba(233, 185, 104, 0.15);
+ border-radius: 9px;
+ background: var(--amber-soft);
+}
+
+.casebook-market-verdict > span:first-child {
+ width: 36px;
+ height: 36px;
+ display: grid;
+ place-items: center;
+ border-radius: 8px;
+ background: rgba(233, 185, 104, 0.12);
+ color: var(--amber);
+}
+
+.casebook-market-verdict svg {
+ width: 18px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.casebook-market-verdict > span:nth-child(2) {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-market-verdict b {
+ font-size: 10px;
+}
+
+.casebook-market-verdict p {
+ margin: 0;
+ padding-left: 16px;
+ border-left: 1px solid rgba(233, 185, 104, 0.2);
+ color: var(--muted-strong);
+ font-size: 9px;
+ line-height: 1.55;
+}
+
+.casebook-analysis-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ border-bottom: 1px solid var(--border);
+}
+
+.casebook-analysis-card {
+ display: flex;
+ gap: 14px;
+ padding: 26px 30px;
+}
+
+.casebook-analysis-card + .casebook-analysis-card {
+ border-left: 1px solid var(--border);
+}
+
+.analysis-icon {
+ width: 38px;
+ height: 38px;
+ display: grid;
+ flex: 0 0 38px;
+ place-items: center;
+ border-radius: 9px;
+ background: rgba(100, 137, 246, 0.1);
+ color: var(--blue);
+}
+
+.casebook-analysis-card.execution .analysis-icon {
+ background: rgba(159, 248, 80, 0.08);
+ color: var(--lime);
+}
+
+.analysis-icon svg {
+ width: 18px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.casebook-analysis-card > div > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.casebook-analysis-card h3 {
+ margin: 5px 0 7px;
+ font-size: 15px;
+}
+
+.casebook-analysis-card p {
+ margin: 0 0 10px;
+ color: var(--muted);
+ font-size: 9px;
+ line-height: 1.6;
+}
+
+.casebook-analysis-card small {
+ color: var(--muted-strong);
+ font-size: 8px;
+ line-height: 1.55;
+}
+
+.casebook-evidence-head,
+.casebook-evidence-row {
+ display: grid;
+ grid-template-columns: 110px 100px minmax(220px, 1fr) 80px 90px;
+ gap: 12px;
+ align-items: center;
+}
+
+.casebook-evidence-head {
+ margin-top: 20px;
+ padding: 0 13px 9px;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.casebook-evidence-row {
+ min-height: 64px;
+ padding: 10px 13px;
+ border-top: 1px solid var(--border);
+ font-size: 9px;
+}
+
+.casebook-evidence-row:nth-child(even) {
+ background: rgba(7, 10, 23, 0.27);
+}
+
+.casebook-evidence-row > span {
+ min-width: 0;
+}
+
+.casebook-evidence-row > span:first-child {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-evidence-row small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.casebook-evidence-row > span:nth-child(2) {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+
+.casebook-evidence-row > span:nth-child(2) i {
+ width: 6px;
+ height: 6px;
+ flex: 0 0 6px;
+ border-radius: 50%;
+ background: var(--blue);
+}
+
+.casebook-evidence-row > span:nth-child(2) i.txline {
+ background: var(--lime);
+}
+
+.casebook-evidence-row > span:nth-child(2) i.samaritan {
+ background: var(--violet);
+}
+
+.casebook-evidence-row > span:nth-child(3) {
+ color: var(--text-soft);
+ line-height: 1.45;
+}
+
+.casebook-evidence-row > span:last-child {
+ width: fit-content;
+ padding: 5px 7px;
+ border-radius: 5px;
+ background: rgba(233, 185, 104, 0.1);
+ color: var(--amber);
+ font-size: 8px;
+ font-weight: 700;
+}
+
+.casebook-evidence-row > span:last-child.no-trade {
+ background: rgba(145, 131, 229, 0.1);
+ color: var(--violet);
+}
+
+.casebook-proof-line {
+ display: grid;
+ grid-template-columns: minmax(190px, 1.1fr) 1fr 1fr auto;
+ gap: 20px;
+ align-items: center;
+ padding: 20px 30px;
+ background: #090d1d;
+}
+
+.casebook-proof-line > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.casebook-proof-status {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 10px !important;
+}
+
+.casebook-proof-status svg,
+.casebook-policy svg {
+ width: 20px;
+ flex: 0 0 20px;
+ fill: none;
+ stroke: var(--lime);
+ stroke-width: 1.6;
+}
+
+.casebook-proof-status > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.casebook-proof-line b {
+ font-size: 9px;
+}
+
+.casebook-proof-line small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.casebook-proof-line code {
+ overflow: hidden;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.casebook-policy {
+ flex-direction: row !important;
+ align-items: center;
+ padding: 7px 9px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--muted-strong);
+ font-size: 7px;
+ white-space: nowrap;
+}
+
+.casebook-policy svg {
+ width: 13px;
+ flex-basis: 13px;
+ stroke: var(--muted);
+}
+
+.casebook-detail-empty {
+ min-height: 520px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ border-radius: 14px;
+ text-align: center;
+}
+
+.casebook-detail-empty svg {
+ width: 18px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.5;
+}
+
+.casebook-detail-empty h2 {
+ margin: 0 0 8px;
+ font-size: 23px;
+ letter-spacing: -0.045em;
+}
+
+.casebook-detail-empty p {
+ max-width: 430px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.65;
+}
+
+.casebook-detail-empty button {
+ margin-top: 20px;
+ padding: 10px 14px;
+ border: 0;
+ border-radius: 7px;
+ background: var(--lime);
+ color: #081018;
+ font-size: 9px;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+@media (max-width: 1260px) {
+ .casebook-hero {
+ grid-template-columns: minmax(330px, 0.75fr) minmax(500px, 1.25fr);
+ }
+
+ .casebook-stat-deck > div {
+ padding: 19px 14px;
+ }
+
+ .casebook-filters {
+ grid-template-columns: minmax(210px, 1fr) minmax(480px, 3fr) auto;
+ }
+
+ .casebook-filter-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+
+ .casebook-filter-select {
+ height: 43px;
+ }
+
+ .casebook-detail-masthead {
+ grid-template-columns: 118px minmax(250px, 0.8fr) minmax(300px, 1.2fr);
+ padding-right: 26px;
+ padding-left: 26px;
+ }
+
+ .casebook-readout-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .casebook-readout-grid > div:nth-child(3) {
+ border-right: 0;
+ }
+
+ .casebook-readout-grid > div:nth-child(-n + 3) {
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+@media (max-width: 1060px) {
+ .casebook-content {
+ padding-right: 20px;
+ padding-left: 20px;
+ }
+
+ .casebook-hero {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-hero-copy {
+ min-height: 230px;
+ padding-right: 38px;
+ }
+
+ .casebook-stat-deck {
+ min-height: 188px;
+ margin: 0;
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .casebook-filters {
+ grid-template-columns: 1fr auto;
+ }
+
+ .casebook-filter-grid {
+ grid-column: 1 / 3;
+ grid-row: 2;
+ }
+
+ .casebook-clear {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .casebook-workbench {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-index {
+ position: static;
+ }
+
+ .casebook-index-row {
+ display: grid;
+ grid-template-columns: minmax(300px, 1fr) 1fr;
+ column-gap: 30px;
+ }
+
+ .casebook-index-time,
+ .casebook-index-match {
+ grid-column: 1;
+ }
+
+ .casebook-index-tags,
+ .casebook-index-outcome {
+ grid-column: 2;
+ }
+
+ .casebook-index-tags {
+ align-self: end;
+ margin-top: 10px;
+ }
+
+ .casebook-index-outcome {
+ align-self: start;
+ }
+}
+
+@media (max-width: 760px) {
+ .casebook-content {
+ padding: 12px 12px 104px;
+ }
+
+ .casebook-hero {
+ border-radius: 12px 12px 3px 3px;
+ }
+
+ .casebook-hero-copy {
+ min-height: 265px;
+ padding: 30px 24px 28px;
+ }
+
+ .casebook-hero h2 {
+ font-size: clamp(32px, 10vw, 44px);
+ }
+
+ .casebook-stat-deck {
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: repeat(2, 112px) 62px;
+ }
+
+ .casebook-stat-deck > div:nth-child(even) {
+ border-right: 0;
+ }
+
+ .casebook-verified-stat {
+ grid-row: 3;
+ }
+
+ .casebook-filters {
+ grid-template-columns: 1fr auto;
+ }
+
+ .casebook-search {
+ min-width: 0;
+ }
+
+ .casebook-search kbd {
+ display: none;
+ }
+
+ .casebook-filter-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .casebook-index-row {
+ display: block;
+ }
+
+ .casebook-index-tags {
+ margin-top: 16px;
+ }
+
+ .casebook-detail-masthead {
+ min-height: 0;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ gap: 25px;
+ padding: 25px 20px;
+ }
+
+ .casebook-detail-id {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 4px 12px;
+ padding: 0 0 14px;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .casebook-detail-id > b {
+ grid-row: 1 / 3;
+ grid-column: 2;
+ margin: 0;
+ }
+
+ .casebook-detail-match {
+ grid-column: 1;
+ }
+
+ .casebook-detail-verdict {
+ grid-column: 1;
+ }
+
+ .casebook-detail-verdict h2 {
+ font-size: 27px;
+ }
+
+ .casebook-matchroom-link {
+ grid-column: 1;
+ width: 100%;
+ justify-content: center;
+ }
+
+ .casebook-decision-rail,
+ .casebook-readout,
+ .casebook-evidence-sequence {
+ padding: 22px 18px;
+ }
+
+ .casebook-rail-stages {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .casebook-rail-stages::before {
+ display: none;
+ }
+
+ .casebook-readout-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .casebook-readout-grid > div {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .casebook-readout-grid > div:nth-child(odd) {
+ border-right: 1px solid var(--border);
+ }
+
+ .casebook-readout-grid > div:nth-child(even) {
+ border-right: 0;
+ }
+
+ .casebook-readout-grid > div:nth-last-child(-n + 2) {
+ border-bottom: 0;
+ }
+
+ .casebook-market-verdict {
+ grid-template-columns: 36px 1fr;
+ }
+
+ .casebook-market-verdict p {
+ grid-column: 1 / 3;
+ padding: 11px 0 0;
+ border-top: 1px solid rgba(233, 185, 104, 0.2);
+ border-left: 0;
+ }
+
+ .casebook-analysis-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-analysis-card {
+ padding: 22px 18px;
+ }
+
+ .casebook-analysis-card + .casebook-analysis-card {
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .casebook-evidence-head {
+ display: none;
+ }
+
+ .casebook-evidence-row {
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-top: 9px;
+ padding: 13px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ }
+
+ .casebook-evidence-row > span:nth-child(3) {
+ grid-column: 1 / 3;
+ grid-row: 2;
+ padding: 9px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ }
+
+ .casebook-evidence-row > span:nth-child(4) {
+ grid-column: 1;
+ }
+
+ .casebook-evidence-row > span:nth-child(5) {
+ grid-column: 2;
+ justify-self: end;
+ }
+
+ .casebook-proof-line {
+ grid-template-columns: 1fr 1fr;
+ padding: 20px 18px;
+ }
+
+ .casebook-proof-status {
+ grid-column: 1 / 3;
+ }
+}
+
+@media (max-width: 420px) {
+ .casebook-hero-copy {
+ min-height: 290px;
+ }
+
+ .casebook-stat-deck {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .casebook-stat-deck > div {
+ padding: 17px 14px;
+ }
+
+ .casebook-stat-deck > div > b {
+ font-size: 22px;
+ }
+
+ .casebook-stat-deck .casebook-primary-stat b {
+ font-size: 39px;
+ }
+
+ .casebook-filters {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-search,
+ .casebook-filter-grid,
+ .casebook-clear {
+ grid-column: 1;
+ }
+
+ .casebook-search {
+ grid-row: 1;
+ }
+
+ .casebook-filter-grid {
+ grid-row: 2;
+ }
+
+ .casebook-clear {
+ grid-row: 3;
+ }
+
+ .casebook-detail-match {
+ gap: 8px;
+ }
+
+ .casebook-detail-match .command-crest {
+ width: 49px;
+ height: 49px;
+ }
+
+ .casebook-detail-verdict > span:first-child {
+ display: none;
+ }
+
+ .casebook-section-head {
+ align-items: flex-start;
+ }
+
+ .casebook-rail-stages {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-readout-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-readout-grid > div,
+ .casebook-readout-grid > div:nth-child(odd),
+ .casebook-readout-grid > div:nth-last-child(-n + 2) {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .casebook-readout-grid > div:last-child {
+ border-bottom: 0;
+ }
+
+ .casebook-analysis-card {
+ flex-direction: column;
+ }
+
+ .casebook-proof-line {
+ grid-template-columns: 1fr;
+ }
+
+ .casebook-proof-status {
+ grid-column: 1;
+ }
+
+ .casebook-policy {
+ width: fit-content;
+ }
+}
+
+/* Study: invalidated protocol audit and separated evidence classes */
+.study-shell {
+ background:
+ radial-gradient(circle at 72% 3%, rgba(159, 248, 80, 0.065), transparent 29rem),
+ radial-gradient(circle at 93% 58%, rgba(100, 137, 246, 0.055), transparent 32rem),
+ var(--bg);
+}
+
+.study-content {
+ width: min(1480px, 100%);
+ margin: 0 auto;
+ padding: 26px 28px 76px;
+}
+
+.study-hero {
+ position: relative;
+ min-height: 340px;
+ display: grid;
+ grid-template-columns: minmax(390px, 0.84fr) minmax(580px, 1.16fr);
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 18px 18px 4px 4px;
+ background:
+ linear-gradient(90deg, rgba(159, 248, 80, 0.035) 1px, transparent 1px) 0 0 / 56px 56px,
+ linear-gradient(rgba(159, 248, 80, 0.035) 1px, transparent 1px) 0 0 / 56px 56px,
+ radial-gradient(circle at 22% 24%, rgba(100, 137, 246, 0.11), transparent 25rem),
+ #0b0f21;
+ box-shadow: var(--shadow);
+}
+
+.study-hero::before {
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 3px;
+ background: var(--lime);
+ content: "";
+}
+
+.study-hero::after {
+ position: absolute;
+ top: -190px;
+ left: 25%;
+ width: 420px;
+ height: 420px;
+ border: 1px solid rgba(159, 248, 80, 0.09);
+ border-radius: 50%;
+ content: "";
+}
+
+.study-hero-copy {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 40px 42px;
+}
+
+.study-kicker {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 9px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.study-kicker i {
+ width: 22px;
+ height: 1px;
+ background: currentColor;
+}
+
+.study-hero h2 {
+ margin: 17px 0 16px;
+ font-size: clamp(42px, 4.4vw, 70px);
+ letter-spacing: -0.075em;
+ line-height: 0.91;
+}
+
+.study-hero h2 em {
+ color: var(--muted-strong);
+ font-style: normal;
+ font-weight: 500;
+}
+
+.study-hero-copy > p {
+ max-width: 560px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+ line-height: 1.72;
+}
+
+.study-primary-question {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 27px;
+ padding-top: 21px;
+ border-top: 1px solid var(--border);
+}
+
+.study-primary-question > span:first-child {
+ width: 38px;
+ height: 38px;
+ display: grid;
+ flex: 0 0 38px;
+ place-items: center;
+ border-radius: 9px;
+ background: rgba(159, 248, 80, 0.08);
+ color: var(--lime);
+}
+
+.study-primary-question svg,
+.lane-mark svg,
+.candidate-lead svg,
+.candidate-selector svg,
+.risk-grid svg {
+ width: 18px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.study-primary-question > span:last-child {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.study-primary-question small,
+.study-clock small,
+.study-lane small,
+.candidate-lead small,
+.candidate-selector small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.study-primary-question b {
+ font-size: 10px;
+}
+
+.study-clock {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-rows: auto 1fr auto auto;
+ border-left: 1px solid var(--border);
+ background: rgba(4, 7, 17, 0.3);
+}
+
+.study-clock > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 17px 23px;
+ border-bottom: 1px solid var(--border);
+}
+
+.study-clock > header > span {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ color: var(--lime);
+ font-size: 9px;
+ font-weight: 800;
+}
+
+.study-clock > header > span i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--lime);
+ box-shadow: 0 0 12px rgba(159, 248, 80, 0.5);
+}
+
+.study-clock > header em {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ font-style: normal;
+}
+
+.study-clock-main {
+ display: grid;
+ grid-template-columns: 74px 1fr 1fr;
+ gap: 22px;
+ align-items: center;
+ padding: 24px 25px 18px;
+}
+
+.study-clock-lock {
+ width: 64px;
+ height: 64px;
+ display: grid;
+ place-items: center;
+ border: 1px solid rgba(159, 248, 80, 0.18);
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(159, 248, 80, 0.12), rgba(159, 248, 80, 0.025));
+ color: var(--lime);
+ box-shadow: 0 0 50px rgba(159, 248, 80, 0.06);
+}
+
+.study-clock-lock svg {
+ width: 25px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.4;
+}
+
+.study-clock-main > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.study-clock-main strong {
+ display: flex;
+ align-items: baseline;
+ gap: 7px;
+ margin: 7px 0 2px;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: clamp(28px, 3.15vw, 50px);
+ font-weight: 400;
+ letter-spacing: -0.08em;
+ line-height: 1;
+}
+
+.study-clock-main strong b {
+ color: var(--lime);
+ font-weight: 500;
+}
+
+.study-clock-main strong em {
+ color: var(--border-strong);
+ font-size: 0.55em;
+ font-style: normal;
+}
+
+.study-clock-main > div > span {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-clock-progress {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 22px;
+ padding: 0 25px 22px;
+}
+
+.study-progress-meter > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.study-progress-meter > div > span {
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 8px;
+}
+
+.study-progress-meter > div > span b {
+ color: var(--text);
+}
+
+.study-progress-meter > div small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 7px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.study-progress-meter > i {
+ height: 3px;
+ display: block;
+ margin-top: 8px;
+ overflow: hidden;
+ border-radius: 3px;
+ background: rgba(157, 169, 218, 0.13);
+}
+
+.study-progress-meter > i em {
+ height: 100%;
+ display: block;
+ background: var(--lime);
+ box-shadow: 0 0 9px rgba(159, 248, 80, 0.5);
+}
+
+.study-clock > footer {
+ display: grid;
+ grid-template-columns: 1fr 1.2fr auto;
+ gap: 15px;
+ align-items: center;
+ padding: 14px 23px;
+ border-top: 1px solid var(--border);
+ background: rgba(7, 10, 24, 0.65);
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-clock > footer b {
+ color: var(--text-soft);
+}
+
+.study-clock > footer > span:last-child {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--lime);
+ font-weight: 800;
+}
+
+.study-clock > footer svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.study-lanes {
+ display: grid;
+ grid-template-columns: 1fr 1fr minmax(260px, 0.76fr);
+ margin-top: 12px;
+ overflow: hidden;
+ border-radius: 4px 4px 12px 12px;
+}
+
+.study-lane,
+.lane-boundary {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 17px 19px;
+ border-right: 1px solid var(--border);
+}
+
+.study-lane.long-run {
+ background: linear-gradient(90deg, rgba(159, 248, 80, 0.08), transparent);
+}
+
+.lane-mark {
+ width: 36px;
+ height: 36px;
+ display: grid;
+ flex: 0 0 36px;
+ place-items: center;
+ border-radius: 9px;
+ background: rgba(159, 248, 80, 0.09);
+ color: var(--lime);
+}
+
+.study-lane.bounty .lane-mark {
+ background: rgba(100, 137, 246, 0.1);
+ color: var(--blue);
+}
+
+.study-lane > span:nth-child(2),
+.lane-boundary > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.study-lane b,
+.lane-boundary b {
+ font-size: 9px;
+}
+
+.study-lane em,
+.lane-boundary small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 7px;
+ font-style: normal;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.study-lane > strong {
+ margin-left: auto;
+ padding: 5px 7px;
+ border: 1px solid rgba(159, 248, 80, 0.18);
+ border-radius: 5px;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.study-lane.bounty > strong {
+ border-color: rgba(100, 137, 246, 0.2);
+ color: var(--blue);
+}
+
+.lane-boundary {
+ border-right: 0;
+ background: var(--surface-soft);
+}
+
+.lane-boundary > svg {
+ width: 19px;
+ flex: 0 0 19px;
+ fill: none;
+ stroke: var(--violet);
+ stroke-width: 1.6;
+}
+
+.study-corrected-evidence {
+ margin-top: 12px;
+ overflow: hidden;
+ border-color: rgba(100, 137, 246, 0.2);
+ border-radius: 14px;
+ background: linear-gradient(125deg, rgba(100, 137, 246, 0.07), rgba(9, 13, 29, 0.95) 45%);
+}
+
+.study-unregistered-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ border: 1px solid rgba(100, 137, 246, 0.24);
+ border-radius: 6px;
+ background: rgba(100, 137, 246, 0.08);
+ color: var(--blue);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.study-unregistered-chip svg,
+.corrected-evidence-boundary svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.corrected-evidence-grid {
+ display: grid;
+ grid-template-columns: 1.35fr repeat(3, 1fr);
+}
+
+.corrected-evidence-grid > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 22px;
+ border-right: 1px solid var(--border);
+}
+
+.corrected-evidence-grid > div:last-child {
+ border-right: 0;
+}
+
+.corrected-evidence-grid small,
+.corrected-evidence-grid em {
+ color: var(--muted);
+ font-size: 7px;
+ font-style: normal;
+ line-height: 1.45;
+}
+
+.corrected-evidence-grid b {
+ color: var(--text-soft);
+ font-size: 18px;
+ letter-spacing: -0.04em;
+}
+
+.corrected-evidence-primary b {
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 29px;
+}
+
+.corrected-evidence-boundary {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ padding: 14px 22px;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ background: rgba(145, 131, 229, 0.06);
+ color: var(--violet);
+}
+
+.corrected-evidence-boundary > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.corrected-evidence-boundary b {
+ color: var(--text-soft);
+ font-size: 9px;
+}
+
+.corrected-evidence-boundary small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.study-corrected-evidence > footer {
+ display: flex;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 12px 22px;
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.study-corrected-evidence > footer code {
+ margin-left: 6px;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+}
+
+.study-synthetic-proof {
+ margin-top: 12px;
+ overflow: hidden;
+ border-color: rgba(159, 248, 80, 0.18);
+ border-radius: 14px;
+ background: linear-gradient(120deg, rgba(159, 248, 80, 0.055), rgba(9, 13, 29, 0.97) 48%);
+}
+
+.synthetic-verified-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ border: 1px solid rgba(159, 248, 80, 0.24);
+ border-radius: 6px;
+ background: rgba(159, 248, 80, 0.07);
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.synthetic-verified-chip svg,
+.synthetic-proof-mark svg,
+.study-synthetic-proof > footer svg {
+ width: 14px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.synthetic-proof-body {
+ display: grid;
+ grid-template-columns: minmax(300px, 1.25fr) minmax(420px, 1fr);
+}
+
+.synthetic-proof-story {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 22px;
+ border-right: 1px solid var(--border);
+}
+
+.synthetic-proof-mark {
+ width: 42px;
+ height: 42px;
+ flex: 0 0 42px;
+ display: grid;
+ place-items: center;
+ border: 1px solid rgba(159, 248, 80, 0.22);
+ border-radius: 11px;
+ background: rgba(159, 248, 80, 0.07);
+ color: var(--lime);
+}
+
+.synthetic-proof-mark svg {
+ width: 20px;
+}
+
+.synthetic-proof-story > div {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.synthetic-proof-story small,
+.synthetic-proof-facts small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.synthetic-proof-story b {
+ color: var(--text-soft);
+ font-size: 12px;
+}
+
+.synthetic-proof-story p {
+ max-width: 580px;
+ margin: 0;
+ color: var(--muted-strong);
+ font-size: 8px;
+ line-height: 1.55;
+}
+
+.synthetic-proof-facts {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+}
+
+.synthetic-proof-facts > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 5px;
+ padding: 18px 14px;
+ border-right: 1px solid var(--border);
+}
+
+.synthetic-proof-facts > span:last-child {
+ border-right: 0;
+}
+
+.synthetic-proof-facts b {
+ color: var(--text-soft);
+ font-size: 9px;
+}
+
+.synthetic-proof-flow {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ padding: 13px 22px;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ background: rgba(159, 248, 80, 0.025);
+}
+
+.synthetic-proof-flow > span {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--muted-strong);
+}
+
+.synthetic-proof-flow b {
+ font-family: var(--mono);
+ font-size: 7px;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+}
+
+.synthetic-proof-flow i {
+ display: grid;
+ place-items: center;
+ color: rgba(159, 248, 80, 0.48);
+}
+
+.synthetic-proof-flow svg {
+ width: 12px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.study-synthetic-proof > footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 12px 22px;
+ color: var(--muted-strong);
+ font-size: 7px;
+}
+
+.study-synthetic-proof > footer > span,
+.study-synthetic-proof > footer a {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+
+.study-synthetic-proof > footer > span svg {
+ color: var(--muted);
+}
+
+.study-synthetic-proof > footer a {
+ color: var(--lime);
+ font-weight: 800;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.study-synthetic-proof > footer a svg {
+ width: 12px;
+}
+
+.study-primary-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.9fr) minmax(285px, 0.72fr);
+ gap: 12px;
+ align-items: stretch;
+ margin-top: 12px;
+}
+
+.study-endpoint,
+.study-guardrails,
+.study-candidate,
+.study-risk,
+.study-queue,
+.study-rules,
+.study-rows {
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.study-panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 21px 23px 17px;
+ border-bottom: 1px solid var(--border);
+}
+
+.study-panel-head > div > span {
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.11em;
+ text-transform: uppercase;
+}
+
+.study-panel-head h2 {
+ margin: 4px 0 0;
+ font-size: 16px;
+ letter-spacing: -0.04em;
+}
+
+.study-sealed-chip,
+.study-result-chip,
+.config-frozen,
+.paper-only,
+.rules-note {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ border: 1px solid rgba(145, 131, 229, 0.25);
+ border-radius: 6px;
+ background: rgba(145, 131, 229, 0.08);
+ color: var(--violet);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.study-sealed-chip svg {
+ width: 12px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.sealed-endpoint-stage {
+ position: relative;
+ min-height: 310px;
+ padding: 32px 34px 25px;
+ background:
+ linear-gradient(90deg, rgba(157, 169, 218, 0.035) 1px, transparent 1px) 0 0 / 56px 56px,
+ linear-gradient(rgba(157, 169, 218, 0.035) 1px, transparent 1px) 0 0 / 56px 56px,
+ var(--surface-soft);
+}
+
+.endpoint-axis-labels {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.endpoint-axis-labels span:nth-child(2) {
+ color: var(--text-soft);
+}
+
+.endpoint-axis-labels span:last-child {
+ text-align: right;
+}
+
+.endpoint-axis {
+ position: relative;
+ height: 18px;
+ margin: 10px 0 27px;
+}
+
+.endpoint-axis > i {
+ position: absolute;
+ top: 8px;
+ right: 0;
+ left: 0;
+ height: 2px;
+ border-radius: 2px;
+ background: linear-gradient(90deg, rgba(241, 110, 122, 0.55), rgba(157, 169, 218, 0.25) 50%, rgba(159, 248, 80, 0.7));
+}
+
+.endpoint-axis > em {
+ position: absolute;
+ top: 1px;
+ left: 50%;
+ width: 1px;
+ height: 16px;
+ background: var(--text-soft);
+}
+
+.endpoint-axis > span {
+ position: absolute;
+ top: 4px;
+ left: calc(50% - 4px);
+ width: 9px;
+ height: 9px;
+ border: 2px solid var(--surface-soft);
+ border-radius: 50%;
+ background: var(--text-soft);
+}
+
+.endpoint-vault {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ padding: 20px;
+ border: 1px solid rgba(145, 131, 229, 0.22);
+ border-radius: 11px;
+ background: linear-gradient(110deg, rgba(145, 131, 229, 0.1), rgba(145, 131, 229, 0.025));
+}
+
+.endpoint-vault > span {
+ width: 48px;
+ height: 48px;
+ display: grid;
+ flex: 0 0 48px;
+ place-items: center;
+ border-radius: 50%;
+ background: rgba(145, 131, 229, 0.13);
+ color: var(--violet);
+}
+
+.endpoint-vault svg {
+ width: 20px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.6;
+}
+
+.endpoint-vault > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.endpoint-vault small {
+ color: var(--violet);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.endpoint-vault b {
+ font-size: 13px;
+}
+
+.endpoint-vault p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.endpoint-method {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1.4fr;
+ gap: 20px;
+ margin-top: 21px;
+}
+
+.endpoint-method > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.endpoint-method small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.endpoint-method b {
+ overflow: hidden;
+ color: var(--text-soft);
+ font-size: 8px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.study-accept-criteria {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ border-top: 1px solid var(--border);
+}
+
+.study-accept-criteria > div {
+ min-height: 101px;
+ display: grid;
+ grid-template-columns: 20px 27px 1fr;
+ gap: 9px;
+ align-items: start;
+ padding: 17px;
+ border-right: 1px solid var(--border);
+}
+
+.study-accept-criteria > div:last-child {
+ border-right: 0;
+}
+
+.study-accept-criteria > div > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.study-accept-criteria > div > i {
+ width: 24px;
+ height: 24px;
+ display: grid;
+ place-items: center;
+ border-radius: 50%;
+ background: rgba(145, 131, 229, 0.1);
+ color: var(--violet);
+}
+
+.study-accept-criteria svg {
+ width: 11px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.study-accept-criteria p {
+ margin: 0;
+ color: var(--muted-strong);
+ font-size: 8px;
+ line-height: 1.48;
+}
+
+.study-guardrails {
+ display: flex;
+ flex-direction: column;
+}
+
+.study-guardrails .study-panel-head > svg {
+ width: 20px;
+ fill: none;
+ stroke: var(--lime);
+ stroke-width: 1.6;
+}
+
+.study-guardrails > p {
+ margin: 0;
+ padding: 15px 20px;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.55;
+}
+
+.study-guardrail-list {
+ padding: 5px 20px;
+}
+
+.study-guardrail-list > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 13px;
+ padding: 14px 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.study-guardrail-list > div > span {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.study-guardrail-list > div > span > i {
+ width: 27px;
+ height: 27px;
+ display: grid;
+ flex: 0 0 27px;
+ place-items: center;
+ border-radius: 7px;
+ background: rgba(145, 131, 229, 0.09);
+ color: var(--violet);
+}
+
+.study-guardrail-list > div.pass > span > i {
+ background: rgba(159, 248, 80, 0.09);
+ color: var(--lime);
+}
+
+.study-guardrail-list > div.fail > span > i {
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+
+.study-guardrail-list svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.study-guardrail-list > div > span > span {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.study-guardrail-list b {
+ font-size: 9px;
+}
+
+.study-guardrail-list small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.study-guardrail-list em {
+ color: var(--text-soft);
+ font-family: var(--mono);
+ font-size: 8px;
+ font-style: normal;
+ white-space: nowrap;
+}
+
+.study-guardrails > footer,
+.study-risk > footer {
+ display: flex;
+ gap: 10px;
+ margin-top: auto;
+ padding: 16px 20px;
+ border-top: 1px solid var(--border);
+ background: var(--surface-soft);
+}
+
+.study-guardrails > footer svg,
+.study-risk > footer svg {
+ width: 17px;
+ flex: 0 0 17px;
+ fill: none;
+ stroke: var(--violet);
+ stroke-width: 1.6;
+}
+
+.study-guardrails > footer span,
+.study-risk > footer span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.study-guardrails > footer b,
+.study-risk > footer b {
+ font-size: 8px;
+}
+
+.study-guardrails > footer small,
+.study-risk > footer small {
+ color: var(--muted);
+ font-size: 7px;
+ line-height: 1.45;
+}
+
+.study-secondary-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.16fr) minmax(260px, 0.8fr) minmax(280px, 1fr);
+ gap: 12px;
+ align-items: stretch;
+ margin-top: 12px;
+}
+
+.config-frozen {
+ border-color: rgba(159, 248, 80, 0.2);
+ background: rgba(159, 248, 80, 0.07);
+ color: var(--lime);
+}
+
+.config-frozen i {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.candidate-lead,
+.candidate-selector {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 18px 22px;
+ border-bottom: 1px solid var(--border);
+}
+
+.candidate-lead > span:first-child,
+.candidate-selector > span:first-child {
+ width: 37px;
+ height: 37px;
+ display: grid;
+ flex: 0 0 37px;
+ place-items: center;
+ border-radius: 9px;
+ background: rgba(159, 248, 80, 0.09);
+ color: var(--lime);
+}
+
+.candidate-selector > span:first-child {
+ background: rgba(100, 137, 246, 0.09);
+ color: var(--blue);
+}
+
+.candidate-lead > span:last-child,
+.candidate-selector > span:last-child {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.candidate-lead b,
+.candidate-selector b {
+ font-size: 10px;
+}
+
+.candidate-lead em,
+.candidate-selector em {
+ color: var(--muted);
+ font-size: 7px;
+ font-style: normal;
+}
+
+.candidate-parameters {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ border-bottom: 1px solid var(--border);
+}
+
+.candidate-parameters > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ padding: 17px 12px;
+ border-right: 1px solid var(--border);
+}
+
+.candidate-parameters > span:last-child {
+ border-right: 0;
+}
+
+.candidate-parameters small,
+.candidate-hash > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.candidate-parameters b {
+ overflow: hidden;
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.candidate-hash {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 13px 18px;
+ background: var(--surface-soft);
+}
+
+.candidate-hash code {
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.paper-only {
+ border-color: rgba(100, 137, 246, 0.22);
+ background: rgba(100, 137, 246, 0.07);
+ color: var(--blue);
+}
+
+.study-risk {
+ display: flex;
+ flex-direction: column;
+}
+
+.risk-orbit {
+ position: relative;
+ min-height: 158px;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border-bottom: 1px solid var(--border);
+ background: radial-gradient(circle, rgba(100, 137, 246, 0.1), transparent 60%);
+}
+
+.risk-bankroll {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.risk-bankroll span,
+.risk-bankroll small,
+.risk-grid small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.risk-bankroll b {
+ margin: 4px 0;
+ font-size: 32px;
+ letter-spacing: -0.06em;
+}
+
+.risk-ring {
+ position: absolute;
+ border: 1px solid rgba(100, 137, 246, 0.12);
+ border-radius: 50%;
+}
+
+.risk-ring.one {
+ width: 128px;
+ height: 128px;
+}
+
+.risk-ring.two {
+ width: 205px;
+ height: 205px;
+}
+
+.risk-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ border-bottom: 1px solid var(--border);
+}
+
+.risk-grid > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 14px 11px;
+ border-right: 1px solid var(--border);
+}
+
+.risk-grid > span:last-child {
+ border-right: 0;
+}
+
+.risk-grid b {
+ font-size: 11px;
+}
+
+.study-queue .study-panel-head > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+}
+
+.queue-flow {
+ display: grid;
+ grid-template-columns: 1fr 18px 1fr 18px 1fr;
+ gap: 8px;
+ align-items: center;
+ padding: 25px 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.queue-flow > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ text-align: center;
+}
+
+.queue-flow b {
+ font-family: var(--mono);
+ font-size: 25px;
+ font-weight: 500;
+}
+
+.queue-flow span {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.queue-flow .eligible b {
+ color: var(--lime);
+}
+
+.queue-flow > i {
+ color: var(--muted);
+}
+
+.queue-flow svg,
+.study-queue > a svg {
+ width: 14px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.queue-rejection {
+ display: flex;
+ gap: 11px;
+ padding: 17px 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.queue-rejection > span:first-child {
+ width: 29px;
+ height: 29px;
+ display: grid;
+ flex: 0 0 29px;
+ place-items: center;
+ border-radius: 7px;
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.queue-rejection svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.queue-rejection > span:last-child {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.queue-rejection b {
+ font-size: 8px;
+}
+
+.queue-rejection small {
+ color: var(--muted);
+ font-size: 7px;
+ line-height: 1.5;
+}
+
+.study-queue > a {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 20px;
+ color: var(--text-soft);
+ font-size: 8px;
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.study-queue > a svg {
+ color: var(--lime);
+}
+
+.study-rules {
+ margin-top: 12px;
+}
+
+.rules-note {
+ border-color: var(--border);
+ background: var(--surface-soft);
+ color: var(--muted);
+}
+
+.study-rule-grid {
+ display: grid;
+ grid-template-columns: 1.25fr 0.9fr 0.9fr;
+}
+
+.study-rule-grid > div {
+ min-height: 188px;
+ padding: 21px 23px;
+ border-right: 1px solid var(--border);
+}
+
+.study-rule-grid > div:last-child {
+ border-right: 0;
+}
+
+.study-rule-grid header {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ margin-bottom: 15px;
+}
+
+.study-rule-grid header > span {
+ width: 28px;
+ height: 28px;
+ display: grid;
+ place-items: center;
+ border-radius: 7px;
+}
+
+.study-rule-grid .accept header > span {
+ background: rgba(159, 248, 80, 0.09);
+ color: var(--lime);
+}
+
+.study-rule-grid .reject header > span {
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+
+.study-rule-grid .inconclusive header > span {
+ background: var(--amber-soft);
+ color: var(--amber);
+}
+
+.study-rule-grid svg {
+ width: 14px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.study-rule-grid header b {
+ font-size: 11px;
+}
+
+.study-rule-grid p {
+ position: relative;
+ margin: 0;
+ padding: 8px 0 8px 15px;
+ border-top: 1px solid var(--border);
+ color: var(--muted-strong);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.study-rule-grid p::before {
+ position: absolute;
+ top: 13px;
+ left: 1px;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: var(--muted);
+ content: "";
+}
+
+.study-rules > footer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 15px 23px;
+ border-top: 1px solid rgba(145, 131, 229, 0.16);
+ background: rgba(145, 131, 229, 0.07);
+ color: var(--text-soft);
+}
+
+.study-rules > footer svg {
+ width: 17px;
+ flex: 0 0 17px;
+ fill: none;
+ stroke: var(--violet);
+ stroke-width: 1.7;
+}
+
+.study-rules > footer b {
+ font-size: 8px;
+}
+
+.study-proof {
+ display: grid;
+ grid-template-columns: minmax(205px, 1.2fr) 1fr 1fr 1fr auto;
+ gap: 21px;
+ align-items: center;
+ margin-top: 12px;
+ padding: 19px 23px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: #090d1d;
+}
+
+.study-proof > span {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.study-proof-status {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 10px !important;
+}
+
+.study-proof-status > svg,
+.study-proof-boundary svg {
+ width: 20px;
+ flex: 0 0 20px;
+ fill: none;
+ stroke: var(--lime);
+ stroke-width: 1.6;
+}
+
+.study-proof-status > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.study-proof b {
+ font-size: 9px;
+}
+
+.study-proof small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.study-proof code {
+ overflow: hidden;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.study-proof-boundary {
+ flex-direction: row !important;
+ align-items: center;
+ padding: 7px 9px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--muted-strong);
+ font-size: 7px;
+ white-space: nowrap;
+}
+
+.study-proof-boundary svg {
+ width: 13px;
+ flex-basis: 13px;
+ stroke: var(--muted);
+}
+
+.study-open-endpoints {
+ display: grid;
+ grid-template-columns: 1.4fr 1fr 1fr 1fr;
+}
+
+.study-open-endpoints > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 29px 22px;
+ border-right: 1px solid var(--border);
+}
+
+.study-open-endpoints > div:last-child {
+ border-right: 0;
+}
+
+.study-open-endpoints span,
+.study-open-endpoints small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-open-endpoints b {
+ margin: 10px 0 3px;
+ font-size: 27px;
+}
+
+.study-endpoint-primary b {
+ color: var(--lime);
+}
+
+.study-incomplete-endpoint {
+ display: flex;
+ gap: 12px;
+ margin: 26px;
+ padding: 18px;
+ border: 1px solid var(--border);
+ border-radius: 9px;
+}
+
+.study-incomplete-endpoint svg {
+ width: 18px;
+ fill: none;
+ stroke: var(--amber);
+ stroke-width: 1.7;
+}
+
+.study-incomplete-endpoint span {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.study-incomplete-endpoint small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.study-result-chip.accept {
+ border-color: rgba(159, 248, 80, 0.2);
+ background: rgba(159, 248, 80, 0.08);
+ color: var(--lime);
+}
+
+.study-result-chip.reject {
+ border-color: rgba(241, 110, 122, 0.2);
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+
+.study-rows {
+ margin-top: 12px;
+}
+
+.study-rows .study-panel-head > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+}
+
+.study-row-head,
+.study-row {
+ display: grid;
+ grid-template-columns: minmax(130px, 1.2fr) repeat(7, minmax(70px, 1fr));
+ gap: 12px;
+ align-items: center;
+ padding: 12px 20px;
+}
+
+.study-row-head {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-transform: uppercase;
+}
+
+.study-row {
+ border-top: 1px solid var(--border);
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.study-row > span:first-child {
+ display: flex;
+ flex-direction: column;
+}
+
+.study-row small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+@media (max-width: 1260px) {
+ .study-hero {
+ grid-template-columns: minmax(340px, 0.72fr) minmax(500px, 1.28fr);
+ }
+
+ .study-hero-copy {
+ padding: 34px;
+ }
+
+ .study-clock-main {
+ grid-template-columns: 58px 1fr 1fr;
+ gap: 16px;
+ }
+
+ .study-clock-lock {
+ width: 52px;
+ height: 52px;
+ }
+
+ .study-secondary-grid {
+ grid-template-columns: 1.2fr 0.8fr;
+ }
+
+ .study-queue {
+ grid-column: 1 / 3;
+ }
+}
+
+@media (max-width: 1060px) {
+ .study-content {
+ padding-right: 20px;
+ padding-left: 20px;
+ }
+
+ .study-hero {
+ grid-template-columns: 1fr;
+ }
+
+ .study-hero-copy {
+ min-height: 310px;
+ }
+
+ .study-clock {
+ min-height: 330px;
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .study-lanes {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .corrected-evidence-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .corrected-evidence-grid > div:nth-child(2) {
+ border-right: 0;
+ }
+
+ .corrected-evidence-grid > div:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .synthetic-proof-body {
+ grid-template-columns: 1fr;
+ }
+
+ .synthetic-proof-story {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .lane-boundary {
+ grid-column: 1 / 3;
+ border-top: 1px solid var(--border);
+ }
+
+ .study-primary-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .study-guardrails {
+ display: grid;
+ grid-template-columns: minmax(230px, 0.7fr) 1fr;
+ }
+
+ .study-guardrails .study-panel-head,
+ .study-guardrails > p,
+ .study-guardrails > footer {
+ grid-column: 1;
+ }
+
+ .study-guardrails > p {
+ border-right: 1px solid var(--border);
+ }
+
+ .study-guardrail-list {
+ grid-column: 2;
+ grid-row: 1 / 4;
+ }
+
+ .study-rule-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .study-rule-grid > div {
+ min-height: 0;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .study-proof {
+ grid-template-columns: 1.2fr 1fr 1fr;
+ }
+
+ .study-proof-boundary {
+ width: fit-content;
+ }
+}
+
+@media (max-width: 760px) {
+ .study-content {
+ padding: 12px 12px 104px;
+ }
+
+ .study-hero {
+ border-radius: 12px 12px 3px 3px;
+ }
+
+ .study-hero-copy {
+ min-height: 340px;
+ padding: 30px 24px;
+ }
+
+ .study-hero h2 {
+ font-size: clamp(44px, 14vw, 61px);
+ }
+
+ .study-clock {
+ min-height: 0;
+ }
+
+ .study-clock-main {
+ grid-template-columns: 1fr 1fr;
+ padding: 24px 19px 18px;
+ }
+
+ .study-clock-lock {
+ grid-column: 1 / 3;
+ }
+
+ .study-clock-progress {
+ grid-template-columns: 1fr;
+ padding-right: 19px;
+ padding-left: 19px;
+ }
+
+ .study-clock > footer {
+ grid-template-columns: 1fr;
+ padding-right: 19px;
+ padding-left: 19px;
+ }
+
+ .study-lanes {
+ grid-template-columns: 1fr;
+ }
+
+ .study-lane,
+ .lane-boundary {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .lane-boundary {
+ grid-column: 1;
+ }
+
+ .corrected-evidence-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .corrected-evidence-grid > div {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .study-corrected-evidence > footer {
+ flex-direction: column;
+ }
+
+ .synthetic-proof-facts {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .synthetic-proof-facts > span:nth-child(2) {
+ border-right: 0;
+ }
+
+ .synthetic-proof-facts > span:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .synthetic-proof-flow {
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .synthetic-proof-flow > span {
+ justify-content: flex-start;
+ }
+
+ .synthetic-proof-flow i {
+ transform: rotate(90deg);
+ }
+
+ .study-synthetic-proof > footer {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .sealed-endpoint-stage {
+ padding: 27px 18px 22px;
+ }
+
+ .endpoint-method {
+ grid-template-columns: 1fr;
+ gap: 11px;
+ }
+
+ .study-accept-criteria {
+ grid-template-columns: 1fr;
+ }
+
+ .study-accept-criteria > div {
+ min-height: 0;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .study-guardrails {
+ display: flex;
+ }
+
+ .study-guardrails > p {
+ border-right: 0;
+ }
+
+ .study-secondary-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .study-queue {
+ grid-column: 1;
+ }
+
+ .study-proof {
+ grid-template-columns: 1fr 1fr;
+ padding: 19px;
+ }
+
+ .study-proof-status {
+ grid-column: 1 / 3;
+ }
+
+ .study-open-endpoints {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .study-open-endpoints > div:nth-child(2) {
+ border-right: 0;
+ }
+
+ .study-open-endpoints > div:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .study-row-table {
+ overflow-x: auto;
+ }
+
+ .study-row-head,
+ .study-row {
+ min-width: 760px;
+ }
+}
+
+@media (max-width: 420px) {
+ .study-hero-copy {
+ min-height: 370px;
+ }
+
+ .study-clock > header {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .study-clock-main strong {
+ font-size: 33px;
+ }
+
+ .study-lane {
+ align-items: flex-start;
+ }
+
+ .study-lane > strong {
+ margin-left: 0;
+ }
+
+ .endpoint-vault {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .candidate-parameters {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .candidate-parameters > span:nth-child(2) {
+ border-right: 0;
+ }
+
+ .candidate-parameters > span:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .risk-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .synthetic-proof-facts {
+ grid-template-columns: 1fr;
+ }
+
+ .synthetic-proof-facts > span {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .risk-grid > span {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .queue-flow {
+ grid-template-columns: 1fr;
+ }
+
+ .queue-flow > i {
+ transform: rotate(90deg);
+ justify-self: center;
+ }
+
+ .study-proof {
+ grid-template-columns: 1fr;
+ }
+
+ .study-proof-status {
+ grid-column: 1;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Judge-facing portable proof */
+
+.judge-proof-shell {
+ background:
+ radial-gradient(circle at 87% 4%, rgba(233, 185, 104, 0.1), transparent 28rem),
+ radial-gradient(circle at 39% 22%, rgba(100, 137, 246, 0.08), transparent 34rem),
+ var(--bg);
+}
+
+.judge-proof-content {
+ width: min(1460px, calc(100% - 48px));
+ margin: 22px auto 0;
+}
+
+.judge-proof-hero {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(370px, 0.82fr) minmax(590px, 1.18fr);
+ min-height: 440px;
+ overflow: hidden;
+ border: 1px solid rgba(233, 185, 104, 0.2);
+ border-radius: 20px;
+ background:
+ linear-gradient(108deg, rgba(233, 185, 104, 0.08), rgba(10, 14, 31, 0.95) 42%, rgba(100, 137, 246, 0.055)),
+ var(--surface-soft);
+ box-shadow: var(--shadow);
+}
+
+.judge-proof-hero::before {
+ position: absolute;
+ top: -180px;
+ left: -110px;
+ width: 440px;
+ height: 440px;
+ border: 1px solid rgba(233, 185, 104, 0.1);
+ border-radius: 50%;
+ box-shadow: 0 0 0 70px rgba(233, 185, 104, 0.018), 0 0 0 140px rgba(233, 185, 104, 0.012);
+ content: "";
+}
+
+.judge-proof-hero::after {
+ position: absolute;
+ right: 48%;
+ bottom: -140px;
+ width: 280px;
+ height: 280px;
+ border-radius: 50%;
+ background: rgba(100, 137, 246, 0.05);
+ filter: blur(80px);
+ content: "";
+}
+
+.judge-proof-hero-copy {
+ position: relative;
+ z-index: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ padding: clamp(38px, 5vw, 72px);
+ border-right: 1px solid var(--border);
+}
+
+.judge-proof-kicker {
+ margin-top: 24px;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 9px;
+ letter-spacing: 0.13em;
+ text-transform: uppercase;
+}
+
+.judge-proof-hero h2 {
+ margin: 11px 0 21px;
+ color: var(--text);
+ font-size: clamp(43px, 5.2vw, 78px);
+ font-weight: 800;
+ letter-spacing: -0.075em;
+ line-height: 0.9;
+}
+
+.judge-proof-hero h2 em {
+ color: var(--amber);
+ font-style: normal;
+}
+
+.judge-proof-hero-copy > p {
+ max-width: 580px;
+ margin: 0;
+ color: var(--muted-strong);
+ font-size: 12px;
+ line-height: 1.75;
+}
+
+.judge-proof-hero-copy > p strong {
+ color: var(--text-soft);
+}
+
+.judge-proof-refusal {
+ position: relative;
+ z-index: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ margin: 34px;
+ overflow: hidden;
+ border: 1px solid var(--border-strong);
+ border-radius: 16px;
+ background: rgba(7, 10, 23, 0.8);
+ box-shadow: 0 28px 70px rgba(0, 0, 0, 0.22);
+}
+
+.judge-proof-refusal > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 18px;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.judge-proof-refusal > header > span {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.judge-proof-refusal > header i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--amber);
+ box-shadow: 0 0 0 4px rgba(233, 185, 104, 0.11);
+}
+
+.judge-proof-refusal > header em {
+ padding: 5px 7px;
+ border: 1px solid rgba(241, 110, 122, 0.2);
+ border-radius: 6px;
+ background: rgba(241, 110, 122, 0.06);
+ color: var(--danger);
+ font-style: normal;
+}
+
+.judge-proof-refusal-flow {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr auto 1fr;
+ align-items: stretch;
+ padding: 24px 18px;
+}
+
+.judge-proof-refusal-flow > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 7px;
+ padding: 18px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: rgba(20, 26, 52, 0.62);
+}
+
+.judge-proof-refusal-flow > div.authority {
+ border-color: rgba(100, 137, 246, 0.24);
+ background: rgba(100, 137, 246, 0.07);
+}
+
+.judge-proof-refusal-flow > div.refused {
+ border-color: rgba(241, 110, 122, 0.24);
+ background: rgba(241, 110, 122, 0.07);
+}
+
+.judge-proof-refusal-flow small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.judge-proof-refusal-flow b {
+ color: var(--text-soft);
+ font-size: 13px;
+ text-transform: uppercase;
+}
+
+.judge-proof-refusal-flow .refused b {
+ color: var(--danger);
+ font-size: 22px;
+ letter-spacing: 0.06em;
+}
+
+.judge-proof-refusal-flow div > span {
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.judge-proof-refusal-flow > i {
+ display: grid;
+ padding: 0 8px;
+ place-items: center;
+ color: var(--muted);
+}
+
+.judge-proof-refusal-flow > i svg {
+ width: 15px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-refusal > footer {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 15px 18px;
+ border-top: 1px solid rgba(100, 137, 246, 0.14);
+ background: rgba(100, 137, 246, 0.055);
+}
+
+.judge-proof-refusal > footer > svg {
+ width: 20px;
+ flex: 0 0 20px;
+ fill: none;
+ stroke: var(--blue);
+ stroke-width: 1.7;
+}
+
+.judge-proof-refusal > footer > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-refusal > footer b {
+ color: var(--text-soft);
+ font-size: 9px;
+}
+
+.judge-proof-refusal > footer small {
+ color: var(--muted);
+ font-size: 7px;
+ line-height: 1.45;
+}
+
+.judge-proof-truth {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ margin-top: 12px;
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.judge-proof-truth > div {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 18px 20px;
+ border-right: 1px solid var(--border);
+}
+
+.judge-proof-truth > div:last-child {
+ border-right: 0;
+}
+
+.judge-proof-truth span,
+.judge-proof-truth small {
+ color: var(--muted);
+ font-size: 8px;
+}
+
+.judge-proof-truth span {
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.judge-proof-truth b {
+ color: var(--text-soft);
+ font-size: 11px;
+}
+
+.judge-proof-workbench {
+ display: grid;
+ grid-template-columns: minmax(0, 1.62fr) minmax(315px, 0.68fr);
+ gap: 12px;
+ align-items: start;
+ margin-top: 12px;
+}
+
+.judge-proof-ledger {
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.judge-proof-panel-head {
+ min-height: 72px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 15px 21px;
+ border-bottom: 1px solid var(--border);
+}
+
+.judge-proof-panel-head > div {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-panel-head > div > span,
+.judge-proof-panel-head > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.judge-proof-panel-head h2 {
+ margin: 0;
+ font-size: 17px;
+ letter-spacing: -0.035em;
+}
+
+.judge-proof-panel-head > span {
+ padding: 6px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+}
+
+.judge-proof-ledger-head,
+.judge-proof-ledger-row {
+ display: grid;
+ grid-template-columns: 42px 38px minmax(230px, 1fr) 116px minmax(150px, 0.72fr);
+ align-items: center;
+}
+
+.judge-proof-ledger-head {
+ min-height: 34px;
+ padding: 0 18px;
+ border-bottom: 1px solid var(--border);
+ background: rgba(7, 10, 23, 0.35);
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.judge-proof-ledger-row {
+ position: relative;
+ min-height: 69px;
+ padding: 9px 18px;
+ border-bottom: 1px solid var(--border);
+}
+
+.judge-proof-ledger-row::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 78px;
+ width: 1px;
+ background: var(--border);
+ content: "";
+}
+
+.judge-proof-ledger-row.risk {
+ background: linear-gradient(90deg, rgba(100, 137, 246, 0.07), rgba(241, 110, 122, 0.035));
+}
+
+.judge-proof-sequence {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 8px;
+}
+
+.judge-proof-event-icon {
+ position: relative;
+ z-index: 1;
+ width: 25px;
+ height: 25px;
+ display: grid;
+ place-items: center;
+ border: 1px solid var(--border-strong);
+ border-radius: 8px;
+ background: var(--surface);
+ color: var(--muted-strong);
+}
+
+.judge-proof-ledger-row.risk .judge-proof-event-icon {
+ border-color: rgba(100, 137, 246, 0.32);
+ background: rgba(100, 137, 246, 0.12);
+ color: var(--blue);
+}
+
+.judge-proof-event-icon svg {
+ width: 13px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-event-copy {
+ min-width: 0;
+ display: grid;
+ gap: 2px;
+ padding-left: 10px;
+}
+
+.judge-proof-event-copy small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.judge-proof-event-copy b {
+ color: var(--text-soft);
+ font-size: 10px;
+}
+
+.judge-proof-event-copy em {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 7px;
+ font-style: normal;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.judge-proof-ledger-row time,
+.judge-proof-ledger-row code {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.judge-proof-ledger-row code {
+ overflow: hidden;
+ color: var(--muted-strong);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.judge-proof-ledger > footer {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ padding: 15px 20px;
+ background: rgba(145, 131, 229, 0.045);
+}
+
+.judge-proof-ledger > footer > svg {
+ width: 17px;
+ flex: 0 0 17px;
+ fill: none;
+ stroke: var(--violet);
+ stroke-width: 1.7;
+}
+
+.judge-proof-ledger > footer > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-ledger > footer b {
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.judge-proof-ledger > footer small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.judge-proof-inspector {
+ display: grid;
+ gap: 12px;
+}
+
+.judge-proof-verify,
+.judge-proof-hashes {
+ overflow: hidden;
+ border-radius: 14px;
+}
+
+.judge-proof-verify > header {
+ display: flex;
+ gap: 13px;
+ padding: 21px;
+ border-bottom: 1px solid var(--border);
+}
+
+.judge-proof-verify > header > span {
+ width: 42px;
+ height: 42px;
+ display: grid;
+ flex: 0 0 42px;
+ place-items: center;
+ border: 1px solid rgba(159, 248, 80, 0.25);
+ border-radius: 12px;
+ background: rgba(159, 248, 80, 0.07);
+ color: var(--lime);
+}
+
+.judge-proof-verify > header > span.invalid {
+ border-color: rgba(241, 110, 122, 0.25);
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+
+.judge-proof-verify > header svg {
+ width: 21px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-verify > header > div {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-verify > header small,
+.judge-proof-hashes small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.judge-proof-verify h2 {
+ margin: 0;
+ font-size: 16px;
+ letter-spacing: -0.035em;
+}
+
+.judge-proof-verify > header p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 7px;
+ line-height: 1.45;
+}
+
+.judge-proof-checks > div {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 13px 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.judge-proof-checks > div > span:first-child {
+ width: 23px;
+ height: 23px;
+ display: grid;
+ flex: 0 0 23px;
+ place-items: center;
+ border-radius: 50%;
+ background: rgba(159, 248, 80, 0.08);
+ color: var(--lime);
+}
+
+.judge-proof-checks > div > span:first-child.fail {
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+
+.judge-proof-checks svg,
+.judge-proof-generation-status svg {
+ width: 12px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+}
+
+.judge-proof-checks > div > span:last-child,
+.judge-proof-generation-status > span {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.judge-proof-checks b,
+.judge-proof-generation-status b {
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.judge-proof-checks small,
+.judge-proof-generation-status small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+.judge-proof-generation-status {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 15px 20px;
+ background: rgba(159, 248, 80, 0.035);
+ color: var(--lime);
+}
+
+.judge-proof-generation-status > svg {
+ width: 18px;
+ flex: 0 0 18px;
+}
+
+.judge-proof-hashes > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.judge-proof-hashes > header svg {
+ width: 14px;
+ fill: none;
+ stroke: var(--muted);
+ stroke-width: 1.7;
+}
+
+.judge-proof-hashes > div {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 13px 18px;
+ border-bottom: 1px solid var(--border);
+}
+
+.judge-proof-hashes code {
+ overflow: hidden;
+ color: var(--muted-strong);
+ font-family: var(--mono);
+ font-size: 7px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.judge-proof-hashes > footer {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+}
+
+.judge-proof-hashes > footer > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ padding: 12px 18px;
+ border-right: 1px solid var(--border);
+}
+
+.judge-proof-hashes > footer > span:last-child {
+ border-right: 0;
+}
+
+.judge-proof-hashes > footer b {
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.judge-proof-hashes > footer small {
+ font-family: var(--sans);
+ text-transform: none;
+}
+
+.judge-proof-download {
+ min-height: 62px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 13px 17px;
+ border: 1px solid rgba(159, 248, 80, 0.2);
+ border-radius: 13px;
+ background: rgba(159, 248, 80, 0.06);
+ color: var(--lime);
+ text-decoration: none;
+ transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
+}
+
+.judge-proof-download:hover {
+ border-color: rgba(159, 248, 80, 0.38);
+ background: rgba(159, 248, 80, 0.09);
+ transform: translateY(-1px);
+}
+
+.judge-proof-download > span {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+}
+
+.judge-proof-download > span > svg,
+.judge-proof-download > svg {
+ width: 17px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-download > span > span {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.judge-proof-download small {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+}
+
+.judge-proof-download b {
+ font-size: 9px;
+}
+
+.judge-proof-reproduce {
+ display: grid;
+ grid-template-columns: minmax(300px, 0.9fr) minmax(500px, 1.1fr);
+ margin-top: 12px;
+ overflow: hidden;
+ border-color: rgba(145, 131, 229, 0.2);
+ border-radius: 14px;
+}
+
+.judge-proof-reproduce-copy {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 25px;
+ border-right: 1px solid var(--border);
+}
+
+.judge-proof-reproduce-copy > span {
+ width: 46px;
+ height: 46px;
+ display: grid;
+ flex: 0 0 46px;
+ place-items: center;
+ border: 1px solid rgba(145, 131, 229, 0.24);
+ border-radius: 13px;
+ background: rgba(145, 131, 229, 0.08);
+ color: var(--violet);
+}
+
+.judge-proof-reproduce-copy svg {
+ width: 21px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-reproduce-copy > div {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-reproduce-copy small,
+.judge-proof-command > span {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 7px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.judge-proof-reproduce-copy h2 {
+ margin: 0;
+ font-size: 15px;
+ letter-spacing: -0.025em;
+}
+
+.judge-proof-reproduce-copy p {
+ margin: 2px 0 0;
+ color: var(--muted);
+ font-size: 8px;
+ line-height: 1.55;
+}
+
+.judge-proof-command {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 9px;
+ padding: 24px;
+}
+
+.judge-proof-command code {
+ overflow-x: auto;
+ padding: 14px 15px;
+ border: 1px solid var(--border-strong);
+ border-radius: 9px;
+ background: #060918;
+ color: var(--lime);
+ font-family: var(--mono);
+ font-size: 9px;
+ white-space: nowrap;
+}
+
+.judge-proof-command a {
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-end;
+ gap: 6px;
+ color: var(--violet);
+ font-size: 8px;
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.judge-proof-command a svg {
+ width: 12px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.7;
+}
+
+.judge-proof-reproduce > footer {
+ grid-column: 1 / -1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 13px 24px;
+ border-top: 1px solid var(--border);
+ background: rgba(145, 131, 229, 0.04);
+}
+
+.judge-proof-reproduce > footer svg {
+ width: 16px;
+ flex: 0 0 16px;
+ fill: none;
+ stroke: var(--violet);
+ stroke-width: 1.7;
+}
+
+.judge-proof-reproduce > footer p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 7px;
+ line-height: 1.5;
+}
+
+.judge-proof-reproduce > footer b {
+ color: var(--text-soft);
+}
+
+.judge-proof-final-boundary {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 11px;
+ margin: 12px 0 0;
+ padding: 18px 20px;
+ border: 1px solid rgba(233, 185, 104, 0.16);
+ border-radius: 12px;
+ background: rgba(233, 185, 104, 0.045);
+}
+
+.judge-proof-final-boundary > svg {
+ width: 18px;
+ flex: 0 0 18px;
+ fill: none;
+ stroke: var(--amber);
+ stroke-width: 1.7;
+}
+
+.judge-proof-final-boundary > span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.judge-proof-final-boundary b {
+ color: var(--text-soft);
+ font-size: 8px;
+}
+
+.judge-proof-final-boundary small {
+ color: var(--muted);
+ font-size: 7px;
+}
+
+@media (max-width: 1240px) {
+ .judge-proof-hero {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .judge-proof-hero-copy {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .judge-proof-refusal {
+ margin-top: 0;
+ }
+
+ .judge-proof-workbench {
+ grid-template-columns: 1fr;
+ }
+
+ .judge-proof-inspector {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .judge-proof-download {
+ grid-column: 1 / -1;
+ }
+}
+
+@media (max-width: 980px) {
+ .judge-proof-content {
+ width: min(100% - 32px, 860px);
+ }
+
+ .judge-proof-refusal-flow {
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .judge-proof-refusal-flow > i {
+ transform: rotate(90deg);
+ }
+
+ .judge-proof-truth {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .judge-proof-truth > div {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .judge-proof-truth > div:nth-child(even) {
+ border-right: 0;
+ }
+
+ .judge-proof-truth > div:last-child {
+ grid-column: 1 / -1;
+ border-bottom: 0;
+ }
+
+ .judge-proof-reproduce {
+ grid-template-columns: 1fr;
+ }
+
+ .judge-proof-reproduce-copy {
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .judge-proof-reproduce > footer {
+ grid-column: 1;
+ }
+}
+
+@media (max-width: 760px) {
+ .judge-proof-content {
+ width: calc(100% - 22px);
+ margin-top: 11px;
+ padding-bottom: 82px;
+ }
+
+ .judge-proof-hero-copy {
+ padding: 34px 25px;
+ }
+
+ .judge-proof-hero h2 {
+ font-size: clamp(39px, 13vw, 62px);
+ }
+
+ .judge-proof-refusal {
+ margin: 14px;
+ }
+
+ .judge-proof-inspector {
+ grid-template-columns: 1fr;
+ }
+
+ .judge-proof-download {
+ grid-column: 1;
+ }
+
+ .judge-proof-ledger-head {
+ display: none;
+ }
+
+ .judge-proof-ledger-row {
+ grid-template-columns: 34px 32px minmax(0, 1fr);
+ row-gap: 6px;
+ min-height: 84px;
+ padding: 12px 14px;
+ }
+
+ .judge-proof-ledger-row::before {
+ left: 63px;
+ }
+
+ .judge-proof-ledger-row time {
+ grid-column: 3;
+ }
+
+ .judge-proof-ledger-row code {
+ display: none;
+ }
+
+ .judge-proof-event-copy {
+ padding-left: 7px;
+ }
+}
+
+@media (max-width: 520px) {
+ .judge-proof-hero-copy > .provenance-badge {
+ white-space: normal;
+ }
+
+ .judge-proof-refusal {
+ margin: 10px;
+ }
+
+ .judge-proof-refusal-flow {
+ padding: 14px;
+ }
+
+ .judge-proof-truth {
+ grid-template-columns: 1fr;
+ }
+
+ .judge-proof-truth > div,
+ .judge-proof-truth > div:nth-child(even),
+ .judge-proof-truth > div:last-child {
+ grid-column: 1;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .judge-proof-truth > div:last-child {
+ border-bottom: 0;
+ }
+
+ .judge-proof-panel-head {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .judge-proof-reproduce-copy {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .judge-proof-command {
+ padding: 18px;
+ }
+
+ .judge-proof-command code {
+ font-size: 8px;
+ }
+
+ .judge-proof-final-boundary {
+ align-items: flex-start;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .judge-proof-download {
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json
new file mode 100644
index 0000000..c366e93
--- /dev/null
+++ b/apps/dashboard/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "types": ["vite/client"]
+ },
+ "include": [
+ "src",
+ "worker",
+ "vite.config.ts",
+ "vite.worker.config.ts",
+ "../../src/dash/public-contract.ts",
+ "../../public/artifacts/edge/manifest.json"
+ ]
+}
diff --git a/apps/dashboard/vite.config.ts b/apps/dashboard/vite.config.ts
new file mode 100644
index 0000000..a57d735
--- /dev/null
+++ b/apps/dashboard/vite.config.ts
@@ -0,0 +1,39 @@
+import { fileURLToPath, URL } from "node:url";
+import react from "@vitejs/plugin-react";
+import { defineConfig, type Plugin } from "vite";
+import { handleDashboardApi, writeApiResult } from "../../src/dash/api.js";
+
+const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
+
+function dashboardApi(): Plugin {
+ return {
+ name: "samaritan-dashboard-api",
+ configureServer(server) {
+ server.middlewares.use(async (request, response, next) => {
+ const url = new URL(request.url ?? "/", "http://localhost");
+ const result = await handleDashboardApi(url.pathname, repoRoot, { method: request.method ?? "GET" });
+ if (!result) {
+ next();
+ return;
+ }
+ writeApiResult(response, result, request.method === "HEAD");
+ });
+ }
+ };
+}
+
+export default defineConfig({
+ root: fileURLToPath(new URL(".", import.meta.url)),
+ publicDir: fileURLToPath(new URL("../../public", import.meta.url)),
+ plugins: [react(), dashboardApi()],
+ build: {
+ // Sites binds static assets from the conventional dist/client directory;
+ // dist/server/index.js is the Worker entry point packaged alongside it.
+ outDir: "dist/client",
+ emptyOutDir: true
+ },
+ server: {
+ host: "127.0.0.1",
+ port: 4173
+ }
+});
diff --git a/apps/dashboard/vite.worker.config.ts b/apps/dashboard/vite.worker.config.ts
new file mode 100644
index 0000000..5c2e0d0
--- /dev/null
+++ b/apps/dashboard/vite.worker.config.ts
@@ -0,0 +1,24 @@
+import { fileURLToPath, URL } from "node:url";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ root: fileURLToPath(new URL(".", import.meta.url)),
+ publicDir: false,
+ build: {
+ target: "es2022",
+ outDir: "dist/server",
+ emptyOutDir: false,
+ copyPublicDir: false,
+ minify: false,
+ lib: {
+ entry: fileURLToPath(new URL("./worker/index.ts", import.meta.url)),
+ formats: ["es"],
+ fileName: () => "index.js"
+ },
+ rollupOptions: {
+ output: {
+ entryFileNames: "index.js"
+ }
+ }
+ }
+});
diff --git a/apps/dashboard/worker/index.ts b/apps/dashboard/worker/index.ts
new file mode 100644
index 0000000..5299cba
--- /dev/null
+++ b/apps/dashboard/worker/index.ts
@@ -0,0 +1,180 @@
+import edgeManifestValue from "../../../public/artifacts/edge/manifest.json";
+import {
+ buildTxlinePulse,
+ createTxlinePulseCache,
+ TXLINE_PULSE_API_PATH
+} from "../../../src/dash/txline-pulse.js";
+
+type AssetFetcher = {
+ fetch(request: Request): Promise;
+};
+
+export type SamaritanWorkerEnv = {
+ ASSETS: AssetFetcher;
+ TXLINE_JWT?: string;
+ TXLINE_API_TOKEN?: string;
+ TXLINE_SERVICE_LEVEL_ID?: string;
+};
+
+type EdgeRoute = {
+ apiPath: string;
+ assetPath: string;
+ bytes: number;
+ sha256: string;
+};
+
+type EdgeManifest = {
+ sourceBundleSha256: string;
+ edgeBundleSha256: string;
+ routes: EdgeRoute[];
+};
+
+const edgeManifest = edgeManifestValue as EdgeManifest;
+const apiRoutes = new Map(edgeManifest.routes.map((route) => [route.apiPath, route]));
+const txlinePulseCache = createTxlinePulseCache();
+
+const securityHeaders: Readonly> = {
+ "content-security-policy": "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'",
+ "cross-origin-opener-policy": "same-origin",
+ "permissions-policy": "camera=(), geolocation=(), microphone=(), payment=(), usb=()",
+ "referrer-policy": "no-referrer",
+ "x-content-type-options": "nosniff",
+ "x-frame-options": "DENY"
+};
+
+function secureHeaders(source?: HeadersInit): Headers {
+ const headers = new Headers(source);
+ for (const [name, value] of Object.entries(securityHeaders)) headers.set(name, value);
+ return headers;
+}
+
+function jsonResponse(
+ status: number,
+ value: unknown,
+ options: { headOnly?: boolean; headers?: HeadersInit } = {}
+): Response {
+ const body = `${JSON.stringify(value)}\n`;
+ const headers = secureHeaders(options.headers);
+ headers.set("cache-control", "no-store");
+ headers.set("content-type", "application/json; charset=utf-8");
+ headers.set("content-length", String(new TextEncoder().encode(body).byteLength));
+ return new Response(options.headOnly ? null : body, { status, headers });
+}
+
+function methodNotAllowed(headOnly: boolean): Response {
+ return jsonResponse(405, { error: "method_not_allowed" }, {
+ headOnly,
+ headers: { allow: "GET, HEAD" }
+ });
+}
+
+function unavailable(headOnly: boolean): Response {
+ return jsonResponse(503, { error: "evidence_unavailable" }, { headOnly });
+}
+
+async function sha256Hex(bytes: ArrayBuffer): Promise {
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
+ return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
+}
+
+async function frozenApiResponse(
+ request: Request,
+ env: SamaritanWorkerEnv,
+ route: EdgeRoute
+): Promise {
+ try {
+ const assetUrl = new URL(route.assetPath, request.url);
+ const assetResponse = await env.ASSETS.fetch(new Request(assetUrl, {
+ method: "GET",
+ headers: { accept: "application/json" }
+ }));
+ if (!assetResponse.ok) return unavailable(request.method === "HEAD");
+ const body = await assetResponse.arrayBuffer();
+ const bytes = new Uint8Array(body);
+ if (bytes.byteLength !== route.bytes || await sha256Hex(body) !== route.sha256) {
+ return unavailable(request.method === "HEAD");
+ }
+ const headers = secureHeaders({
+ "cache-control": "no-store",
+ "content-length": String(bytes.byteLength),
+ "content-type": "application/json; charset=utf-8",
+ "x-samaritan-edge-bundle": edgeManifest.edgeBundleSha256,
+ "x-samaritan-public-bundle": edgeManifest.sourceBundleSha256
+ });
+ return new Response(request.method === "HEAD" ? null : bytes, { status: 200, headers });
+ } catch {
+ return unavailable(request.method === "HEAD");
+ }
+}
+
+function isSpaRoute(pathname: string): boolean {
+ const finalSegment = pathname.split("/").at(-1) ?? "";
+ return pathname === "/" || pathname.endsWith("/") || !finalSegment.includes(".");
+}
+
+function staticResponse(request: Request, response: Response, spaFallback: boolean): Response {
+ const headers = secureHeaders(response.headers);
+ const pathname = new URL(request.url).pathname;
+ if (spaFallback || pathname === "/" || pathname === "/index.html") {
+ headers.set("cache-control", "no-cache");
+ } else if (pathname.startsWith("/artifacts/")) {
+ headers.set("cache-control", "no-store");
+ } else if (pathname.startsWith("/assets/") && response.ok) {
+ headers.set("cache-control", "public, max-age=31536000, immutable");
+ } else if (!response.ok) {
+ headers.set("cache-control", "no-store");
+ }
+ return new Response(request.method === "HEAD" ? null : response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers
+ });
+}
+
+async function assetResponse(request: Request, env: SamaritanWorkerEnv): Promise {
+ const direct = await env.ASSETS.fetch(request);
+ const url = new URL(request.url);
+ if (direct.status !== 404 || !isSpaRoute(url.pathname)) {
+ return staticResponse(request, direct, false);
+ }
+ const fallbackUrl = new URL("/index.html", url);
+ const fallback = await env.ASSETS.fetch(new Request(fallbackUrl, {
+ method: request.method,
+ headers: request.headers
+ }));
+ return staticResponse(request, fallback, true);
+}
+
+async function handleRequest(request: Request, env: SamaritanWorkerEnv): Promise {
+ const method = request.method.toUpperCase();
+ if (method !== "GET" && method !== "HEAD") return methodNotAllowed(false);
+
+ const pathname = new URL(request.url).pathname;
+ const route = apiRoutes.get(pathname);
+ if (route) return frozenApiResponse(request, env, route);
+ if (pathname === TXLINE_PULSE_API_PATH) {
+ const credentials = env.TXLINE_SERVICE_LEVEL_ID === "12" && env.TXLINE_JWT && env.TXLINE_API_TOKEN
+ ? { jwt: env.TXLINE_JWT, apiToken: env.TXLINE_API_TOKEN }
+ : null;
+ const pulse = await txlinePulseCache.get(
+ credentials === null ? "unconfigured" : "configured",
+ () => buildTxlinePulse({ credentials })
+ );
+ return jsonResponse(200, pulse, { headOnly: method === "HEAD" });
+ }
+ if (pathname === "/api/v1/health") {
+ return jsonResponse(200, { status: "ok", service: "samaritan-dashboard", readOnly: true }, {
+ headOnly: method === "HEAD"
+ });
+ }
+ if (pathname.startsWith("/api/")) {
+ return jsonResponse(404, { error: "not_found" }, { headOnly: method === "HEAD" });
+ }
+ return assetResponse(request, env);
+}
+
+export default {
+ fetch(request: Request, env: SamaritanWorkerEnv): Promise {
+ return handleRequest(request, env);
+ }
+};
diff --git a/config/captures/england-argentina-2026-07-15.json b/config/captures/england-argentina-2026-07-15.json
new file mode 100644
index 0000000..93a4210
--- /dev/null
+++ b/config/captures/england-argentina-2026-07-15.json
@@ -0,0 +1,40 @@
+{
+ "captureId": "paired-england-argentina-2026-07-15",
+ "status": "human_confirmed_for_capture_only",
+ "tradeable": false,
+ "confirmedBy": "Deborah",
+ "confirmedAt": "2026-07-12",
+ "txline": {
+ "fixtureId": "18241006",
+ "home": "England",
+ "away": "Argentina",
+ "kickoffUtc": "2026-07-15T19:00:00.000Z"
+ },
+ "polymarket": {
+ "eventId": "694581",
+ "eventSlug": "fifwc-eng-arg-2026-07-15",
+ "totalsEventId": "694786",
+ "totalsEventSlug": "fifwc-eng-arg-2026-07-15-more-markets",
+ "home": "England",
+ "away": "Argentina",
+ "kickoffUtc": "2026-07-15T19:00:00.000Z",
+ "rulesPeriod": "first_90_minutes_plus_stoppage_time"
+ },
+ "capture": {
+ "scheduledStartUtc": "2026-07-15T16:00:00.000Z",
+ "scheduledEndUtc": "2026-07-15T22:00:00.000Z",
+ "durationMinutes": 360,
+ "runLabel": "paired-england-argentina-2026-07-15",
+ "polymarketMaxAssets": 50,
+ "discoveryIntervalSeconds": 900,
+ "startupGraceSeconds": 180,
+ "streamStaleSeconds": 300,
+ "maxStartupSkewSeconds": 120
+ },
+ "evidence": {
+ "txlineFixtures": "samples/fixtures/mainnet-world-cup-fixtures.json",
+ "polymarketEvents": "data/live/gamma-discovery/open-world-cup-events.json",
+ "readinessReport": "docs/research/capture-readiness.md"
+ },
+ "note": "Deborah confirmed public-data capture only. This config cannot authorize authentication beyond existing TXLine data access, token activation, wallet access, trading, approval, deposit, order placement, or money movement."
+}
diff --git a/config/captures/france-england-2026-07-18.json b/config/captures/france-england-2026-07-18.json
new file mode 100644
index 0000000..06a82c1
--- /dev/null
+++ b/config/captures/france-england-2026-07-18.json
@@ -0,0 +1,40 @@
+{
+ "captureId": "paired-france-england-2026-07-18",
+ "status": "human_confirmed_for_capture_only",
+ "tradeable": false,
+ "confirmedBy": "Deborah",
+ "confirmedAt": "2026-07-18",
+ "txline": {
+ "fixtureId": "18257865",
+ "home": "France",
+ "away": "England",
+ "kickoffUtc": "2026-07-18T21:00:00.000Z"
+ },
+ "polymarket": {
+ "eventId": "708596",
+ "eventSlug": "fifwc-fra-eng-2026-07-18",
+ "totalsEventId": "708643",
+ "totalsEventSlug": "fifwc-fra-eng-2026-07-18-more-markets",
+ "home": "France",
+ "away": "England",
+ "kickoffUtc": "2026-07-18T21:00:00.000Z",
+ "rulesPeriod": "first_90_minutes_plus_stoppage_time"
+ },
+ "capture": {
+ "scheduledStartUtc": "2026-07-18T18:00:00.000Z",
+ "scheduledEndUtc": "2026-07-19T00:00:00.000Z",
+ "durationMinutes": 360,
+ "runLabel": "paired-france-england-2026-07-18",
+ "polymarketMaxAssets": 50,
+ "discoveryIntervalSeconds": 900,
+ "startupGraceSeconds": 180,
+ "streamStaleSeconds": 300,
+ "maxStartupSkewSeconds": 120
+ },
+ "evidence": {
+ "txlineFixtures": "samples/fixtures/mainnet-world-cup-fixtures.json",
+ "polymarketEvents": "data/live/gamma-discovery/open-world-cup-events.json",
+ "readinessReport": "docs/research/capture-readiness.md"
+ },
+ "note": "Deborah authorized capture-only preparation on July 18. This config cannot authorize study admission, mapping approval, Polymarket authentication, wallet access, trading, approval, deposit, order placement, Solana submission, or money movement."
+}
diff --git a/config/captures/france-spain-2026-07-14.json b/config/captures/france-spain-2026-07-14.json
new file mode 100644
index 0000000..f13b912
--- /dev/null
+++ b/config/captures/france-spain-2026-07-14.json
@@ -0,0 +1,40 @@
+{
+ "captureId": "paired-france-spain-2026-07-14",
+ "status": "human_confirmed_for_capture_only",
+ "tradeable": false,
+ "confirmedBy": "Deborah",
+ "confirmedAt": "2026-07-12",
+ "txline": {
+ "fixtureId": "18237038",
+ "home": "France",
+ "away": "Spain",
+ "kickoffUtc": "2026-07-14T19:00:00.000Z"
+ },
+ "polymarket": {
+ "eventId": "691040",
+ "eventSlug": "fifwc-fra-esp-2026-07-14",
+ "totalsEventId": "691131",
+ "totalsEventSlug": "fifwc-fra-esp-2026-07-14-more-markets",
+ "home": "France",
+ "away": "Spain",
+ "kickoffUtc": "2026-07-14T19:00:00.000Z",
+ "rulesPeriod": "first_90_minutes_plus_stoppage_time"
+ },
+ "capture": {
+ "scheduledStartUtc": "2026-07-14T16:00:00.000Z",
+ "scheduledEndUtc": "2026-07-14T22:00:00.000Z",
+ "durationMinutes": 360,
+ "runLabel": "paired-france-spain-2026-07-14",
+ "polymarketMaxAssets": 50,
+ "discoveryIntervalSeconds": 900,
+ "startupGraceSeconds": 180,
+ "streamStaleSeconds": 300,
+ "maxStartupSkewSeconds": 120
+ },
+ "evidence": {
+ "txlineFixtures": "samples/fixtures/mainnet-world-cup-fixtures.json",
+ "polymarketEvents": "data/live/gamma-discovery/open-world-cup-events.json",
+ "readinessReport": "docs/research/capture-readiness.md"
+ },
+ "note": "Deborah confirmed public-data capture only. This config cannot authorize authentication beyond existing TXLine data access, token activation, wallet access, trading, approval, deposit, order placement, or money movement."
+}
diff --git a/config/captures/spain-argentina-2026-07-19.json b/config/captures/spain-argentina-2026-07-19.json
new file mode 100644
index 0000000..68a315a
--- /dev/null
+++ b/config/captures/spain-argentina-2026-07-19.json
@@ -0,0 +1,40 @@
+{
+ "captureId": "paired-spain-argentina-2026-07-19",
+ "status": "human_confirmed_for_capture_only",
+ "tradeable": false,
+ "confirmedBy": "Deborah",
+ "confirmedAt": "2026-07-18",
+ "txline": {
+ "fixtureId": "18257739",
+ "home": "Spain",
+ "away": "Argentina",
+ "kickoffUtc": "2026-07-19T19:00:00.000Z"
+ },
+ "polymarket": {
+ "eventId": "708597",
+ "eventSlug": "fifwc-esp-arg-2026-07-19",
+ "totalsEventId": "708641",
+ "totalsEventSlug": "fifwc-esp-arg-2026-07-19-more-markets",
+ "home": "Spain",
+ "away": "Argentina",
+ "kickoffUtc": "2026-07-19T19:00:00.000Z",
+ "rulesPeriod": "first_90_minutes_plus_stoppage_time"
+ },
+ "capture": {
+ "scheduledStartUtc": "2026-07-19T16:00:00.000Z",
+ "scheduledEndUtc": "2026-07-19T22:00:00.000Z",
+ "durationMinutes": 360,
+ "runLabel": "paired-spain-argentina-2026-07-19",
+ "polymarketMaxAssets": 50,
+ "discoveryIntervalSeconds": 900,
+ "startupGraceSeconds": 180,
+ "streamStaleSeconds": 300,
+ "maxStartupSkewSeconds": 120
+ },
+ "evidence": {
+ "txlineFixtures": "samples/fixtures/mainnet-world-cup-fixtures.json",
+ "polymarketEvents": "data/live/gamma-discovery/open-world-cup-events.json",
+ "readinessReport": "docs/research/capture-readiness.md"
+ },
+ "note": "Deborah authorized capture-only preparation on July 18. This backup config cannot authorize study admission, mapping approval, Polymarket authentication, wallet access, trading, approval, deposit, order placement, Solana submission, or money movement."
+}
diff --git a/docs/01-research.md b/docs/01-research.md
index 6529704..82e5867 100644
--- a/docs/01-research.md
+++ b/docs/01-research.md
@@ -1,6 +1,6 @@
# 01 — Research: The Bounty, The Data, The Battlefield
-*Research conducted July 3–4 and updated from Phase 0/0.5 captures through July 10, 2026. Captured samples override documentation when they disagree.*
+*Research conducted July 3–4 and updated from Phase 0/0.5 captures through July 10, with bounty/competition status refreshed July 18, 2026. Captured samples override documentation when they disagree.*
---
@@ -13,13 +13,13 @@
| Track prize | **16,000 USDT** — 10,000 / 4,000 / 2,000 |
| Hackathon window | June 24 – **July 19, 2026** (submission deadline; final match day) |
| Winners announced | July 29, 2026 |
-| Competition | ~13 submissions in the track as of July 3 |
-| Eligibility | Global. Explicitly open to individual devs, teams, **and autonomous AI agents** |
+| Competition | **76 submissions reported by the official listing count endpoint as of July 18**; the number is volatile and must be rechecked at submission |
+| Eligibility | Global listing; the brief mentions AI agents, while the binding terms restrict participation/submission to natural persons. Deborah is the sole participant and owner. |
| Other tracks | Prediction Markets & Settlement; Consumer & Fan Experiences (not ours) |
| Contact | Telegram: TxLINEChat |
| Hackathon pitch | "Real products powered by TxODDS live football API on Solana" — real-time World Cup data wired into on-chain products. TxODDS is waiving data fees and the token payment requirement for the World Cup tier. |
-**Reading the judges:** it's a TxODDS + Solana event. Winning entries will (a) actually use TXLine deeply, (b) have a crypto-native/on-chain angle, (c) be real products, not demos. Our on-chain verifiable decision ledger and Merkle-proof validation hit (b) directly; the replay-backtested strategy engine hits (a) and (c).
+**Reading the judges:** this is a TxODDS + Solana event, but the brief is more specific than that shorthand. It scores core ingestion, autonomous operation, defensible logic/architecture, novelty, and production readiness, and says the demo video is weighted heavily because live match activity may be absent during review. Samaritan must therefore show a working strategy lifecycle, not merely architecture, while distinguishing local hash-chain verification from any Solana anchor that has actually been submitted and verified.
### 1.1 Hackathon legal terms — VERIFIED ([terms page](https://txline.txodds.com/documentation/legal/hackathon-terms))
@@ -28,7 +28,7 @@ These are binding and shape the build:
| Rule | Exact/near-exact language | Consequence for Samaritan |
|---|---|---|
| Originality | "Original work created during the Hackathon period"; open-source components OK with attribution; "significant portions … developed during the Hackathon" | We start clean July 4 — fully compliant |
-| **AI clause** | "Entries must be created, developed and submitted by **human participants**" — AI-generated submissions prohibited | TxLINE clarified that AI assistance is allowed. **Deborah remains the participant/project owner and submits Samaritan; Claude is development assistance and a runtime product component.** Never market the entry as “AI-built.” |
+| **AI clause** | "Entries must be created, developed and submitted by **human participants**"; TxODDS may disqualify an entry materially controlled by a bot or autonomous process | **Deborah must remain the participant, product decision-maker, narrator, reviewer, and submitter.** Claude is a bounded runtime component and development assistant, never the entrant or autonomous submitter. The listing's broader “AI agents” language does not erase the stricter legal terms; retain written sponsor clarification before relying on any interpretation. |
| **Judge access** | TxODDS "shall not be required to purchase any software, subscription, licence, token, cryptocurrency, digital asset or third-party service, **nor establish any blockchain wallet or account**" | Demo must be a **hosted app with our credentials server-side** + bundled replay dataset. Judges never touch a wallet. Non-compliance = disqualification. |
| Data license | No redistribution/publishing/sublicensing of Data; no reconstructing competing data products; hackathon data license **terminates when the hackathon ends** | Public-facing surfaces must not re-serve raw TXLine feeds. Post-hackathon we operate under the normal free/paid tier terms (which explicitly allow commercial use) — separate license from the hackathon one. |
| Compliance | Must comply with all gambling/gambling-tech laws; restricted jurisdictions may be excluded | Real-money leg (Polymarket) is our own account/jurisdiction responsibility, separate from the submission itself |
@@ -57,12 +57,13 @@ TXLine = "cryptographically verifiable sports data through a hybrid Solana on-ch
| Service Level | Data | Latency | Networks |
|---|---|---|---|
-| **SL1** | World Cup + Int'l Friendlies | **60-second delay** | mainnet + devnet |
+| **SL1 mainnet** | World Cup + Int'l Friendlies | **60-second delay** | mainnet |
+| **SL1 devnet** | World Cup + Int'l Friendlies | Current on-chain row reports `samplingIntervalSec = 0`; re-read the pricing matrix at subscription time | devnet |
| **SL12** | World Cup + Int'l Friendlies | **Real-time** | **mainnet only** |
Both include **full historical replay access**. Free tier is explicitly allowed for commercial use.
-**Implication:** in-play trading on a 60s delay is dead on arrival — the market has already repriced. We run **SL12 on mainnet** for live trading and can use devnet SL1 for integration testing. (Our wallet situation supports both.)
+**Implication:** mainnet SL1's 60-second delay is unsuitable for time-sensitive decisions. We run **SL12 on mainnet** for authoritative real-time evidence and use devnet SL1 for integration/on-chain validation. The official July 12 page now reports devnet SL1 `samplingIntervalSec = 0`, contradicting the earlier blanket 60-second description; code must inspect the current on-chain pricing row rather than infer devnet latency from the service-level number.
### 2.3 Endpoints (from the OpenAPI spec)
@@ -86,6 +87,8 @@ Both include **full historical replay access**. Free tier is explicitly allowed
SSE notes: standard `id`/`event`/`data`/`retry` format; heartbeat events; resumable via `Last-Event-ID`; `Accept-Encoding: gzip` cuts bandwidth 70–80%.
+**July 18 repository/docs drift:** the current official `txodds/tx-on-chain` repository includes runnable mainnet and devnet examples for `GET /api/scores/stat-validation-v3` plus the on-chain `validateStatV3` multiproof method. Its payload uses `statsToProve`, `multiproof.indices`, and `multiproof.hashes`. The hosted validation prose and checked OpenAPI still emphasize the legacy/V2 surface, so Samaritan records V3 as a discovered sponsor capability, not as an endpoint already integrated or demonstrated. Do not claim V3 proof support until a real response is captured and its exact network-matched IDL path passes locally. [Official validation guide](https://github.com/txodds/tx-on-chain/blob/main/documentation/examples/onchain-validation.mdx) · [official mainnet V3 example](https://github.com/txodds/tx-on-chain/blob/main/examples/mainnet/scripts/subscription_scores_v3c.ts)
+
### 2.4 Data models
**Fixture:** `FixtureId` (int64), `Participant1/2` (+ IDs), `Participant1IsHome`, `StartTime`, `Competition`, `CompetitionId`, `FixtureGroupId`, `Ts`.
@@ -99,7 +102,7 @@ SSE notes: standard `id`/`event`/`data`/`retry` format; heartbeat events; resuma
| `SuperOddsType` | Market: Match Result, Over/Under, Handicap | The liquid core markets |
| `MarketParameters` | e.g. the O/U line or handicap value | Line-move vs price-move distinction |
| `MarketPeriod` | Full Time, Half Time, … | Period-specific markets |
-| `PriceNames`, `Prices` | Outcome labels + raw odds (int32, ×1000) | Price stream |
+| `PriceNames`, `Prices` | Outcome labels + raw odds (int32, ×1000) | Price stream. The Spain-Belgium synchronized SSE capture also contained mapped market rows with intact `PriceNames` but both `Prices: []` and `Pct: []`; these are no-quote transitions and are skipped rather than normalized as zero prices. Partially populated/misaligned arrays still fail validation. |
| `Pct` | **De-vigged implied probabilities** as 3dp strings on a 0–100 percent scale | The margin removal is DONE FOR US, but ingest must divide by 100 for Samaritan's internal 0–1 probability convention. Phase 0 devnet + mainnet rows summed near 100, not near 1. |
| `InRunning` | Live-market flag | Pre-match vs in-play routing |
| `GameState` | Match state at time of odds | Context for every tick when populated. Phase 0 devnet + mainnet historical odds rows had this blank on all scanned rows. |
@@ -120,8 +123,9 @@ SSE notes: standard `id`/`event`/`data`/`retry` format; heartbeat events; resuma
- **CLOB V2 is live on production** at `https://clob.polymarket.com` as of Apr 28, 2026; legacy V1 SDKs/V1-signed orders are no longer production-compatible. Build against `@polymarket/clob-client-v2` / V2 REST only ([changelog](https://docs.polymarket.com/changelog)).
- **Public historical prices are recoverable by outcome token ID:** `GET https://clob.polymarket.com/prices-history?market=&startTs=&endTs=&fidelity=1` returns `history: [{t,p}]`. Despite the parameter name, `market` is the CLOB asset/token ID. Absolute `startTs`/`endTs` must not be combined with `interval`. Phase 0.5 captured official HTTP 400 responses for multi-week requests (`interval is too long`), so the rescue archive uses atomic five-day segments and timestamp de-duplication. The response has no bid, ask, size, or spread fields and must not be treated as an executable quote archive.
- **Public live order books require no authentication:** subscribe asset IDs at `wss://ws-subscriptions-clob.polymarket.com/ws/market`, set `custom_feature_enabled: true` for `best_bid_ask`, `new_market`, and `market_resolved`, and send `PING` every 10 seconds. Initial `book`, `price_change`, `last_trade_price`, and tick-size events are recorded separately from TXLine until the shared Phase 1 bus exists.
+- **Public V2 execution metadata requires no authentication:** `GET /clob-markets/{condition_id}` returns compact condition/token identity plus `mos` minimum order size, `mts` tick size, and `fd = {r,e,to}` fee-curve details. A July 12 smoke returned sports rate `0.05`, exponent `1`, taker-only, and minimum size `5`. The OpenAPI lists a staging hostname, but it returned 503 during validation and no official test-funds workflow was documented; do not treat it as a Polymarket devnet.
- **Phase 0.5 archive scale:** 799 match-family Gamma records representing 100 unique matches; 300 Match Result and 861 full-time totals conditions; 2,320 non-empty outcome-token histories containing 90,795,313 points. Two tokens for one Morocco–Haiti O/U 8.5 condition returned empty HTTP 200 histories. Candidate mapping covers 98/102 captured TXLine fixtures and is research-only.
-- Live in-play markets exist across matches. The proposed repricing lag after goals/reds is still a hypothesis, not a Phase 0 result: one-minute price history cannot establish a seconds-level stale-order edge, and the captured history is not bid/ask data. It requires synchronized TXLine + live Polymarket order-book measurement.
+- Live in-play markets exist across matches. The July 10 synchronized Spain-Belgium capture tested the proposed post-goal repricing lag with executable books: across three goals and six exact market groups, 12/18 exploratory cases had already moved at least 50 probability bps before TXLine first delivery, 6/18 had no material 30-second move, and 0/18 showed a clean post-TXLine stale window. One match is not a fitted distribution, but it does not support STALE_QUOTE; the detector remains disabled and paper-only.
---
@@ -138,7 +142,7 @@ SSE notes: standard `id`/`event`/`data`/`retry` format; heartbeat events; resuma
2. **Market fields verified but constrained.** Phase 0 confirmed Match Result / Over-Under / Handicap coverage in the free World Cup capture, but also confirmed the free tier exposes consensus `TXLineStablePriceDemargined` rows rather than multiple bookmaker feeds. SSE score payloads are richer than the original docs summary; keep parsers open around actions and state fields.
3. **Thin remaining schedule.** Live demo material is scarce. The rescued TXLine + Polymarket archive carries the pre-match study; remaining fixtures supply synchronized live microstructure evidence. Fixture/market discovery must roll because later matches are published over time.
4. **Polymarket V2 is already live, but market metadata still needs per-market verification.** Mitigation: isolate execution behind an adapter interface; paper adapter is the fallback that always works; fetch tick size/fees/rules per token before any mapping is tradeable.
-5. **Real-money risk.** We ARE wiring real execution (decision locked), so bankroll limits, kill switches, and the risk-manager veto are not optional features — they're the difference between a trading system and a donation.
+5. **Real-money risk.** The bounty build is paper-only and has no connected production trading adapter or credential. Any later real-money work remains behind the Phase 3 gate, Deborah's explicit authority, bankroll limits, kill switches, and the deterministic risk-manager boundary.
6. **World Cup ends July 19.** Post-hackathon, the system must retarget to club football (TXLine paid tiers, USDT-priced) — architecture must be competition-agnostic from day one.
---
diff --git a/docs/02-architecture.md b/docs/02-architecture.md
index 1967baa..31367c2 100644
--- a/docs/02-architecture.md
+++ b/docs/02-architecture.md
@@ -1,6 +1,8 @@
# 02 — Architecture: The Samaritan Harness
-*Design doc, v0.2 (July 10, 2026). Status: LOCKED FOR V1 — updated from Phase 0/0.5 ground truth.*
+*Design doc, v0.4 (July 18, 2026). Status: target architecture plus an explicitly identified bounty slice. Corrected v2 is registered for forward paper observation only; no qualifying v2 fixture or real-Claude lifecycle has yet been admitted.*
+
+> **Implementation boundary:** the bounty build currently ships the canonical live/replay data path, three deterministic detectors, single-turn Haiku triage, a single-turn Opus structured evaluator over a code-assembled signal bundle, deterministic paper risk/execution, a v2 local decision ledger, and offline-safe receipt/anchor tooling. The read-only analyst tools, risk-agent judgment pass, strategy tournament, real-money adapter, Data Doctor, and submitted Solana anchor remain roadmap work and must not be described as implemented.
## Design thesis
@@ -18,15 +20,15 @@ Everyone else connecting Claude to a sports API will build: `odds → prompt →
│ signals (rare, structured)
┌───────────────▼──────────────────────────────────────────┐
│ LAYER 2 — REASONING (Claude, invoked on signal only) │
- │ triage agent (Haiku) → analyst agent (Opus 4.8 + tools) │
- │ → structured TRADE THESIS → risk manager (veto power) │
+ │ triage (Haiku) → analyst (Opus 4.8, bounded bundle) │
+ │ → structured TRADE THESIS → deterministic paper risk │
└───────────────┬──────────────────────────────────────────┘
│ approved tickets
┌───────────────▼──────────────────────────────────────────┐
│ LAYER 3 — EXECUTION & SCORING │
│ strategy tournament (paper bankrolls, CLV/Brier scored) │
- │ execution adapters: PAPER │ POLYMARKET CLOB (real $) │
- │ decision ledger → hashed → anchored on Solana │
+ │ bounty adapter: PAPER; real CLOB remains gated roadmap │
+ │ decision ledger → hashed → receipt; anchor is human-gated│
└──────────────────────────────────────────────────────────┘
```
@@ -58,22 +60,18 @@ Polymarket Match Result is represented as three binary conditions per match (hom
## Layer 2 — Claude reasoning
-**Triage agent — `claude-haiku-4-5`.** First contact for every signal: dedupe (the same move seen through multiple outcomes = one case), classify against known context, kill obvious noise. Cheap enough to run on every signal. Output: drop / escalate, with a one-line rationale.
+**Triage agent — `claude-haiku-4-5` target runtime.** First contact for every admitted signal: dedupe (the same move seen through multiple outcomes = one case), classify against known context, kill obvious noise. Output: drop / escalate, with a one-line rationale. The public complete-lifecycle case uses a deterministic Haiku-shaped stub. Corrected v2 is registered, but real Anthropic admission still fails closed unless a fresh fixture passes every mapping, capture, lifecycle, timing, and immutable-input gate.
-**Analyst agent — `claude-opus-4-8`, adaptive thinking, prompt-cached system context.** Gets escalated cases with the evidence bundle. Tools (strict schemas):
-- `query_series` — interrogate our own time-series store (odds history, features, comparable past cases)
-- `get_match_state` — live score/state for the fixture
-- `web_search` — team news, lineups, injury reports; the *cause* behind unexplained moves
-- `get_polymarket_book` — depth/spread on the target market (is the edge actually captureable after slippage?)
-- `submit_thesis` — the only exit: a structured trade thesis
+**Analyst agent — implemented bounty slice.** `claude-opus-4-8` receives the strict triage decision plus one code-assembled detector signal/evidence bundle and may emit only `submit_thesis`. It has adaptive thinking, fixed input/output/time bounds, schema validation, identity checks, deterministic timestamps, and append-only cost accounting. It does **not** currently expose `query_series`, `get_match_state`, `web_search`, `get_polymarket_book`, or episodic-search tools. Those remain a post-bounty extension unless implemented and demonstrated before release.
**Trade thesis (strict schema — this is the API between AI and money):**
```
-{ market, venue, side, fair_prob, market_prob, edge_bps, confidence,
- thesis: string, invalidation_condition: string, ttl_seconds,
- suggested_stake_fraction, strategy_attribution }
+{ schemaVersion, signalId, fixtureId, marketKey, outcome, direction,
+ recommendation: "paper_trade" | "no_trade", fairProbability,
+ thesisSummary, evidenceFor[], steelmanAgainst, invalidationConditions[],
+ submittedAtTsMs, expiresAtTsMs, analystModel }
```
-No thesis, no trade. Free-text never touches the execution path.
+No thesis, no trade. The thesis contains no stake, order, venue credential, wallet, or execution field; deterministic code downstream owns the paper stake and every execution decision. Free text never becomes an order.
**Risk manager — separate agent + hard-coded rule layer, veto power.** Deliberately adversarial split: the agent that wants the trade never sizes the trade. Two-stage: (1) deterministic hard rules that no LLM can override — max exposure per fixture, per strategy, per day; fractional Kelly cap; correlation limits (no stacking correlated positions across a match's markets); real-money kill switch; (2) an agent pass for judgment calls (does this thesis actually support this size? is the invalidation condition checkable?). Rejections are logged with reasons — they're training data for the playbook.
@@ -83,20 +81,17 @@ No thesis, no trade. Free-text never touches the execution path.
## Layer 3 — Execution, tournament, ledger
-**Strategy tournament (agent-vs-agent).** Strategy personas run in parallel on the same signal stream, each with an isolated paper bankroll: `Momentum` (follow confirmed StablePrice regime moves into lagging Polymarket prices), `Fader` (fade Polymarket moves StablePrice did not confirm), `Arb` (persistent cross-market divergence capture), `Modeler` (in-play model-vs-market). Personas differ in signal consumption and analyst instructions — same harness, different priors. Continuous scoring: **CLV**, **Brier score**, realized paper P&L, and drawdown. A head-trader loop reallocates virtual capital toward what's working; paper capital only in v1.
+**Strategy tournament — target architecture, not bounty-release functionality.** The intended personas would run in parallel on the same signal stream with isolated paper bankrolls: `Momentum`, `Fader`, `Arb`, and `Modeler`. The current evidence accepts only a narrow totals candidate; it does not ship or claim a four-persona tournament or head-trader allocation loop.
-**Execution adapters — one interface, three implementations:**
-1. `PaperAdapter` — fills at observed market price + modeled slippage; always available; the replay-mode default.
-2. `PolymarketAdapter` — real orders via CLOB **V2** SDK. Real-money flow is gated: only pre-match theses that pass both risk stages, carry `venue: polymarket`, use a human-confirmed mapping, and fit within every bankroll/exposure/minimum-order constraint. Kill switch halts it globally. If no valid order fits, no trade is the correct output.
-3. `ReplayAdapter` — used by the backtest runner; fills against historical series.
+**Execution adapter boundary.** The bounty release implements only the deterministic `PaperAdapter`, including depth-aware fill/no-fill behavior. Captured replay feeds that same paper adapter rather than a separate strategy path. A Polymarket CLOB **V2** real-order adapter and any distinct historical-fill adapter remain gated roadmap work; no trading credential or real-order route is connected.
-**Decision ledger + on-chain anchoring.** Every thesis, veto, fill, and outcome is an append-only ledger row. Periodically (per match / per hour), the ledger segment is hashed and the hash written to Solana via a memo tx. Inbound TXLine data we acted on is spot-validated against the Merkle-proof endpoints. Result: a **provably un-cherry-picked track record** on verified data — "proof of alpha." This is the crypto-native feature that fits the judges, and post-hackathon it's the trust layer if we ever sell signals.
+**Decision ledger + on-chain anchoring.** Every thesis, veto, fill, and outcome is an append-only ledger row. The target proof path hashes a versioned canonical record and writes the verified segment head to Solana, while inbound TXLine evidence is spot-validated where the official proof endpoints permit it. Local hash-chain integrity, deterministic replay parity, and external timestamping are reported as distinct guarantees. None of them alone is called proof of alpha; profitability requires separate valid evidence.
**Replay/backtest harness.** The rescued archive supplies five-minute TXLine odds/scores plus one-minute Polymarket sampled prices. The replay engine re-emits normalized records onto the same bus as live data; the stack runs unchanged. Historical Polymarket `t,p` points have no bid/ask/depth and therefore support signal/CLV research with conservative cost proxies, not claims of executable fills. Synchronized live books provide the spread/depth evidence. Outputs: per-strategy signal CLV, conservative simulated P&L, detector precision/recall, and calibration plots.
-**Ops watchdog ("Data Doctor" — credit: rival memo).** A cheap scheduled agent pass over feed health: SSE gap detection, schema drift vs the OpenAPI spec, latency measurements (event `Ts` vs receive time), missing-fixture checks. Trading halts automatically on feed degradation — a trading system that doesn't know its data is bad is a donation machine.
+**Ops watchdog ("Data Doctor") — target architecture.** A future scheduled agent pass would inspect SSE gaps, schema drift, latency, and missing fixtures. The current capture supervisor has deterministic preflight, stale-stream, process, and deadline guards; it does not claim the Data Doctor agent exists.
-**Demo constraint (binding, from verified hackathon terms):** judges must be able to evaluate with **zero cost and zero wallets** — so the submission surface is a hosted dashboard with our TXLine credentials server-side, a bundled replay dataset, and read-only access to the live board. The Solana anchoring runs on OUR wallet; judges just see the explorer links.
+**Demo constraint (binding, from verified hackathon terms):** judges must be able to evaluate with **zero cost and zero wallets**. The deployed [judge MVP](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/) is a derived-only, read-only dashboard backed by frozen public artifacts and an aggregate operational pulse; it requires no credentials. There is no submitted Solana transaction or explorer link; anchoring stays optional and human-gated.
---
@@ -108,8 +103,8 @@ No thesis, no trade. Free-text never touches the execution path.
| AI | `@anthropic-ai/sdk` — Opus 4.8 (adaptive thinking) + Haiku 4.5; strict tools; structured outputs; prompt caching | Custom loop, purpose-built for the triage→analyst→risk pipeline. Not the coding-agent SDK — this harness IS the product |
| Storage | SQLite (better-sqlite3) or DuckDB | Append-only time series at modest volume; zero infra |
| Bus | In-process typed EventEmitter → upgrade path to Redis streams later | One process is plenty for the World Cup; don't build Kafka for 15 matches |
-| Dashboard | Lightweight web UI (live signals, agent reasoning feed, tournament leaderboard, CLV curves) | The demo surface for judges |
-| Chain | Solana mainnet (TXLine SL12 subscription + ledger anchoring), devnet for integration tests | Decision locked |
+| Dashboard | Read-only derived evidence UI; broader live/tournament views are roadmap | The current judge surface separates real research from the synthetic lifecycle |
+| Chain | Solana mainnet for the existing TXLine SL12 subscription; optional devnet receipt anchoring remains unsubmitted | Current decision receipts are local and visibly unanchored |
## Post-hackathon path
diff --git a/docs/03-strategy-playbook.md b/docs/03-strategy-playbook.md
index 812ae92..98af19b 100644
--- a/docs/03-strategy-playbook.md
+++ b/docs/03-strategy-playbook.md
@@ -1,6 +1,6 @@
# 03 — Strategy Playbook
-*v0.2 (July 10, 2026). Status: V1 STRATEGIES LOCKED; numeric thresholds remain unset until the Phase-3 gate.*
+*v0.4 (July 18, 2026). Status: STRATEGY DESIGN SPEC. The four personas remain the intended tournament roster, not a claim that all four currently run. The corrected historical gate supports only a Total Goals `CONSENSUS_MOVE` candidate; v2 is registered for forward paper observation only, STALE_QUOTE remains disabled, and MODELER is not implemented.*
## First principles (why anything here works at all)
@@ -86,7 +86,7 @@ Effectively the mirror image of CONSENSUS_MOVE, sharing its infrastructure: `Pol
- **Fractional Kelly.** Stake = bankroll × kelly_fraction × edge/odds, with kelly_fraction ≤ 0.25 (quarter-Kelly) — full Kelly is ruin-adjacent under estimation error, and our edges ARE estimates. Per-trade hard cap regardless of formula output.
- **Exposure caps:** per fixture, per market type, per strategy, per day. Correlated-position rule: positions on the same fixture across markets count toward one bucket.
-- **Real-money gates (execution = BOTH paper and real, per decision):** $50 total bankroll ceiling; no more than $15 aggregate live exposure initially; pre-match only; thesis + deterministic rules + risk judgment + human-confirmed mapping required. Per-trade and drawdown caps are frozen at the Phase-3 review. If Polymarket's minimum order exceeds any cap, Samaritan does not trade.
+- **Bounty execution is paper-only:** the implemented paper configuration uses a $50 reference bankroll, no more than $15 aggregate paper exposure, pre-match admission, a strict thesis boundary, deterministic rules, and a Deborah-reviewed mapping. Any future real-money adapter remains outside the bounty proof and would additionally require the completed Phase-3 gate, fresh explicit authority, frozen per-trade/drawdown caps, and the manual kill switch. If a venue minimum exceeds any cap, the system must refuse the order.
- **Drawdown circuit breakers:** strategy paused at X% paper drawdown; real-money halted entirely at Y% — X, Y to be agreed.
- **No martingale, no chasing, ever.** Stake sizing is a pure function of (edge, bankroll); losses don't change the function.
diff --git a/docs/04-algorithms.md b/docs/04-algorithms.md
index 98726e0..8eb4667 100644
--- a/docs/04-algorithms.md
+++ b/docs/04-algorithms.md
@@ -56,6 +56,8 @@ e_exec,o(t) = C_o(t) − PM_ask,o(t) − fees − slip(D(t), stake)
## 3. STALE_QUOTE — post-event stale orders (in-play; paper-only in v1)
+**Current gate evidence (Spain-Belgium, July 10).** At an exploratory 50-probability-bps move threshold, 0/18 goal/market cases showed a clean post-TXLine stale window: 12 had already moved before TXLine first delivery and 6 showed no material move within 30 seconds. This detector is disabled. The specification below remains a measurement definition for additional captures, not an enabled signal.
+
**Setup.** Score event `E` at `t0` (goal or red card — the two largest instant repricers). Model jump: `Δp_model = p_model(post-state) − p_model(pre-state)` from MODELER's state machine (available instantly — it's a lookup on the fitted state grid, no fitting at trade time).
**Trigger:** at `t0 + δ`: the synchronized live PM book still shows resting orders within `θ_stale` of pre-event prices, size ≥ `s_min`, on the side that the event moved against. `δ` is fitted from live evidence, never assumed to be single-digit.
diff --git a/docs/05-harness.md b/docs/05-harness.md
index d351c3a..fb197d1 100644
--- a/docs/05-harness.md
+++ b/docs/05-harness.md
@@ -1,6 +1,8 @@
# 05 — The Agent Harness
-*v0.2 (July 10, 2026). Status: V1 HARNESS DECISIONS LOCKED. This is the machine room: deterministic context assembly, strict tools, bounded agent stages, persistent memory, and hooks that code—not the model—enforces.*
+*v0.3 (July 14, 2026). Status: target harness with the implemented bounty slice identified below.*
+
+> **Implementation boundary:** the current analyst is one bounded Opus call over the detector signal bundle and its only tool is `submit_thesis`. The multi-call investigative loop, pull-based evidence tools, episodic retrieval, risk-agent pass, debate council, learned-memory workflow, and Data Doctor are design targets, not current submission claims. The implemented path is deterministic scheduling, strict Haiku/Opus outputs, paper risk/execution, portfolio lifecycle, restart reconstruction, spend/decision ledgers, and a synthetic end-to-end proving case.
## 0. The core abstraction: the CASE
@@ -33,14 +35,16 @@ while case.alive:
invariants.check(case) # hooks — code, unoverridable
```
-Within an agent stage (e.g. INVESTIGATING), the analyst runs a bounded tool-use loop: max `K` tool calls, max token budget, hard wall-clock deadline derived from the case TTL. Exceeding any bound ends the stage with whatever the model has — a late thesis is a dead thesis, so the deadline is real.
+The implemented INVESTIGATING stage is deliberately smaller: one bounded call, one strict exit tool, a maximum output budget, and a hard request timeout. A future multi-call loop may add a maximum `K`, but it is not part of the bounty runtime.
+
+The paper runtime splits judgment from execution. It appends `signal_received` before model I/O, measures real wall-clock time through triage and analysis, maps completion back onto event time, and appends `analysis_completed`. Only then can the first later canonical book become an execution candidate. Model-supplied timestamps are replaced by deterministic completion/expiry stamps, so an analyst cannot shorten reported latency or extend its own validity window.
**Stage table:**
| Stage | Executor | Bounded by | Exit |
|---|---|---|---|
-| TRIAGE | Haiku 4.5, single turn, no tools | 1 call | drop / escalate(+priority) |
-| INVESTIGATING | Opus 4.8, tool loop | K calls, tokens, deadline | `submit_thesis` or `no_trade` |
+| TRIAGE | Haiku 4.5, single turn, submission tool only | 1 call, 512 output tokens, 60 s | drop / escalate(+priority) |
+| INVESTIGATING | Opus 4.8 adaptive thinking, bounded evidence bundle | 1 call initially, 8,192 output tokens, 180 s | `submit_thesis` carrying paper-trade or no-trade |
| RISK_REVIEW | Rules engine (code) → Opus judgment pass | 1 call | veto / resize-down / approve |
| EXECUTING | Adapter (code only) | price-tolerance recheck | filled / expired |
| POST_MORTEM | Batch job (Haiku/Opus, off-peak) | daily budget | lesson entries |
@@ -59,7 +63,7 @@ The single biggest quality-and-cost lever. The model never "goes and fetches eve
- **k nearest historical cases** (same detector, similar market/state) with their outcomes and CLV — retrieved from the ledger by feature similarity. This is episodic memory doing real work: the analyst sees how prior CONSENSUS_MOVE cases with similar magnitude/persistence behaved.
4. **Freshness stamp:** every evidence item carries its timestamp; the thesis must reference evidence ≤ TTL-fresh, enforced at submit.
-Tools available during INVESTIGATING are **pull-based extensions** of the bundle, not replacements: `query_series` (ad-hoc slices of our store), `get_match_state`, `get_polymarket_book` (live re-read), `web_search` (news/lineups — the only external tool, allowlisted), `find_similar_cases` (deeper episodic search), `submit_thesis` / `no_trade` (the only exits).
+The current INVESTIGATING call receives the signal and triage output assembled by code. Its only tool is `submit_thesis`, whose recommendation is either `paper_trade` or `no_trade`. Proposed pull-based extensions—`query_series`, `get_match_state`, `get_polymarket_book`, `web_search`, and `find_similar_cases`—are not wired into the current runtime.
## 3. Deliberation design — when agents debate (the two-speed answer)
@@ -92,8 +96,9 @@ One mechanism, honest accounting: debate is a *governance* tool, not a *trading*
- **Priority queue** over live cases: `priority = f(expected_edge × stake_capacity, ttl_remaining)`. V1 concurrency cap is **3** concurrent INVESTIGATING cases.
- **Preemption:** a score event on a fixture instantly invalidates that fixture's in-flight cases back to TRIAGE (the world changed; the evidence is stale). This is a hook, not a model decision.
-- **Token budgets:** per-case caps (triage ~500 out; investigation ~8K out), per-day controls, **$200 operating target**, and **$300 hard project ceiling**. Degradation ladder: raise triage escalation bar → shrink K → pause lowest-CLV persona → detectors-only mode. At the hard ceiling all new model investigations stop; signals continue to be logged and no model-dependent real trade can proceed.
+- **Token budgets:** per-case caps (triage 512 out; investigation 8,192 out), per-day controls, **$200 operating target**, and **$300 hard project ceiling**. An append-only SQLite spend ledger atomically reserves worst-case request cost before network I/O, settles measured usage afterward, and conservatively charges the reservation when billing is unknown. Degradation ladder: raise triage escalation bar → shrink K → pause lowest-CLV persona → detectors-only mode. At the hard ceiling all new model investigations stop; signals continue to be logged and no model-dependent real trade can proceed.
- **Prompt-cache discipline:** static prefix byte-stable per deploy; per-case content strictly after the cache breakpoint; cache hit rate is a monitored metric (a silent invalidator is a 10× cost bug).
+- **Clock discipline:** market/book freshness is checked in canonical event time; public fee-metadata freshness is checked in processing wall time. Neither clock is rewritten to make replay pass.
## 6. Hooks — the unoverridable layer
diff --git a/docs/06-gate-study.md b/docs/06-gate-study.md
new file mode 100644
index 0000000..a9b8b1a
--- /dev/null
+++ b/docs/06-gate-study.md
@@ -0,0 +1,107 @@
+# Phase 3 Gate Study
+
+*Drafted July 11, 2026. Invalidated for decision use on July 14 after a causal-selector audit. The original measurements remain below as an audit record, not evidence of edge. This report does not authorize paper promotion or real-money execution. See `docs/research/historical-gate-study-v1-invalidation.md`.*
+
+## Decision
+
+The real-money gate remains **closed**. The corrected rerun supports preparing a fresh Total Goals–only forward paper protocol for Deborah's review, but no paper study is active or registered.
+
+The v1 historical lane does not currently support promoting any detector. Its dynamic-total market was selected with a Polymarket observation after the last eligible signal time for 95 of 98 fixtures; coverage was also counted without the same cutoff. The reported signal sample additionally mixed unsupported sells and complementary expressions of the same economic move. The synchronized Spain-Belgium evidence remains valid for its narrow stale-quote feasibility conclusion because it does not depend on the historical selector.
+
+| Lane | Evidence decision | Reason |
+|---|---|---|
+| `CONSENSUS_MOVE`, dynamic total | **Registered for forward paper observation** | Corrected v4: 38 held-out normalized Total Goals buys / 18 fixtures; `+132.7 bps` after the 100 bps proxy; fixture-clustered 95% CI `+14.3` to `+243.9 bps`. Sampled prices still cannot prove fills or profitability; Deborah registered v2 on July 18 under a closed real-money gate |
+| `CONSENSUS_MOVE`, Match Result | No-go for v1 | 34 heldout signals; mean after-cost CLV `-67.0 bps` |
+| `XMARKET_DIVERGENCE` | No-go for v1 | Overall mean after-cost CLV `-2.1 bps`; Match Result was `-15.2 bps`. The positive total slice does not justify promoting a detector whose sealed aggregate result failed |
+| `FADER_CANDIDATE` | No-go for v1 | Overall mean after-cost CLV `-46.0 bps`, with both market families negative |
+| `STALE_QUOTE` | Disabled | The synchronized Spain-Belgium study found zero clean post-TXLine repricing windows in 18 market-event cases |
+| `MODEL_MARKET_GAP` | Not evaluated | No fitted goal model or calibration certificate exists yet |
+
+The prior paper approval remains suspended. The corrected selector, economic-case definition, versioned rerun, and candidate protocol draft now exist, but only Deborah can register a fresh forward study. Historical Polymarket observations are sampled prices, not executable books, so even the corrected historical study cannot establish spread, slippage, fill probability, or settlement correctness.
+
+## Corrected V4 Result — Research Evidence Only
+
+Protocol `historical-gate-causal-economic-v4-2026-07-14` preserves the 68/30 chronological split and records configuration SHA-256 `9a4eeff928f697fc55ab5147a4dc07f611c40bb749501fc3bd92b211f24b2e54` plus hashes of the archive, mappings, and causal selector evidence.
+
+- All 590 selector rows pass the causal audit: zero future probabilities, future coverage rows, late selector cutoffs, or nonzero untimestamped liquidity/volume inputs.
+- The training-selected `CONSENSUS_MOVE` configuration had 135 normalized Total Goals training cases before held-out Total Goals review.
+- Held-out Total Goals produced 38 normalized buy cases across 18 fixtures.
+- Mean directional CLV was `+232.7 bps`; after the fixed 100 bps historical proxy it was `+132.7 bps`.
+- The 10,000-iteration whole-fixture bootstrap interval was `+14.3` to `+243.9 bps` after the proxy.
+- The aggregate detector, including Match Result, did not pass its clustered interval; Match Result stays rejected.
+- No model calls occurred in the deterministic historical replay, so model operating cost was `$0.00`; a forward paper case must report inference economics separately.
+
+This result clears only a historical signal threshold for a **fresh forward paper review**. It is not an executable backtest: the Polymarket observations are sampled prices without bid/ask, depth, or fill probability. The proposed replacement protocol is documented at `docs/09-paper-study-v2-candidate.md` and remains deliberately unsigned.
+
+## Historical Lane — Invalidated V1 Audit Record
+
+The study used 98 mapped World Cup fixtures and selected configurations only on a chronological training partition. Identical kickoff timestamps were kept in the same partition.
+
+- Training: 68 fixtures, 136/136 source-aligned market groups, 164,655 canonical events, and 227,368 feature snapshots.
+- Heldout: 30 fixtures, 52/60 source-aligned market groups, 60,092 canonical events, and 77,445 feature snapshots.
+- Window: final three hours before kickoff through kickoff; in-running TXLine records excluded.
+- Labels: 15-minute forward horizon and 25 probability-bps minimum movement.
+- Selection: labeled training F1 with at least 30 predicted-positive training cases and detector gaps no smaller than the 100 probability-bps cost floor.
+- Cost handling: 100 probability bps subtracted per signal, with 50/150/200 bps sensitivity checks.
+
+The four fixtures missing from heldout source alignment were France-Morocco (`18209181`), Spain-Belgium (`18218149`), Norway-England (`18213979`), and Argentina-Switzerland (`18222446`). Missing groups were excluded rather than imputed.
+
+### Original Sealed Results — Not Valid for Decision Use
+
+| Detector | Train-selected candidate | Heldout precision | Heldout recall | CLV signals | Fixtures | Raw CLV | Net at 100 bps |
+|---|---|---:|---:|---:|---:|---:|---:|
+| `XMARKET_DIVERGENCE` | gap `100 bps`, persistence `0s`, stable z `< 1` | 50.0% | 1.9% | 372 | 25 | `+97.9 bps` | `-2.1 bps` |
+| `CONSENSUS_MOVE` | z `>= 1`, CUSUM `10 bps`, gap `100 bps`, updates `5` | 50.5% | 0.6% | 107 | 23 | `+159.9 bps` | `+59.9 bps` |
+| `FADER_CANDIDATE` | PM z `>= 1`, gap `100 bps`, persistence `0s`, stable z `< 1` | 5.6% | 0.1% | 72 | 16 | `+54.0 bps` | `-46.0 bps` |
+
+These are the original v1 outputs retained for traceability. They must not be described as held-out edge, alpha, or an executable opportunity. The causal-selector and economic-case defects require a full rerun rather than a numerical adjustment.
+
+### Cost Sensitivity
+
+| Detector | 50 bps | 100 bps | 150 bps | 200 bps |
+|---|---:|---:|---:|---:|
+| `XMARKET_DIVERGENCE` | `+47.9` | `-2.1` | `-52.1` | `-102.1` |
+| `CONSENSUS_MOVE` | `+109.9` | `+59.9` | `+9.9` | `-40.1` |
+| `FADER_CANDIDATE` | `+4.0` | `-46.0` | `-96.0` | `-146.0` |
+
+These values are mean directional signal CLV after each assumed cost. They are sensitivity estimates, not historical P&L.
+
+## Dynamic Total — Defective V1 Method
+
+V1 selected the exact mapped full-time total closest to 50/50 using a point as late as five minutes before kickoff, while eligible signals ended at kickoff minus fifteen minutes. That is future-informed market selection. Its 1,000-point coverage calculation was also not filtered to the same cutoff. The following distribution is retained only to identify the invalidated artifact:
+
+| Line | Fixtures |
+|---|---:|
+| O/U 1.5 | 5 |
+| O/U 2.5 | 77 |
+| O/U 3.5 | 15 |
+| O/U 4.5 | 1 |
+
+Volume and liquidity were intentionally unweighted because the captured Gamma values are post-close and zero for most lines. No part of this selector is a production or paper default. The corrected selector must use only evidence available before detector evaluation begins and must apply one as-of cutoff to every eligibility input.
+
+## Live Lane
+
+The synchronized Spain-Belgium capture normalized 2,262,950 Polymarket book events across Match Result and five exact full-time totals. Across three unique goals and six market groups, Polymarket had already moved in the five seconds before first TXLine delivery in 12/18 cases. The remaining 6/18 had no material move within 30 seconds. No case showed a clean post-TXLine stale window without a prior Polymarket move.
+
+Eight public-WebSocket outages totaling 91.171 seconds were excluded rather than classified as unchanged markets. This evidence disables `STALE_QUOTE`; it does not prove that such a window can never occur.
+
+## Pending Gate Work
+
+The following items block any signed-off Phase 3 gate:
+
+1. Deborah must review the corrected v4 evidence and either register or reject the unsigned v2 paper candidate.
+2. Only fresh post-signature v2 ledgers may pursue the 20-match/40-fill stopping rule without endpoint peeking.
+3. Polymarket settlement rules and every live mapping need human confirmation.
+4. `MODEL_MARKET_GAP` needs a fitted group-stage model and Brier comparison against the market.
+5. Any real-money proposal needs separate per-trade/drawdown approval; the locked `$3` stake and `$20` stop apply only to paper validation.
+
+Until all required review items pass, thresholds remain research-only, no real-money adapter may act on them, and the paper components may be used only for clearly labelled engineering or bounty demonstrations—not a qualifying profitability study.
+
+## Evidence
+
+- Historical detector report: `docs/research/historical-gate-study.md`
+- Historical machine-readable output: `data/research/historical-gate-study.json`
+- Corrected causal/economic report: `docs/research/historical-gate-study-causal-economic-v4.md`
+- V3→V4 methodology note: `docs/research/historical-gate-study-v3-to-v4-methodology-note.md`
+- Synchronized live report: `docs/research/paired-spain-belgium-2026-07-10-live-lane.md`
+- Synchronized live machine-readable output: `data/research/paired-spain-belgium-2026-07-10-live-lane.json`
diff --git a/docs/07-paper-study-preregistration.md b/docs/07-paper-study-preregistration.md
new file mode 100644
index 0000000..17e179b
--- /dev/null
+++ b/docs/07-paper-study-preregistration.md
@@ -0,0 +1,185 @@
+# Paper-Study Pre-Registration — Totals-Only CONSENSUS_MOVE
+
+*Registered July 11, 2026 and signed by Deborah July 12. **Invalidated and suspended July 14 before producing any paper observation:** its upstream historical market selector used information later than the eligible signal window. This document is preserved as the immutable v1 audit record; it must not admit new cases. A corrected study requires a new protocol ID and signature. The real-money gate remains **closed**.*
+
+The corrected implementation was reviewed separately and registered for forward paper observation only on July 18, 2026 as `paper-study-v2-2026-07-18`; see [`10-paper-study-v2-registration.md`](10-paper-study-v2-registration.md). That decision does not repair or reactivate this v1 protocol, copy any v1 row forward, admit a fixture, or unlock real money.
+
+## Purpose
+
+The historical gate study originally appeared to produce a pre-match `CONSENSUS_MOVE` candidate on a dynamically selected full-time total. A July 14 audit found that the selector chose the market using a price observed after the last eligible signal for 95 of 98 fixtures. The v1 sample also mixed unsupported sells and complementary expressions of the same economic move. Its previously reported positive measurements are therefore audit history, not valid held-out edge evidence.
+
+That number rests on three proxies that a backtest cannot remove:
+
+- entry at a **sampled Polymarket midpoint** rather than an executable ask/bid,
+- fair value proxied by the **bookmaker consensus close** rather than the real Polymarket close/settlement,
+- a **flat 100 bps** stand-in for spread, slippage, fees, and fill risk.
+
+This v1 paper study is suspended and cannot receive new cases. Its ledgers remain preserved. A v2 registration may be created only after the causal selector and executable-case definition are fixed and the historical study is rerun without using future information.
+
+## Locked candidate (immutable for this study)
+
+No parameter below may be re-tuned during or after the run. Any change voids this registration and requires a new one.
+
+### Detector — `CONSENSUS_MOVE`, totals markets only
+
+```json
+{
+ "velocityWindowMs": 300000,
+ "consensusMoveAbsZ": 1,
+ "consensusCusumThreshold": 0.001,
+ "consensusMinimumUpdates": 5,
+ "consensusMinimumRawGap": 0.01,
+ "consensusStableAbsZ": 1
+}
+```
+
+- Runs **only** on the `total_goals:full_time` market selected by the dynamic-total selector below.
+- `XMARKET_DIVERGENCE`, `FADER_CANDIDATE`, match-result `CONSENSUS_MOVE`, `STALE_QUOTE`, and `MODEL_MARKET_GAP` stay disabled / research-only. They are **not** in this study.
+
+### Dynamic-total selector
+
+```json
+{
+ "minimumCoveragePoints": 1000,
+ "minimumVolume": 0,
+ "minimumLiquidity": 0,
+ "maximumDistanceFromEven": 0.15,
+ "weights": { "balance": 1, "volume": 0, "liquidity": 0, "coverage": 0 }
+}
+```
+
+Selects the exact mapped full-time total whose pre-kickoff Over probability is closest to 0.50, at least five minutes before kickoff; fails closed if no eligible line is within 0.15 of even. **Note:** liquidity is unweighted only because historical Gamma depth was post-close/zero. The paper study must record the live executable depth of the line it picks (see Guardrails) so we learn whether "most balanced" is also tradeable.
+
+### Feature and forward-label config
+
+```json
+{
+ "feature": {
+ "velocityWindowsMs": [60000, 300000, 900000],
+ "velocityEwmaHalfLifeMs": 900000,
+ "cusumDriftProbability": 0.0005,
+ "scoreContextWindowMs": 300000,
+ "freshnessMaxAgeMs": 360000
+ },
+ "label": { "horizonMs": 900000, "minimumGapClosure": 0.0025 },
+ "window": { "beforeKickoffMs": 10800000, "includeInRunning": false }
+}
+```
+
+Signals are eligible only when detected at least 15 minutes before kickoff (one forward-label horizon), pre-match, in-running rows excluded — identical to the study that produced the candidate.
+
+## What the paper study must replace
+
+| Proxy in the backtest | Reality the paper study must use |
+|---|---|
+| Sampled Polymarket midpoint as entry | A conservative simulated marketable order against the first canonical book observed after measured signal-to-order latency: buy by walking **asks**, sell only against owned inventory by walking **bids** |
+| Consensus close as fair value | The real Polymarket **closing midpoint at kickoff** as the primary CLV reference, directional closing bid/ask as a supporting executable-liquidation mark, and actual **settlement** where available |
+| Flat 100 bps cost | Measured spread and book-walk slippage plus the official per-market fee parameters loaded and recorded at runtime |
+| Assumed 100% fill | Depth- and latency-aware fills, including partial/no fills; feed outages, insufficient depth, stale books, and invalid mappings are no-trades rather than wins |
+
+The harness extends the existing live-lane path (production normalizers, reconstructed top-of-book and best-level depth, explicit reconnect-window exclusion) already used in `src/research/paired-live-study.ts`.
+
+## Sample and stopping rule
+
+- **Unit of analysis is the match**, not the signal. Signals inside one match are correlated; they do not count as independent bets.
+- **Primary plan:** run forward on live mapped fixtures as they occur. Where live match supply is insufficient, the study may extend to the next mapped competition or replay **captured live order books** (not sampled history), but the executable-price and depth requirements above still apply.
+- **Minimum to decide:** ≥ **20 matches** that each produce ≥ 1 executed paper signal, **and** ≥ **40 executed signals** total. **Target: 30 matches.**
+- **No peeking:** primary endpoints are not computed or inspected until the minimum is reached. If anyone reads interim P&L to decide whether to continue, the run is disclosed as exploratory and does not count toward the gate.
+
+### Two separate lanes
+
+- **Bounty demonstration lane:** runs through submission using remaining live fixtures and captured-book replay to demonstrate the complete autonomous product. Its individual trades and metrics are explicitly exploratory and excluded from the registered profitability decision because the demo must expose them before the minimum sample exists.
+- **Long-run profitability lane:** starts with a fresh ledger and unseen fixtures under this unchanged protocol. It alone can satisfy the 20-match/40-fill stopping rule. It continues after the hackathon if necessary.
+- The long-run fixture universe is every eligible mapped full-time total exposed through Samaritan's lawful TXLine access after the lane starts. Any competition expansion requires a dated protocol addendum before its first included fixture and cannot be chosen using observed strategy results.
+
+## Primary endpoint and decision rule
+
+The primary endpoint is **executable CLV**, not realized settlement P&L. On ~20–30 matches, binary settlement P&L is too high-variance to gate a decision; CLV against the executable close is the standard, higher-power leading indicator.
+
+**Executable CLV per signal** = (Polymarket closing midpoint at kickoff − simulated fill price) in the traded direction, net of measured entry fees and book-walk slippage. Directional closing bid/ask CLV is reported separately as a supporting liquidation-cost sensitivity check; it is not called a midpoint.
+
+**ACCEPT (advance to a real-money discussion — not to real money) requires all of:**
+
+1. Mean executable CLV net of costs `> 0`, with the **lower bound of the match-clustered 95% CI `> 0`**.
+2. Mean realized settlement P&L (where markets resolved) `> 0` — reported with its CI, not required to reach significance.
+3. Strategy net result **beats the no-trade baseline (0)** and a **random-direction control**.
+4. All guardrails below satisfied.
+
+**REJECT** if the match-clustered 95% CI for executable CLV includes or sits below 0, or any guardrail fails materially.
+
+**INCONCLUSIVE** if the minimum sample is not reached, or required close/settlement evidence is incomplete — in which case extend the sample under the same registration or shelve the candidate. Inconclusive is not a pass. To resolve the original wording conflict conservatively, after the minimum is reached the explicit REJECT rule above controls: a CLV interval that touches or straddles 0 is a REJECT, not an ACCEPT.
+
+The reproducible random-direction control uses a seeded SHA-256 sign assignment per signal. It applies that sign to gross directional midpoint CLV and subtracts the same observed entry cost as the strategy. This is a matched-cost synthetic control, not a claim that the untraded complement token had identical depth; the seed and method are reported.
+
+## Required outputs (both are gating deliverables)
+
+### 1. Match-clustered confidence interval
+
+Compute the 95% CI by **block bootstrap over matches**, not over signals:
+
+- Resample the set of matches **with replacement**, `B = 10000` iterations (seeded and recorded for reproducibility).
+- Within each resampled match, take all of its signals; recompute the mean per-signal executable CLV across the resample.
+- Report the 2.5th / 50th / 97.5th percentiles of the bootstrap distribution as [CI low, median, CI high].
+- Report the same match-clustered CI for realized settlement P&L.
+
+This is the mechanism that prevents ~20 matches from masquerading as 40+ independent bets. Report the number of matches and signals alongside every interval.
+
+### 2. Per-match P&L table
+
+One row per match, columns:
+
+`fixtureId · kickoff · selected total line · signals · fills · fill rate · mean half-spread bps · mean realized slippage bps · gross CLV bps · net CLV bps · realized settlement P&L ($) · net return (bps)`
+
+Plus a footer with: match count, signal count, fraction of matches net-positive, aggregate net CLV with match-clustered CI, aggregate settlement P&L with CI, and the no-trade / random-direction baseline rows.
+
+### Supporting metrics (report, non-gating)
+
+CLV distribution (mean, median, p25, p75, positive rate); hit rate; **Brier score** of entry-implied probabilities vs outcomes, compared to the market's Brier; equity curve and **max drawdown**; realized vs assumed (100 bps) cost, so we learn whether the historical proxy was optimistic.
+
+## Guardrails (each must hold for ACCEPT)
+
+- **Fill rate ≥ 60%** of eligible signals filled at or through the modeled price at stake size; misses logged, never scored as wins.
+- **Mean realized slippage ≤ 100 bps** (the historical cost proxy). If realized costs run higher, the edge is presumed gone regardless of CLV point estimate.
+- **Max drawdown within the risk cap** below.
+- **Selected-line depth recorded** for every trade, so we can see whether the balance-only selector routinely picks thin books.
+
+Source timestamps may regress slightly in capture order. Such observations are counted and rejected atomically before feature or quote state mutation. Kickoff marks select the latest canonical venue/source timestamp at or before kickoff; source, local observation, and processing timestamps are all retained so delayed delivery is visible.
+
+## Risk caps (paper, mirroring intended live limits)
+
+Paper stakes are **simulated USD notional** — no wallet, no tokens, no chain. The paper study reads real live order books and simulates fills in software; it needs nothing to spend. Stake size still mirrors the intended live limits because slippage and fill probability scale with order size, so the simulation must trade the size we would really trade. (For context: the only devnet in this project is the TXLine **data** feed on Solana used for integration testing and on-chain anchoring; the trading venue, Polymarket, is real-money USDC on Polygon with no test-token equivalent. See the handoff note.)
+
+- Bankroll: **$50** (locked).
+- Aggregate open exposure ceiling: **$15** (locked).
+- Per-trade stake: **$3** (locked; up to five concurrent positions within the $15 ceiling).
+- Max drawdown rejection stop: **$20** (locked; 40% of bankroll). Breaching it halts and rejects that paper lane; only Deborah may authorize a new, separately registered study.
+
+All money values are stored as integer USD micro-units. The analyst cannot propose or modify stake, exposure, or drawdown values; deterministic risk code owns them.
+
+## Fee protocol
+
+The July 12 official Polymarket documentation lists the sports taker fee rate as `0.05`, maker fee as `0`, and the formula `fee = shares × feeRate × price × (1 − price)`. Fees are enabled per market and can change. Therefore Samaritan records the V2 fee rate, exponent, taker-only flag, minimum order size, and tick size returned by `getClobMarketInfo(conditionID)` for every simulated fill; runtime market parameters win over this dated documentation snapshot. Missing, legacy-only, malformed, wrong-token, unsupported-exponent, sub-minimum, or off-grid metadata fails closed. Fees are conservatively rounded to the documented `0.00001 USDC` precision.
+
+## Out of scope / still closed
+
+- **Real money.** ACCEPT here only unlocks a *separate* human decision about tiny real-money validation; it is not that decision.
+- Polymarket **settlement-rule and mapping confirmation** for the totals markets remains a human prerequisite before any real-money step.
+- Other detectors and `MODEL_MARKET_GAP` remain disabled/research-only.
+
+## Locked run inputs
+
+1. Paper stake is `$3`; aggregate exposure is `$15`; the lane is rejected at `$20` drawdown.
+2. Fee parameters are loaded from the official per-market CLOB metadata at runtime and recorded with each fill.
+3. The bounty lane and long-run lane use separate ledgers. The bounty lane may be viewed freely; the long-run primary endpoints remain sealed until its stopping rule is met.
+4. Real money may be discussed only after a long-run `ACCEPT` result and still requires a separate Deborah approval.
+5. The persistent long-run ledger began at `2026-07-12T09:45:17.212Z` under frozen config SHA-256 `7dcb4c20ac05195bdb4a6c8803432079eeefb8cc747c186f576084dc772fd2a2`. Earlier fixtures and captures are bounty/research evidence only and cannot count toward its stopping rule.
+
+## Sign-off
+
+- [x] Deborah accepts the locked candidate, sample rule, and decision thresholds above (July 12, 2026).
+- [x] `$3` paper stake, `$15` aggregate exposure, and `$20` drawdown rejection stop are fixed.
+- [x] Two-lane bounty/long-run protocol is fixed.
+- [x] Real-money gate confirmed **closed** for the duration of this study and until separate post-`ACCEPT` approval.
+
+Registered by: research validation review, July 11, 2026. Signed off by Deborah, July 12, 2026.
+Evidence basis: `docs/06-gate-study.md`, `docs/research/historical-gate-study.md`, `data/research/historical-gate-study.json`.
diff --git a/docs/08-winning-submission-plan.md b/docs/08-winning-submission-plan.md
new file mode 100644
index 0000000..dd41dfa
--- /dev/null
+++ b/docs/08-winning-submission-plan.md
@@ -0,0 +1,835 @@
+# Samaritan Winning Submission Plan
+
+**Status:** Active war-room plan
+**Owner and hackathon participant:** Deborah
+**Internal ship date:** July 18, 2026
+**Hard deadline:** July 19, 2026 at 23:59:59 UTC
+**Real-money gate:** **CLOSED**
+
+This document converts the July 18 product, code, evidence, competition, legal, and submission audit into the shortest credible path to a winning TxODDS Trading Tools and Agents submission.
+
+It is a bounty-critical overlay on `plan.md`, not a replacement for the long-term Samaritan roadmap. It does not authorize real-money trading, relax any risk invariant, or permit raw TXLine data to be published.
+
+## 0. July 18 winner-ready checkpoint
+
+The product is no longer an idea looking for a demo. The core judge story and public no-login deployment are implemented. The remaining release gates are the exact default-branch commit and CI result, the final video, sponsor clarification, and Deborah's human review and submission:
+
+| Surface | Current evidence | Status |
+|---|---|---|
+| Real TXLine use | Mainnet SL12 ingestion and a synchronized Spain–Belgium replay are documented; the public case ends in a disciplined no-trade | **Ready, narrowly claimed** |
+| Shared paper conductor | Live and captured-replay adapters feed the same `runPaperSession` canonical-event conductor without exposing a mode flag to strategies | **Implemented; not a 24/7 deployment claim** |
+| Complete judge lifecycle | `pnpm demo` sends 20 invented canonical events through `runPaperSession` and completes signal → deterministic agent stubs → hard risk → paper fill → close → settlement | **Ready, synthetic and performance-excluded** |
+| Decision proof | The deterministic synthetic lifecycle produces a v2 ledger and offline-verifiable receipt; exact final hashes must be recorded only after the public commit and bundle are frozen | **Ready, local integrity only; final hashes pending freeze** |
+| Claude invocation evidence | Separate append-only hash chain verifies hash-only run metadata locally; a receipt may carry a generation-time reference, but its offline verifier does not independently prove membership | **Local audit only; no provider attestation and no active v2 run** |
+| Public evidence bundle | A strict allowlist of derived artifacts passes the public-data audit during development; exact file count and bundle hash must come from the final frozen export | **Ready for final public-commit audit** |
+| Dashboard | Command, Matchroom, Casebook, Study, and Proof are deployed read-only at the [public judge MVP](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/) | **Deployed; signed-out desktop/mobile routes and API methods verified** |
+| Historical claim | Corrected causal/economic v4 reports 38 held-out normalized buys / 18 fixtures / `+132.7` bps after proxy / clustered interval `+14.3` to `+243.9` | **Signal research only; v2 registered July 18 for forward paper observation** |
+| External anchor | Offline devnet preparation and human-gated tooling exist; no signer, RPC submission, wallet action, or transaction is part of the bounty release | **Intentionally unanchored; no explorer claim** |
+| Capture expansion | France–Spain failed closed before kickoff. England–Argentina also failed closed after a watchdog-detected Polymarket stall and incomplete terminal manifests, so no semifinal paired capture is currently admissible | **No eligible semifinal paired capture; capture-only evidence remains blocked** |
+
+The release candidate has passed root/dashboard typechecks, tests, the deterministic demo, receipt verification, the public-artifact audit, the production dashboard build, dependency gates, isolated Phase 0 checks, and signed-out production-route checks. A clean clone and GitHub CI run on the final public commit remain the authoritative code-release evidence.
+
+The remaining path to submission is deliberately small and evidence-sensitive:
+
+1. Preserve both failed semifinal captures as fail-closed evidence; any later capture remains separate until every registered-study admission gate passes.
+2. Keep registered v2 on fresh versioned ledgers and admit nothing until exact evidence and mapping gates pass.
+3. Preserve the frozen judge-evidence manifest, Proof route, and derived-only TXLine pulse without publishing licensed rows or exact probabilities.
+4. Freeze an intentional public Git commit, run CI from that exact commit, and merge it to the public default branch.
+5. Deborah sends and retains the sponsor clarification, reviews every claim in her own words, narrates and verifies the sub-five-minute video, and submits the entry herself.
+
+The deployed site has passed signed-out route, strict API-method, desktop, and mobile-width checks. The uploaded video still requires its own signed-out, end-to-end playback check.
+
+### July 18 competitor delta
+
+Five public self-described track builds were audited without copying their code, copy, or assets. Only Voight carries a detected MIT license; SignalDesk, EdgeKeeper, STEAM, and TxGuard are treated as all-rights-reserved.
+
+- **SignalDesk** is the submission benchmark: deployed MVP, short video, live derived pulse, verified replay, and one-call judge evidence.
+- **EdgeKeeper** is closest to Samaritan's governance/receipt story but opens on an empty live state and has weaker receipt durability.
+- **Voight** has the clearest simple production narrative and real capture story, but no equivalent no-login governed runtime.
+- **STEAM** and **TxGuard** are visually accessible but materially demo/synthetic; they are examples of clarity, not evidence quality.
+
+Paper trading, replay, receipts, and multiple deterministic “agents” are not unique. Samaritan's remaining winning gap is one authentic chain:
+
+> real TXLine evidence → real bounded Claude thesis → non-overridable deterministic risk → paper action or refusal → durable pre-action receipt
+
+The independent patterns adopted into this plan are a licence-safe `/api/judge/evidence` manifest, a dedicated `/proof` route, a derived-only live health pulse, persistent evidence-class labels, and a default judge-ready case. Raw fixture rows, exact odds/probabilities, copied visuals, random demo P&L, fake transaction signatures, and shallow agent grids are explicitly rejected.
+
+---
+
+## 1. The decision
+
+Samaritan will not compete as another AI betting bot, generic sharp-move detector, or unfinished autonomous hedge fund.
+
+It will compete as:
+
+> **The governed autonomous sports-trading system: Claude investigates uncertainty, deterministic code alone controls risk and execution, and every action—or refusal—is committed before it happens.**
+
+The bounty submission addresses the five published criteria directly:
+
+1. **Core Functionality & Data Ingestion:** Samaritan consumes official TXLine data through one shared live/captured-replay paper conductor.
+2. **Autonomous Operation:** one synthetic, explicitly performance-excluded case completes the conductor end to end with deterministic stubs; an authentic Claude lifecycle is not claimed without an admitted fixture.
+3. **Clean, deterministic, defensible logic/code:** code owns eligibility, paper risk, pre-action ledgering, execution, and failure handling.
+4. **Innovation/novelty:** an LLM can contribute bounded judgment without receiving authority over money, while refusals and invalidated research remain first-class evidence.
+5. **Production readiness:** a stranger can open the [no-wallet judge path](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/), inspect its failure boundaries, and independently verify disclosed decision ordering and paper results.
+
+The submission will not depend on:
+
+- real-money Polymarket execution;
+- four personas or a tournament;
+- MODELER;
+- a Head Trader;
+- a Data Doctor;
+- additional detectors;
+- unsupported profitability claims.
+
+Those remain post-submission roadmap items.
+
+---
+
+## 2. Winning position
+
+### Category
+
+**Governed autonomous sports-market intelligence**
+
+### One-line pitch
+
+> **Samaritan turns verified TXLine data into bounded, replayable market decisions. Claude investigates; deterministic code controls risk and execution; every action—or refusal—is independently auditable.**
+
+### Judge-facing promise
+
+Most agents prove that they made a prediction. Samaritan proves why an autonomous system was permitted—or forbidden—to act.
+
+### Buyer
+
+The primary buyer is a prediction-market desk, sportsbook trading team, market operator, or B2B intermediary that wants agent automation without surrendering risk control, reproducibility, or auditability.
+
+### Defensible moat
+
+- Identical canonical event contracts in replay and live operation.
+- Evidence-gated strategy promotion instead of story-driven backtests.
+- A strict LLM authority boundary: thesis only, never size or order.
+- Deterministic risk and execution after the thesis.
+- Append-only lifecycle records written before action.
+- Independently verifiable local decision receipts, with an explicit human-gated path to external Solana timestamping that is not yet exercised.
+- Honest refusal of unsupported strategies.
+
+### Words we will not use without completed evidence
+
+- “Proof of alpha”
+- “Profitable strategy”
+- “Production real-money trader”
+- “Every decision is anchored on-chain”
+- “Provably un-cherry-picked”
+- “73 executable trades”
+- “AI-built submission”
+
+---
+
+## 3. Definition of winner-ready
+
+Samaritan is winner-ready only when all of the following are true:
+
+- [ ] A clean public clone installs and launches the judge demo with one documented command.
+- [x] The demo does not require TXLine credentials, a wallet, paid access, or local private archives.
+- [x] Mainnet capture and ingestion tests demonstrate SL12 consumption and the shared canonical source contract; the optional Command pulse is connectivity metadata, not a live lifecycle or study observation.
+- [x] One explicitly synthetic case visibly completes signal → triage stub → thesis stub → risk → intent → paper result → close → settlement.
+- [x] One real captured case visibly ends in a justified no-trade or veto.
+- [x] Any synthetic proving case is unmistakably labelled and excluded from performance results.
+- [x] The corrected evidence study contains no future-informed market selection.
+- [x] Historical counts represent unique executable economic decisions, not duplicate detector emissions.
+- [x] Paper fills use actual observation readiness plus model latency plus venue delay.
+- [x] Restarting the runtime reconstructs dedupe, positions, exposure, P&L, peak equity, and drawdown.
+- [x] The decision hash authenticates the full canonical record, including event type and timestamps.
+- [x] The bounty release intentionally remains unanchored: no transaction or explorer link is claimed. Any later devnet submission would be a separate, explicitly authorized post-release action.
+- [x] An independent offline verifier validates the frozen receipt. Network-backed anchor verification remains conditional on an actually submitted transaction.
+- [x] The deployed dashboard exposes only the allowlisted, licence-safe derived bundle and seven-field aggregate connectivity pulse; its public-artifact audit passes.
+- [ ] Run the final claims audit across the README, dashboard, recorded video, and submission form on the exact public commit; no draft review substitutes for that gate.
+- [ ] Deborah can explain and defend the architecture, evidence, limitations, and major design decisions.
+- [ ] Sponsor clarification on human participation and public derived data is retained in writing.
+- [ ] The video is below five minutes and every URL works in an incognito browser.
+
+---
+
+## 4. P0 — Repair the truth before building the show
+
+These items block every profit, edge, or production-readiness claim.
+
+### 4.1 Invalidate gate-study v1
+
+The original v1 dynamic-total study chose the market using a price observed as late as kickoff minus five minutes, even though eligible signals stopped at kickoff minus fifteen minutes. In the audited artifact, 95 of 98 selected lines used information after the signal window. Its coverage calculation also counted observations without the same as-of cutoff; v1 is preserved as invalidated history and the corrected selector is used by v2.
+
+Actions:
+
+- [x] Mark the current `+119 probability bps` result as causally invalidated.
+- [x] Preserve the old artifact for audit history; do not silently overwrite it.
+- [x] Add a dated methodology note explaining the defect and its impact.
+- [x] Create a new protocol/config version before recomputing results.
+- [x] Remove the invalidated number from the README, dashboard, video, and submission copy until a corrected result exists.
+
+Acceptance tests:
+
+- The old result is clearly labelled invalidated anywhere it remains visible.
+- No current product surface calls it held-out edge, alpha, or profitability evidence.
+- The corrected protocol has a distinct identifier and config hash.
+
+### 4.2 Make total-line selection causal
+
+Choose the exact market using only information available before the first eligible detector decision. A fixed T−180-minute cutoff or the first eligible jointly observable book are acceptable starting designs; the rule must be deterministic, frozen, and applied before labels or future prices are inspected.
+
+Actions:
+
+- [x] Define one causal selector timestamp and document why it is live-reproducible.
+- [x] Apply the same cutoff to probability, coverage, volume, liquidity, and market availability.
+- [x] Fail closed when no line is eligible at the cutoff.
+- [x] Prohibit selector inputs later than the earliest evaluated signal.
+- [x] Record selector evidence and cutoff in every fixture artifact.
+- [x] Add tests that inject a highly attractive future line and prove it cannot affect selection.
+- [x] Add tests that future coverage cannot make a line eligible.
+
+Acceptance tests:
+
+- Every selected fixture satisfies `selectorEvidenceTsMs <= detectorEvaluationStartTsMs`.
+- Replacing all post-cutoff market data produces the same selected line.
+- A fixture with insufficient as-of evidence is rejected rather than rescued by future observations.
+
+### 4.3 Count executable economic decisions
+
+The current totals result contains buys, unsupported sells, and simultaneous complementary Over/Under expressions of the same move.
+
+Actions:
+
+- [x] Define a deterministic economic-case identity for fixture, market condition, direction, and decision window.
+- [x] Collapse complementary same-move emissions into one case.
+- [x] Either convert a sell into a verified complement-token buy or exclude it from executable results.
+- [x] Prevent both sides of one binary condition from being counted as independent opportunities.
+- [x] Report raw detector emissions separately from cases, orders, fills, and unique fixtures.
+- [x] Cluster uncertainty by fixture and disclose repeated decisions within a match.
+
+Acceptance tests:
+
+- Every performance row maps to an order the current paper executor can actually express.
+- One underlying movement cannot inflate the primary sample through complementary tokens.
+- The report shows emissions → unique cases → approved intents → fills as separate counts.
+
+### 4.4 Rerun and report the corrected study
+
+Actions:
+
+- [x] Rerun chronological training/held-out evaluation from scratch.
+- [x] Preserve the train/test boundary.
+- [x] Report gross CLV, venue-cost sensitivities, unique cases, unique fixtures, and confidence intervals.
+- [x] Report model operating cost separately from executable CLV.
+- [x] Keep the real-money gate closed regardless of the bounty result.
+- [x] Reject unsupported strategy families and make the rejection part of the demo; the one positive Total Goals family remains only a forward-paper candidate.
+
+Acceptance tests:
+
+- A single command regenerates the corrected JSON and Markdown artifacts.
+- The generated report contains protocol ID, config hash, source hashes, and causal selector diagnostics.
+- No manual result selection occurs after held-out metrics are visible.
+
+---
+
+## 5. P0 — Make paper execution causally honest
+
+### 5.1 Separate market time from knowledge time
+
+`sourceTsMs` says when a market event occurred. `observedTsMs` or processing time says when Samaritan could act on it. Execution readiness must use knowledge time.
+
+Actions:
+
+- [x] Carry signal source time and signal observed/processing time separately.
+- [x] Start measured decision latency from actual signal observation/processing.
+- [x] Compute `readyAtTsMs` from observation time, not source time.
+- [x] Retain source time for provenance, market ordering, and latency measurement.
+- [x] Fail closed on invalid, regressing, or impossible observed timestamps.
+- [x] Test delayed delivery and reconnect catch-up explicitly.
+
+Acceptance tests:
+
+- A five-second-late event cannot produce an execution before its actual receipt.
+- Replaying the same captured source and observed timestamps yields the same eligibility outcome.
+- Dashboard evidence shows source → observed → signal → decision-ready → execution timestamps.
+
+### 5.2 Model current Polymarket sports-order behavior
+
+Actions:
+
+- [x] Add the current sports marketable-order placement delay to paper readiness.
+- [x] Use the first eligible book after analysis latency **and** venue delay.
+- [ ] Parse and validate current accepting-orders, start-time, clear-book, tick, size, and fee metadata required for safe execution.
+- [x] Represent delayed, unmatched, partial, filled, and no-fill outcomes explicitly.
+- [x] Fail closed if required fee/tick/minimum-size metadata is absent or unsupported.
+- [x] Freeze the current sports-order delay and execution-metadata contract in the study configuration.
+
+Acceptance tests:
+
+- A favorable book inside the delay window cannot fill the simulated order.
+- A market that stops accepting orders cannot produce an intent or fill.
+- A later adverse book is used when it is the first actually eligible execution state.
+
+### 5.3 Attribute AI operating cost
+
+Actions:
+
+- [ ] Record triage and analyst cost per case.
+- [ ] Report executable CLV and operating economics as different metrics.
+- [ ] Add a deterministic pre-Opus economic-value gate.
+- [ ] Allow the gate only to avoid model spend; it must never create or enlarge a trade.
+- [ ] Prefer deterministic or Haiku-only handling when Opus cannot plausibly earn its cost.
+
+Acceptance tests:
+
+- Every completed case reports model cost, venue cost, and their units.
+- The dashboard never subtracts dollar inference cost directly from probability CLV.
+- Opus is not invoked when the configured value threshold fails.
+
+---
+
+## 6. P0 — Make the runtime autonomous and restart-safe
+
+### 6.1 Build one production orchestration entrypoint
+
+The bounty-critical service must connect the implemented pieces without test-only assembly.
+
+Required path:
+
+```text
+TXLine/Polymarket events
+→ canonical bus
+→ feature engine
+→ frozen detector
+→ case scheduler
+→ Haiku
+→ Opus
+→ deterministic risk
+→ paper executor
+→ portfolio lifecycle
+→ ledger
+→ dashboard projection
+```
+
+Actions:
+
+- [ ] Add one documented service command for live and one for frozen judge replay.
+- [x] Inject live and captured-replay sources at the edge into `runPaperSession`; do not expose a replay/live flag to strategies.
+- [ ] Surface feed health, queue state, current cases, terminal failures, and ledger head.
+- [ ] Shut down cleanly and finish durable writes before exit.
+
+Acceptance tests:
+
+- [x] One command runs the frozen synthetic judge case from input through final receipt.
+- [x] The same conductor accepts live and captured-replay canonical sources without strategy-code changes.
+- [x] A source/runtime failure stops the session fail closed with its durable ledger state preserved.
+
+The first two checks prove conductor wiring and source parity. They do not prove a completed real-feed/real-Claude lifecycle or a deployed autonomous daemon.
+
+### 6.2 Reconstruct state from the append-only ledger
+
+Actions:
+
+- [x] Rehydrate seen signals and case terminal states.
+- [x] Rehydrate open, closed, and settled positions.
+- [x] Recompute exposure, realized P&L, equity peak, and drawdown.
+- [x] Recover or deterministically expire pending cases.
+- [x] Reject inconsistent or incomplete ledger histories.
+- [x] Resume only after local ledger-chain verification and state reconstruction succeed; external Solana verification remains a separate release gate.
+
+Acceptance tests:
+
+- Restarting with an open position retains exactly the same exposure and drawdown.
+- Replaying an already-seen signal after restart cannot spend Claude tokens or create another order.
+- Corrupt lifecycle ordering prevents startup.
+
+---
+
+## 7. P0 — Turn local integrity into external proof
+
+### 7.1 Version the canonical decision envelope
+
+The record hash must authenticate everything a judge relies on.
+
+Minimum committed fields:
+
+- schema version;
+- sequence;
+- entry ID;
+- case ID;
+- event kind;
+- event/source timestamp;
+- observed timestamp where applicable;
+- durable insertion timestamp;
+- canonical payload;
+- previous hash.
+
+Actions:
+
+- [x] Define one stable canonical serialization.
+- [x] Introduce a new hash schema version without destructively rewriting the old ledger.
+- [x] Verify every field used by UI or proof claims.
+- [x] Add tamper tests for kind, case ID, timestamps, payload, order, and prior hash.
+
+Acceptance tests:
+
+- Changing any committed field breaks verification.
+- Old v1 evidence remains identifiable and readable but cannot masquerade as v2 proof.
+- The verifier rejects reordered, omitted, duplicated, or rewritten events.
+
+### 7.2 Produce the Samaritan Decision Receipt
+
+Every demonstrated case should export one small, licence-safe receipt containing:
+
+- source proof reference or derived source hash;
+- code/config hash;
+- signal evidence;
+- model and prompt version;
+- model cost;
+- structured thesis;
+- deterministic risk verdict;
+- execution intent;
+- post-readiness book evidence;
+- fill/no-fill;
+- close/settlement evidence;
+- final ledger head;
+- optional Solana network and transaction signature, present only after an actual transaction is submitted and verified.
+
+Actions:
+
+- [x] Define the receipt schema.
+- [x] Generate receipts directly from verified ledger entries.
+- [x] Add a CLI verifier that needs no private database.
+- [x] Make receipt verification visible in the dashboard as a separate synthetic evidence class.
+
+### 7.3 Optional post-release external anchoring
+
+This track is intentionally outside the paper-only bounty release. The submitted receipt remains **unanchored**, contains no explorer link, and makes no TXLine Merkle/on-chain source-verification claim.
+
+Release action:
+
+- [x] Freeze the public receipt and every judge-facing surface at `not_submitted` / unanchored.
+
+Future actions require a separate Deborah authorization after the bounty release:
+
+- Anchor a specific ledger head on Solana devnet.
+- Store network, signature, slot, commitment status, and anchored head.
+- Surface a working public explorer link only after verification.
+- Verify that the public receipt head equals the anchored value.
+- Separately integrate and test a TXLine Merkle/validation demonstration where the official API permits it.
+
+Conditional acceptance tests, only if Deborah later authorizes and a transaction is submitted:
+
+- A stranger can run the verifier and reach the explorer transaction without a wallet.
+- The dashboard distinguishes local integrity, replay parity, and external anchoring.
+- No page calls a replay-manifest hash a decision-ledger head.
+
+---
+
+## 8. P0 — Demonstrate one complete product, not ten partial products
+
+### Required evidence pair
+
+#### A. Real captured refusal
+
+Use Spain–Belgium or another verified synchronized capture to demonstrate:
+
+- exact source timing;
+- what hypothesis was tested;
+- contrary evidence;
+- deterministic reason for no trade;
+- zero money moved;
+- no claim that the case passed through Claude if it did not.
+
+Correct the case classification: stale-quote feasibility evidence must not be labelled `CONSENSUS_MOVE`.
+
+#### B. Complete lifecycle case
+
+Preferred order:
+
+1. A real captured eligible paper case.
+2. A captured-book replay of a real eligible case.
+3. A clearly labelled synthetic proving fixture if no real case qualifies before recording.
+
+The lifecycle case must show:
+
+- detector signal;
+- bounded triage result (the current public synthetic case uses a deterministic Haiku-shaped stub);
+- strict `submit_thesis` result (the current public synthetic case uses a deterministic Opus-shaped stub; real Claude requires a registered eligible session);
+- deterministic risk verdict;
+- execution intent recorded before execution;
+- post-readiness book;
+- fill, partial fill, no-fill, or veto;
+- close and settlement;
+- decision receipt;
+- optional external anchor only if an actual transaction has been submitted and verified.
+
+Synthetic data must be excluded from every research, profitability, and strategy-performance metric.
+
+### Claude's visible value
+
+Claude should contribute judgment a deterministic detector does not already provide:
+
+- cause classification;
+- strongest argument against the trade;
+- evidence limitations;
+- invalidation conditions;
+- market/settlement-rule interpretation where relevant.
+
+Before submission, either:
+
+- [ ] implement the promised minimal read-only evidence tools; or
+- [x] change all documentation to describe the analyst accurately as a bounded structured evaluator over a code-assembled evidence bundle.
+
+Never claim tools the runtime does not expose.
+
+---
+
+## 9. P0 — Ship a clean, safe public product
+
+### 9.1 Repository recovery and reproducibility
+
+Actions:
+
+- [x] Review modified and untracked release categories deliberately and freeze the inclusion allowlist in `docs/submission/public-release-scope.md`.
+- [x] Remove temporary root scripts, design explorations, and social-render projects from submission scope through `.gitignore` without deleting local work.
+- [x] Confirm the current two-commit Git history contains no secret-bearing path or known credential signature; only the safe `phase0/.env.example` is tracked.
+- [ ] Commit the intended Phase 3/4/runtime/dashboard work in understandable commits.
+- [x] Configure the approved public GitHub remote.
+- [ ] Push the exact release commit, merge it to the public default `main` branch, and verify all public documentation links against that branch.
+- [x] Add CI for install, typecheck, tests, build, and frozen-demo verification.
+- [x] Pin Node and pnpm requirements.
+- [x] Provide a clean-clone quickstart.
+
+Clean-clone acceptance command:
+
+```bash
+pnpm install --frozen-lockfile
+pnpm check
+pnpm demo
+```
+
+Expected result: a safe local judge surface with a verified frozen receipt, without private archives or credentials.
+
+### 9.2 Derived-only public artifact bundle
+
+The public demo must not depend on the ignored 8.6 GB `data/` tree or 41 GB `samples/` tree.
+
+Actions:
+
+- [x] Export the minimum judge fixtures from verified receipts.
+- [x] Include only transformed, non-reconstructive, licence-safe evidence.
+- [x] Replace exact raw/fair-probability histories with movement, deltas, z-scores, aggregates, and decision evidence unless written sponsor approval says otherwise.
+- [x] Add a build-time check that raw TXLine payload shapes are absent.
+- [x] Document which artifacts are synthetic, captured-derived, and externally verifiable.
+
+### 9.3 Dashboard correction
+
+Required routes:
+
+- Command
+- Matchroom
+- Casebook
+- Study
+- Proof/Receipt
+
+Actions:
+
+- [x] Replace offline-only projection assumptions with the frozen public bundle plus runtime API.
+- [x] Bind the service to the validated platform interface and deploy the no-login [judge MVP](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/).
+- [x] Correct detector and proof labels.
+- [x] Show zero-state, failure, veto, no-trade, and synthetic paper-fill states honestly and with persistent evidence-class labels.
+- [x] Surface model cost and execution latency in the captured case and downloadable receipt.
+- [x] Surface the exact Solana status (`not_submitted`) and offline verifier result without implying network proof.
+- [x] Keep long-run sealed results sealed.
+- [x] Remove any present-tense claim for an unimplemented feature from current judge-facing copy.
+
+Acceptance tests:
+
+- The deployed dashboard works on mobile and desktop in an incognito session.
+- No wallet, subscription, API key, or authentication is required.
+- Every displayed metric can be traced to a frozen public receipt or live derived runtime state.
+
+---
+
+## 10. Legal and sponsor gates
+
+This section is an operational checklist, not legal advice.
+
+### Human participation
+
+The track markets agent products, but the listing metadata and binding terms create ambiguity around agent participation.
+
+- [x] Deborah remains the participant, submitter, narrator, and interview representative in every current submission artifact.
+- [x] Claude is described as a constrained runtime component inside Deborah's product.
+- [x] The entry is never described as AI-built or autonomously submitted.
+- [ ] Obtain written sponsor clarification and retain the response.
+- [ ] Deborah can explain every important product and architecture decision.
+
+### TXLine licence
+
+- [x] Publish no raw captures, raw replay files, tokens, or reconstructive feed output in the frozen public bundle.
+- [ ] Obtain written clarification for any exact normalized probability series proposed for the public UI.
+- [x] Prefer derived signals, transformations, decisions, aggregates, and hashes.
+- [x] Document that continued post-hackathon use requires the appropriate licence.
+
+### Submission safety
+
+- [x] No secrets in the intended release tree or current Git history under the recorded path/signature scans; rerun on the final commit.
+- [x] No FIFA logo, tournament emblem, team crest, or implication of endorsement; public display copy uses neutral “World Cup” wording.
+- [x] No real-money dependency.
+- [x] No judge payment, token purchase, wallet, or subscription requirement.
+- [x] All judge-facing third-party data services, fonts, assets, and software boundaries are documented in `docs/submission/third-party-notices.md`.
+
+---
+
+## 11. Scope cuts that protect the win
+
+The following work is frozen until every P0 acceptance test above passes:
+
+- Polymarket real-money adapter
+- Mainnet execution
+- Four-persona tournament
+- Head Trader allocation
+- MODELER calibration
+- Data Doctor agent
+- Additional strategy families
+- Risk-manager judgment model
+- Light theme
+- General-purpose plugin architecture
+- Multi-competition expansion
+- Nonessential animation or dashboard ornament
+
+Exception: a frozen item may only be resumed before submission if all winner-ready gates are green and it materially improves the five-minute demonstration.
+
+---
+
+## 12. Four-day execution order
+
+### July 14 — Truth and causality
+
+- [x] Declare the old gate result invalidated.
+- [x] Fix causal total selection and as-of coverage.
+- [x] Collapse economic duplicates and resolve sell semantics.
+- [x] Correct the execution clock and model venue delay.
+- [x] Verify France–Spain; it failed closed and remains inadmissible.
+- [x] Attempt England–Argentina in its watchdog-supervised window; preserve the Polymarket-stall failure and incomplete terminal manifests as inadmissible fail-closed evidence.
+- [ ] Send the sponsor clarification questions.
+- [ ] Recover and commit the intended working tree.
+
+**End-of-day gate:** corrected study can run; no invalid claim remains judge-facing.
+
+### July 15 — Golden path and durability
+
+- [x] Add the shared `runPaperSession` production orchestration entrypoint and source adapters.
+- [x] Reconstruct state from the ledger on startup.
+- [ ] Produce one complete ledgered real-Claude paper case; v2 registration is complete, but no case may run until a fresh fixture passes every admission and mapping-review gate.
+- [x] Produce one real evidence-based refusal.
+- [x] Record case-level latency and model cost in the receipt contract; the synthetic proving case records zero stub cost.
+- [x] Complete close and settlement evidence for the synthetic proving fixture.
+
+**End-of-day gate:** one command completes the explicitly synthetic, performance-excluded product lifecycle through the shared conductor. The real-Claude evidence gate remains open.
+
+### July 16 — Proof and public deployment
+
+- [x] Version the complete hash envelope.
+- [x] Generate the Decision Receipt.
+- [x] Keep the bounty release intentionally unanchored; publish no explorer link or network-backed proof claim.
+- [x] Ship the independent offline receipt verifier.
+- [x] Freeze the licence-safe public artifact bundle.
+- [x] Deploy the read-only dashboard.
+- [x] Add CI and verify a clean-clone-shaped bundle without private data; rerun from the final public commit.
+
+**End-of-day gate:** a stranger can reproduce and verify the case without private access.
+
+### July 17 — Submission packaging
+
+- [x] Rewrite README around the judge's 90-second path.
+- [x] Draft the required TXLine API feedback for Deborah's final review.
+- [ ] Record and edit the demo below five minutes.
+- [ ] Rehearse Deborah's narration and likely judge questions.
+- [ ] Test every applicable link, command, and mobile layout; confirm that no Solana explorer link appears in the intentionally unanchored release.
+
+**End-of-day gate:** submission package is complete and frozen.
+
+### July 18 — Submit
+
+- [ ] Run the final clean-clone test.
+- [ ] Run CI from the public branch.
+- [ ] Watch the uploaded video from beginning to end.
+- [x] Test the deployed MVP signed out across desktop/mobile routes and strict API methods.
+- [ ] Submit before the internal deadline.
+- [ ] Preserve a local copy of the exact submission text and URLs.
+
+July 19 is buffer, not feature-development time.
+
+---
+
+## 13. Five-minute demo script — target 4:30–4:45
+
+### 0:00–0:20 — Hook
+
+> “The dangerous thing about an AI trader is not a wrong prediction. It is that nobody can prove what it saw or why it was allowed to touch money.”
+
+### 0:20–0:50 — Authority boundary
+
+Show:
+
+```text
+TXLine → deterministic detector → bounded Claude thesis
+→ deterministic risk → paper execution → decision receipt
+```
+
+State that Claude has no wallet, order, or position-sizing capability.
+
+### 0:50–1:35 — Real captured refusal
+
+Show the synchronized real case:
+
+- event and observation timestamps;
+- tested hypothesis;
+- market moved first or no stale window existed;
+- deterministic no-trade conclusion;
+- zero money moved.
+
+Close the segment with: **“Every decision. Especially the pass.”**
+
+### 1:35–2:45 — Complete synthetic autonomous lifecycle
+
+Show:
+
+- the prominent synthetic/performance-excluded label;
+- invented signal;
+- deterministic Haiku-shaped triage stub;
+- deterministic Opus-shaped thesis stub and argument against itself;
+- deterministic risk verdict;
+- intent recorded before execution;
+- latency- and venue-adjusted book;
+- fill/no-fill;
+- close and settlement.
+
+### 2:45–3:15 — Falsification
+
+Show “Strategies Samaritan refused to deploy”:
+
+- XMARKET rejected;
+- FADER rejected;
+- Match Result momentum rejected;
+- STALE_QUOTE unsupported;
+- totals result reported exactly as the corrected study found it.
+
+Do not hide a corrected negative result.
+
+### 3:15–4:00 — Independent proof
+
+Show:
+
+- ordered lifecycle entries;
+- receipt hash;
+- verifier command;
+- visible `not_submitted` Solana status and no explorer link;
+- disclosed TXLine-derived source-reference boundary without raw feed redistribution;
+- distinction between local integrity and external anchoring.
+
+### 4:00–4:30 — Production posture
+
+Show:
+
+- passing tests and CI;
+- live/replay identity;
+- restart reconstruction;
+- fail-closed boundaries;
+- licence-safe public deployment;
+- no-wallet judge access.
+
+### 4:30–4:45 — Close
+
+> **“Samaritan does not promise certainty. It makes autonomous trading accountable.”**
+
+---
+
+## 14. README and submission structure
+
+The public README should answer these questions in this order:
+
+1. What is Samaritan?
+2. Why is it different from an AI betting bot?
+3. How can a judge run or open it in under 90 seconds?
+4. Which TXLine endpoints and fields does it use?
+5. What complete case can the judge inspect?
+6. What does the Decision Receipt prove?
+7. What evidence was rejected or remains inconclusive?
+8. What is paper-only and what is unimplemented?
+9. What are the legal/data boundaries?
+10. Where are the demo, deployed MVP, repository, verifier, and—only if one was actually submitted—the Solana transaction?
+
+Required links near the top:
+
+- Hosted judge MVP
+- Demo video
+- Public repository
+- Quickstart
+- Architecture
+- Corrected evidence report
+- Example Decision Receipt
+- Explicit unanchored status; no Solana explorer link in this release
+- TXLine API feedback
+
+---
+
+## 15. Judge questions Deborah must be ready to answer
+
+### “Where is the alpha?”
+
+The correct answer is the corrected evidence result, including limitations. The product's novelty is governed autonomous execution and falsification, not an unsupported promise of profit.
+
+### “Why use Claude?”
+
+Claude handles bounded interpretation: competing explanations, contrary evidence, invalidation conditions, and rules ambiguity. It cannot size, approve risk, or place an order.
+
+### “Why TXLine?”
+
+TXLine supplies normalized, low-latency sports-market and score data suitable for reproducible decisions. Samaritan demonstrates its value through timing, probability normalization, replay parity, health monitoring, and locally committed source references. TXLine's validation surfaces were researched, but Samaritan does not claim that the current receipt performs Merkle or on-chain source verification.
+
+### “Can the agent lose money?”
+
+The bounty build is paper-only. Implemented deterministic code owns paper eligibility, fixed-stake caps, exposure, execution, and drawdown rejection. A global manual kill switch belongs to the separately authorized real-money roadmap and is not implemented in this release. The real-money gate remains closed.
+
+### “What is actually on-chain?”
+
+No Samaritan decision receipt is on-chain. TXLine access uses Solana-backed subscription infrastructure, but Samaritan's receipt is locally hash-chained and intentionally unanchored for this bounty release. There is no explorer transaction to show. Any later timestamping would require separate authorization and exact network verification.
+
+### “Can I reproduce this?”
+
+Yes: use the public clean-clone command, verify the frozen receipt independently, or open the signed-out [hosted no-wallet path](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/).
+
+### “What failed?”
+
+Explain the rejected strategies and the selector causality defect openly. The system is designed to reject weak or invalid evidence before money is authorized.
+
+---
+
+## 16. Final submission kill criteria
+
+Do not submit a claim or feature if any of these is true:
+
+- It requires private raw TXLine data to understand or reproduce.
+- It cannot be demonstrated from the public branch.
+- It relies on the invalidated historical selector.
+- It counts a decision the current executor cannot express.
+- It uses source time as though it were observation time.
+- It ignores the current venue delay.
+- It calls an unanchored or incomplete hash external proof.
+- It resets risk state after restart.
+- It implies real-money readiness.
+- It requires a judge wallet, payment, token, API key, or subscription.
+- Deborah cannot explain it confidently in a live interview.
+
+---
+
+## 17. Final rally point
+
+The winning move is not to look like the biggest system in the competition.
+
+It is to be the most complete, honest, and verifiable autonomous system a judge can actually run.
+
+> **Fix the evidence. Finish one vertical slice. Prove every boundary. Ship the receipt.**
diff --git a/docs/09-paper-study-v2-candidate.md b/docs/09-paper-study-v2-candidate.md
new file mode 100644
index 0000000..3aa5c3f
--- /dev/null
+++ b/docs/09-paper-study-v2-candidate.md
@@ -0,0 +1,113 @@
+# Paper Study V2 Candidate — Deborah Review Draft
+
+> **Resolved July 18, 2026:** Deborah registered this exact frozen candidate for forward paper observation only. The immutable decision record is [`10-paper-study-v2-registration.md`](10-paper-study-v2-registration.md). This draft remains preserved as the pre-decision review artifact; it is not the current operational status.
+
+**Draft status at preparation:** `engineering_candidate_unregistered`
+**Code protocol:** `paper-study-v2-candidate-2026-07-14`
+**Frozen candidate config SHA-256:** `93e61c1903d0a13bbeb1dbbd3ad9b11af0335b96c82bd2ca7aa9ddedeeabf3ce`
+**Prepared:** July 14, 2026
+**Real-money gate:** **closed**
+
+This is a review draft, not a registration. It cannot admit a qualifying observation, spend Claude tokens through the operational study command, initialize replacement ledgers, authorize a paper candidate, or unlock real money until Deborah explicitly signs the final protocol. The invalidated v1 ledgers remain preserved and must never be relabelled.
+
+## Why a fresh protocol is justified
+
+The corrected historical study is:
+
+- protocol `historical-gate-causal-economic-v4-2026-07-14`;
+- configuration SHA-256 `9a4eeff928f697fc55ab5147a4dc07f611c40bb749501fc3bd92b211f24b2e54`;
+- 68 training fixtures / 30 held-out fixtures with an unchanged chronological boundary;
+- zero future-probability, future-coverage, late-cutoff, or untimestamped-liquidity selector violations across 590 evidence rows;
+- normalized into executor-expressible Total Goals buys, with complementary expressions collapsed and sell-only cases excluded.
+
+The aggregate `CONSENSUS_MOVE` detector did not pass its fixture-clustered interval. The **prespecified Total Goals–only family** did meet the historical signal criteria under the training-selected configuration:
+
+- 135 normalized Total Goals training cases;
+- 38 held-out normalized buy cases across 18 fixtures;
+- mean `+132.7` probability bps after the 100 bps historical cost proxy;
+- fixture-clustered 95% interval `+14.3` to `+243.9` bps.
+
+This is not alpha, profitability, or fill evidence. Historical Polymarket observations are sampled prices without executable bid/ask depth. It supports only a fresh forward paper study using real observed books, measured costs, and no endpoint peeking.
+
+## Frozen candidate
+
+### Market and detector
+
+- Full-time `total_goals` only.
+- `CONSENSUS_MOVE` only.
+- Move absolute z-score ≥ `1`.
+- CUSUM threshold `0.001` probability (`10` bps).
+- Minimum raw consensus/market gap `0.01` probability (`100` bps).
+- Minimum consensus updates `5`.
+- Every other detector and Match Result remain disabled for this study.
+
+### Causal total-line selection
+
+- Exact line frozen at kickoff minus `180` minutes, before the first detector snapshot.
+- Probability and coverage both require `source_ts_ms <= selector_cutoff_ts_ms`.
+- At least `1,000` as-of coverage points.
+- Closest-to-even balance rule, within `0.15` probability of 50/50.
+- Historical volume and liquidity are zero-weighted and forbidden because no timestamped as-of values exist.
+- Missing or late selector evidence fails closed.
+
+### Economic-case identity
+
+- Retain only an actual Total Goals `buy` the paper executor can express.
+- `buy Over + sell Under` is one Over case; `buy Under + sell Over` is one Under case.
+- Collapse duplicate buys and complementary sell expressions in the same detector/fixture/market/source-time/observation-time/economic-outcome case.
+- Drop sell-only cases unless a future protocol version can prove the complementary token's executable ask. This version cannot synthesize that proof.
+- Match Result is outside the candidate and is never treated as a binary complement.
+
+### Execution timing
+
+- `detectedAtTsMs` records source market time.
+- `observedAtTsMs` records Samaritan's knowledge time.
+- Model readiness begins at observation time and uses the greater of measured wall latency or the configured minimum.
+- A fixed `1,000 ms` Polymarket sports placement delay is then added.
+- Only the first canonical book observed after readiness **and** placement delay can fill.
+- Small negative source/observation skew remains diagnostic; impossible or regressing knowledge time fails closed.
+
+### Paper risk proposal carried from v1 — requires Deborah's renewed approval
+
+- Simulated bankroll: `$50` (`50,000,000` micro-USD).
+- Fixed paper stake: `$3` (`3,000,000` micro-USD).
+- Aggregate open exposure cap: `$15` (`15,000,000` micro-USD).
+- Drawdown rejection stop: `$20` (`20,000,000` micro-USD).
+- No LLM can create, enlarge, or override these values.
+- Real-money gate remains closed regardless of outcome.
+
+### Stopping and evaluation proposal
+
+- Minimum `20` filled matches and `40` fills; target `30` matches.
+- Unit of uncertainty is the match, not the signal.
+- Primary endpoint: executable kickoff CLV net of measured entry cost.
+- `10,000` whole-match bootstrap iterations, seed `20260714`.
+- Minimum fill rate `60%`.
+- Mean slippage no greater than `100` bps.
+- Every selected depth, close, and settlement field complete.
+- Max drawdown within the proposed `$20` stop.
+- Bounty/demo output remains exploratory; qualifying long-run endpoints remain sealed until both stopping thresholds are met.
+
+## Operational gates before registration
+
+- [x] Causal selector repaired and independently diagnosed.
+- [x] Economic cases normalized and sample gates changed from emissions to cases.
+- [x] Observation-time readiness and venue delay implemented.
+- [x] Restart rehydration implemented for dedupe, pending cases, positions, exposure, P&L, peak, and drawdown.
+- [x] Full v2 decision hash envelope and tamper tests implemented.
+- [x] Deborah confirmed the proposed paper risk values above on July 18, 2026.
+- [x] Deborah confirmed the stopping/decision rules above on July 18, 2026.
+- [x] Deborah confirmed that the corrected historical result is sufficient only to begin forward paper observation—not to claim alpha or unlock money.
+- [x] Fresh v2 bounty and long-run ledgers were initialized after registration; preserved v1 ledgers remain untouched.
+- [x] The operational protocol status changed from `engineering_candidate_unregistered` to `paper-study-v2-2026-07-18` in code and current-status documentation.
+
+## Deborah decision — resolved by the registration record
+
+- [x] **REGISTER V2 FOR FORWARD PAPER ONLY** under the exact config hash above.
+- [ ] **DO NOT REGISTER**; revise or shelve the candidate.
+
+Name: Deborah
+Decision date/time (UTC): `2026-07-18T07:03:55Z`
+Signature/confirmation reference: explicit written authorization retained in [`10-paper-study-v2-registration.md`](10-paper-study-v2-registration.md)
+
+Registration, if granted, authorizes only a paper study and bounded model spend under existing project controls. It does not authorize Polymarket authentication, a wallet, token approval, deposit, order placement, real-money trading, or any risk-limit change.
diff --git a/docs/10-paper-study-v2-registration.md b/docs/10-paper-study-v2-registration.md
new file mode 100644
index 0000000..d0205aa
--- /dev/null
+++ b/docs/10-paper-study-v2-registration.md
@@ -0,0 +1,66 @@
+# Paper Study V2 Registration — Forward Paper Only
+
+**Decision:** REGISTERED
+**Protocol:** `paper-study-v2-2026-07-18`
+**Registered at:** `2026-07-18T07:03:55Z`
+**Participant and decision owner:** Deborah
+**Frozen candidate config SHA-256:** `93e61c1903d0a13bbeb1dbbd3ad9b11af0335b96c82bd2ca7aa9ddedeeabf3ce`
+**Real-money gate:** **closed**
+
+## Authorization record
+
+On July 18, 2026, after reviewing the current readiness verdict and the exact limits below, Deborah gave explicit written authorization: “alright go ahead, you have my authorization.” This registration resolves the human decision gate recorded in [`09-paper-study-v2-candidate.md`](09-paper-study-v2-candidate.md) without rewriting that candidate draft or the invalidated v1 history.
+
+Deborah authorizes Samaritan to:
+
+- initialize fresh, versioned v2 bounty and long-run paper ledgers;
+- observe qualifying cases prospectively under the frozen v2 protocol;
+- run the bounded Haiku triage and Opus analyst path only after every fixture-admission gate passes;
+- execute deterministic **paper** orders under the limits below; and
+- evaluate the registered study under the frozen stopping and decision rules.
+
+This decision does **not** authorize Polymarket authentication, token approval, deposits, venue orders, wallet access, Solana submission, real-money trading, or any expansion of an agent's authority.
+
+## Frozen paper risk envelope
+
+| Control | Registered value |
+|---|---:|
+| Simulated bankroll | `$50` (`50,000,000` micro-USD) |
+| Fixed paper stake | `$3` (`3,000,000` micro-USD) |
+| Maximum aggregate open exposure | `$15` (`15,000,000` micro-USD) |
+| Drawdown rejection stop | `$20` (`20,000,000` micro-USD) |
+| Real-money gate | `closed` |
+
+No model, prompt, agent output, or runtime option may create, enlarge, or override those values. Existing Claude spend controls remain in force: `$200` operating target, `$300` hard project ceiling, pre-request reservation, append-only accounting, and durable invocation evidence for API-backed study runs.
+
+## Frozen study scope
+
+- Full-time `total_goals` only.
+- `CONSENSUS_MOVE` only.
+- Move absolute z-score at least `1`.
+- Directional CUSUM at least `0.001` probability (`10` bps).
+- Raw consensus/market gap at least `0.01` probability (`100` bps).
+- At least `5` consensus updates.
+- Causal total-line selection frozen at kickoff minus `180` minutes.
+- Live executable book evidence, exact verified mapping, complete lifecycle evidence, and all existing freshness/timing rules remain mandatory.
+- Every other detector and Match Result remain outside this registered study.
+
+## Frozen stopping and decision rules
+
+- Minimum `20` filled matches and `40` fills; target `30` matches.
+- Match is the unit of uncertainty.
+- Primary endpoint is executable kickoff CLV net of measured entry cost.
+- `10,000` whole-match bootstrap iterations with seed `20260714`.
+- Minimum fill rate `60%`.
+- Mean slippage no greater than `100` bps.
+- Selected depth, close, and settlement evidence must be complete.
+- Maximum drawdown must remain within the registered `$20` stop.
+- Bounty/demo output remains exploratory; long-run rows and endpoints stay sealed until both stopping thresholds are met.
+
+The corrected historical v4 result only justified beginning forward observation. It remains sampled-price signal research—not alpha, fill evidence, profitability, or a real-money gate.
+
+## State-separation rule
+
+The invalidated v1 ledgers, manifest, report, and source artifacts remain preserved at their original paths. Registered v2 uses fresh versioned storage under `data/paper/v2/`. No v1 row may be copied, relabelled, or counted as a v2 observation.
+
+Registration alone admits no fixture. A case must still pass exact TXLine/Polymarket identity, settlement-rules review, synchronized executable evidence, causal selector, terminal-manifest, kickoff-close, resolution, freshness, and immutable-input checks before any model request or paper decision.
diff --git a/docs/PHASE-1-STATUS.md b/docs/PHASE-1-STATUS.md
index b69b3cb..7dcfb5a 100644
--- a/docs/PHASE-1-STATUS.md
+++ b/docs/PHASE-1-STATUS.md
@@ -1,6 +1,6 @@
# Phase 1 — Data Backbone Status
-*Completed July 10, 2026. The synchronized Spain–Belgium capture remains scheduled evidence for the Phase-3 live lane; it is no longer treated as a blocker to the Phase-1 software exit criterion.*
+*Completed July 10, 2026; evidence status corrected July 15. The synchronized Spain–Belgium capture and replay are complete. Its 18 goal×market feasibility observations found no clean post-TXLine stale window, so STALE_QUOTE remains disabled and the corpus is research-only.*
## Spain–Belgium capture confirmation
@@ -15,7 +15,7 @@ The machine-readable record is `config/captures/spain-belgium-2026-07-10.json`.
## Implemented
-- Node `22.23.1`, pnpm `10.32.1`, strict TypeScript, ESM, and vitest are pinned at the workspace root.
+- Node `22.23.1`, pnpm `11.13.0`, strict TypeScript, ESM, and vitest are pinned at the workspace root.
- `CanonicalEventBus` exposes one event union to every downstream consumer. There is no live/replay mode field.
- TXLine normalization uses captured field shapes. `Pct` is divided by 100; `Prices` remains integer x1000; `NA` stays missing rather than becoming zero.
- Total lines are exact integer milli-goals, so `2.5` is keyed as `2500` rather than by floating-point identity.
@@ -61,4 +61,4 @@ The full local archive import completed in 73.6 seconds and created `data/resear
Gamma rolling discovery is connected to the mapping-candidate writer. A July 10 smoke found 57 open World Cup match-family events, refreshed 48 candidate assets, merged them with the research registry, and produced zero tradeable assets.
-The synchronized Spain–Belgium capture will still be replayed through the product bus and used for the Phase-3 live lane. `STALE_QUOTE` remains paper-only because historical sampled prices do not contain executable bid/ask depth.
+The synchronized Spain–Belgium capture was replayed through the product bus and used for the live-lane feasibility study. Across 18 goal×market observations, 12 had already moved before TXLine and 6 had no material reprice; none established a clean post-TXLine stale window. `STALE_QUOTE` remains disabled, and the capture did not enter an execution or Claude runtime.
diff --git a/docs/PHASE-2-STATUS.md b/docs/PHASE-2-STATUS.md
index 64842d1..45c2a1c 100644
--- a/docs/PHASE-2-STATUS.md
+++ b/docs/PHASE-2-STATUS.md
@@ -12,6 +12,7 @@
- Threshold grids are injected, hashed, and evaluated in parallel. Future labels are withheld until an explicit horizon elapses, then applied to the earlier prediction; pending cases are bounded by the horizon and discarded if no fresh resolution point arrives.
- The current post-hoc labels distinguish cross-market gap convergence, Polymarket reversion while consensus stays fixed, and Polymarket following a sustained consensus direction. Label horizons and minimum movements remain Phase-3 parameters.
- The dynamic main-total selector is evidence-driven and parameterized. It can weight distance from 50/50, log volume, log liquidity, and history coverage; it fails closed when no exact TX-observed full-time line passes the injected eligibility requirements.
+- The paired-capture live-lane analyzer streams captured TXLine frames and Polymarket NDJSON through the production normalizers, reconstructs canonical top-of-book state and best-level depth, excludes explicit reconnect windows, and measures score-event delivery against every exact mapped market group without loading the 3.3 GB capture into memory.
## Real-archive smoke evidence
@@ -34,13 +35,25 @@ This early result is useful precisely because it rejects an assumed “3–4 poi
The total-line evidence builder produced 590 TX-observed full-time lines across all 98 mapped fixtures, with a latest Over probability at least five minutes before kickoff for every line. A balance-only exploratory rule exactly reproduced the Phase-0 closest-to-50/50 distribution: O/U 2.5 for 77 fixtures, 3.5 for 15, 1.5 for 5, and 4.5 for 1. Across balance-only, equal-weight, and balance-heavy exploratory schemes, weighting changed the winner for one fixture (`17588320`) and 69/98 fixtures still showed disagreement among the raw balance/volume/liquidity/coverage criteria. This is evidence for a stable candidate rule, not approval to freeze it. Archived post-close Gamma liquidity is often zero, so the gate must not treat it as historical executable depth.
+The synchronized Spain-Belgium run added 2,262,950 normalized canonical Polymarket events across Match Result and five exact full-time totals groups. TXLine delivered three unique goals with first-seen receive latency of -8 to 49 ms; confirmation revisions arrived 74–162 seconds later. At an exploratory 50-probability-bps material-move threshold, Polymarket had already moved during the five seconds before TXLine first delivery in 12/18 market-event cases. The other 6/18 showed no material move inside 30 seconds, and 0/18 showed a clean post-TXLine repricing case without a prior move. This match does not support the STALE_QUOTE hypothesis. Eight public-WebSocket outages totaled 91.171 seconds and are marked unavailable rather than unchanged-market periods. Full evidence is in `docs/research/paired-spain-belgium-2026-07-10-live-lane.md`.
+
+## Historical gate evidence
+
+**July 14 correction:** the dynamic-total historical result described below is invalidated for decision use. The selector used a Polymarket observation after the eligible signal window for 95/98 fixtures, and its coverage calculation was not constrained to the same as-of cutoff. The reported sample also mixed unsupported sells and complementary expressions of the same economic move. The original values remain below as audit history only; no totals candidate is currently approved. See `docs/research/historical-gate-study-v1-invalidation.md`.
+
+**Corrected rerun:** `docs/research/historical-gate-study-causal-economic-v4.md` uses a T−180-minute selector cutoff, zero future/coverage violations across 590 evidence rows, normalized buy-only Total Goals cases, and fixture-clustered uncertainty. The prespecified Total Goals `CONSENSUS_MOVE` family supports a fresh forward paper study (38 held-out cases / 18 fixtures; `+132.7 bps` after the proxy; 95% CI `+14.3` to `+243.9`), but it is sampled-price signal evidence, not alpha or fill proof. Deborah registered paper v2 for forward observation only on July 18; no qualifying v2 observation existed at registration.
+
+The chronological historical study used 68 training fixtures and 30 sealed heldout fixtures. Training had both sources for 136/136 market groups; heldout had both for 52/60 groups. Configurations were selected only on training labels, required at least 30 predicted-positive training cases, and could not use a detector gap below the 100 probability-bps cost proxy.
+
+The original v1 run produced the numerical outputs retained in `docs/research/historical-gate-study.md`, but the dynamic-total slice does not advance to human review after the causal audit. `XMARKET_DIVERGENCE`, `FADER_CANDIDATE`, and Match Result `CONSENSUS_MOVE` remain rejected on their existing evidence. Dynamic-total `CONSENSUS_MOVE` returns to unapproved research status until a new causal study is completed. The real-money gate remains closed.
+
## Verification
-`pnpm check` passes under Node `22.23.1` with 33 tests across ten files. `pnpm build` also passes. Tests cover acceleration, sampled-history research gating, archive event reconstruction, no-mode replay identity, future-label isolation, bounded deferred scoring, grid expansion, dynamic total-line ranking/fail-closed behavior, detector behavior, storage integrity, mappings, and source normalization.
+`pnpm check` passes under Node `22.23.1` with 39 tests across twelve files. `pnpm build` also passes. The Phase 0 recorder has an additional Node test that proves an open SSE response is aborted at the capture deadline. Tests cover acceleration, sampled-history research gating, archive event reconstruction, no-mode replay identity, future-label isolation, bounded deferred scoring, grid expansion, dynamic total-line ranking/fail-closed behavior, chronological split isolation, detector behavior, storage integrity, mappings, source normalization, outage pairing, latency histograms, and recorder shutdown.
## Still open
-- Fit and freeze the dynamic main-full-time-total selector on train/test evidence; do not hard-code O/U 2.5 or treat current exploratory weights as production values.
+- Review and either freeze or reject the dynamic main-full-time-total candidate; do not hard-code O/U 2.5 or treat the candidate as production before Deborah's decision.
- Add `MODEL_MARKET_GAP` only behind a fitted model and valid calibration certificate. No hand-set match-state multipliers are allowed.
-- Add `STALE_QUOTE` and score-event latency labels from synchronized TXLine plus executable Polymarket book data. Historical one-minute samples cannot support this detector.
-- Run the full train/test Phase-3 gate study and freeze thresholds only after review with Deborah.
+- Repeat synchronized live-lane measurement on any remaining mapped matches if practical. `STALE_QUOTE` remains disabled: Spain-Belgium supplied score-event latency labels but no supporting post-TXLine stale-window case.
+- Review the totals-only `CONSENSUS_MOVE` candidate with Deborah, collect executable-book evidence, and freeze or reject its threshold. `XMARKET_DIVERGENCE`, Match Result `CONSENSUS_MOVE`, and `FADER_CANDIDATE` are no-go for v1 on current evidence.
diff --git a/docs/PHASE-4-STATUS.md b/docs/PHASE-4-STATUS.md
new file mode 100644
index 0000000..439cbaa
--- /dev/null
+++ b/docs/PHASE-4-STATUS.md
@@ -0,0 +1,85 @@
+# Phase 4 — Reasoning and Paper Execution Status
+
+*Started July 12, 2026. The real-money gate is closed.*
+
+> **July 18 governance status:** the signed v1 paper protocol remains invalidated and suspended because its upstream market selector was future-informed. Its zero-observation ledgers remain preserved as audit history and cannot admit cases. Deborah registered the exact corrected v2 configuration for forward paper observation only under `paper-study-v2-2026-07-18`; fresh versioned v2 ledgers are required, the real-money gate remains closed, and registration does not itself admit a fixture or create a performance result.
+
+## Implemented vertical slice
+
+- Strict Zod contracts define triage and analyst outputs. The thesis schema is closed to unknown fields and contains no stake, bankroll, order, wallet, or venue-authentication field.
+- Bounded Anthropic Messages adapters use `claude-haiku-4-5` for triage and `claude-opus-4-8` with adaptive thinking for analysis. Haiku is forced through one strict `submit_triage` call; Opus cannot combine adaptive thinking with forced tool choice, so code accepts exactly one strict `submit_thesis` call and rejects prose, missing/duplicate/wrong tools, refusal, or truncation.
+- Prompt versions, input/output limits, request deadlines, retry limits, and idempotency keys are code-owned. The analyst schema also binds model identity and a 15-minute thesis window; sizing or order fields remain impossible.
+- A separate append-only SQLite spend ledger atomically reserves conservative worst-case cost before each API request, settles measured uncached/cache/output usage at the current model rates, and charges the full reservation when billing is unknown. The `$300` hard project ceiling is checked before network I/O.
+- One production composition factory shares the bounded Claude client and spend ledger across isolated bounty/long-run decision ledgers. It rejects multiple credential sources, a spend ceiling above `$300`, shared lane ledgers, or a fixture universe built for a different long-run start.
+- The thesis identity, market, outcome, and direction must exactly match the originating detector signal.
+- The implemented paper risk configuration is code-owned: `$50` bankroll, `$3` fixed stake, `$15` aggregate exposure, `$20` drawdown rejection stop, totals-only `CONSENSUS_MOVE`, live-book evidence, and a closed real-money gate. The v1 registration is preserved but suspended; the repaired v2 configuration was registered for forward paper observation on July 18.
+- Deterministic risk rechecks the locked gap/z-score/CUSUM evidence, market family, score context, thesis expiry, post-latency book identity/freshness, current fee metadata, exposure, drawdown, and owned inventory for sells.
+- Deterministic risk independently vetoes every `research_only` signal even if upstream fixture filtering is bypassed.
+- The paper adapter never calls a venue. It walks canonical asks/bids, honors the deterministic limit, supports partial/no fills, applies per-market taker fees, records entry half-spread, executable displayed depth, and realized slippage, and stores all USD values as integer micro-units.
+- A separate SQLite decision ledger hash-chains signal, triage, thesis, risk verdict, execution intent, paper result, and terminal state. Update/delete triggers enforce append-only storage.
+- The orchestrator records the risk verdict and execution intent before invoking the paper adapter. Agent failure, schema failure, mismatch, stale evidence, malformed fees, and execution errors terminate fail-closed.
+- Live and captured-replay adapters now expose the same async canonical-event source contract to one `runPaperSession` conductor. Source kind and provenance stay at the adapter/evidence boundary; no replay/live mode field is exposed to strategy code. The conductor verifies the decision chain before and after a run and returns a terminal summary rather than silently accepting partial input.
+- The scheduler creates lane-specific bounty/long-run case identities, records the signal before model I/O, measures actual triage+analysis wall time, deterministically stamps thesis completion/expiry in event time, ignores books before that completion, executes against the first eligible pre-kickoff canonical book, updates paper exposure from actual simulated consideration, and terminally expires cases with no executable book. Analysis that reaches kickoff cannot become an in-play entry.
+- One shared frozen config now owns the signed feature windows, detector threshold, 15-minute pre-kickoff cutoff, and dynamic-total selector. The canonical runtime sends every event to existing pending cases first, then features/detectors, and permanently suppresses duplicate signal IDs.
+- The unauthenticated V2 CLOB resolver validates condition/token identity and caches explicit fee rate/exponent/taker-only, minimum-order, and tick-size parameters. Legacy `base_fee`-only shapes are rejected rather than converted by assumption.
+- Captured book freshness remains event-time based while fee-metadata freshness is checked against processing wall time, so offline replay neither rewrites captured timestamps nor falsely rejects newly fetched condition metadata.
+- Paper execution enforces the venue's minimum shares and tick grid and rounds fees conservatively to the documented five-decimal USDC precision.
+- The paper portfolio records actual entry cost, distinguishes kickoff midpoint CLV from executable bid-side liquidation CLV, settles binary payouts into integer-micro-unit P&L/Brier, and feeds realized drawdown and open exposure back into risk state.
+- Capture-order replay rejects regressing source timestamps atomically before stale TXLine observations can affect CUSUM/de-vig state or stale Polymarket books can overwrite the current quote. Per-source rejection counts remain observable.
+- The kickoff close scheduler caches only canonical books whose venue timestamp is at or before kickoff, refuses capture-order regressions, and records cutoff, source, observation, and processing timestamps separately.
+- The public Polymarket `market_resolved` frame is canonicalized with condition and winning asset identity. The settlement scheduler pays a paper position only when its exact asset won and refuses to settle a position that lacks its registered kickoff close.
+- The implemented evaluator can rebuild eligible signals, fills, closes, and settlements from an append-only ledger. If a corrected protocol is registered, bounty output remains exploratory and long-run rows/endpoints stay sealed until 20 filled matches and 40 fills, then use a seeded 10,000-iteration match-block bootstrap plus fill-rate, slippage, depth, settlement, and drawdown guardrails. None of those future-v2 endpoints is active now.
+- The preserved v1 bounty and long-run SQLite ledgers start with one hash-chained `study_initialized` record containing every frozen feature, detector, selector, risk, stopping, bootstrap, and real-money-gate setting. The v1 long-run start is `2026-07-12T09:45:17.212Z`; its initial report is sealed at zero matches/signals/fills. These ledgers cannot admit v2 cases.
+- The generated fixture universe physically separates evidence grades. Spain-Belgium has synchronized paired books but starts inside the 15-minute cutoff, so it is lifecycle-only; Norway-England and Argentina-Switzerland have sampled prices only, so they are signal-research-only. None can enter the long-run stopping count.
+- The persistent lane factory exposes only strategy-executable fixture keys to the paper scheduler. Sampled-history and late-start paired captures cannot reach risk or execution even if replayed on the canonical bus.
+- Rolling admission plans both lanes before mutation, then adds only post-start fixtures with verified mappings and pre-match paired executable books to the scheduler and kickoff-close maps. Removing or changing an admitted fixture fails closed; `research_only` signals are rejected before any Claude request.
+- `paper:report` reconstructs machine-readable JSON and the exact protocol-configured per-match Markdown evidence table from the decision ledgers. Bounty evidence stays visibly exploratory and sealed long-run reports carrying rows/endpoints are rejected as invalid.
+- Real Anthropic responses can emit hash-only invocation metadata (prompt, response, usage/cost, model, status, and evidence-reference hashes) into a separate append-only evidence ledger. Receipt generation accepts a reference only when that local chain verifies and the exact run is present. The portable offline verifier checks that disclosed reference for internal consistency but does not independently prove ledger membership. This remains neither provider attestation, an Anthropic-signed receipt, independent billing verification, nor proof that a response was good.
+- The deterministic 20-event full-lifecycle judge fixture now executes through `runPaperSession`, including signal, risk, paper fill, kickoff close, settlement, and receipt generation. Its triage and analyst boundaries are deterministic stubs, every external-call counter is zero, and the result remains permanently `excluded_synthetic`.
+- A read-only authenticated TXLine refresh exact-matched France-Spain fixture `18237038` and England-Argentina fixture `18241006` to their public Polymarket match/totals families at 19:00 UTC on July 14 and 15. Deborah confirmed capture-only use on July 12. Both mappings remain `tradeable: false`.
+- France-Spain verification failed closed on July 15, 2026 at `00:23 UTC`: the capture artifacts stop at `2026-07-14T18:18:03Z`, before kickoff; the run log has no completion marker; the PID file remained behind; no public `market_resolved` event was captured; and the local candidate registry still has zero evidence-bearing mapping records for fixture `18237038`. The fixture remains inadmissible for canonical paired replay or any study lane.
+- England-Argentina verification failed closed on July 16, 2026 at `00:17 UTC`. The supervisor started within the allowed window before the `2026-07-15T18:45:00Z` signal cutoff, but recorded a Polymarket stall and marked the run failed at `2026-07-15T20:16:33.299Z`. The symlinked raw files contain observed traffic into the match, yet both terminal manifests remain stuck at `status: "running"` with `endedAt: null`; the Polymarket terminal manifest reports zero messages/connects/in-scope books despite a nonempty capture file, the TXLine terminal manifest never finalized its stream summaries, the score stream never reached `game_finalised`, no public `market_resolved` event was captured, and the local candidate registry still has zero evidence-bearing records for fixture `18241006`. The sealed analysis manifest is `failed_closed`, so the fixture is inadmissible for canonical paired replay or any study lane.
+
+## Explicit boundary
+
+This slice starts from a canonical event source or an emitted `DetectorSignal`; code ledgers a signal before model work, runs bounded triage/analysis, and records measured readiness before the scheduler can accept the first later pre-kickoff `PolymarketBookEvent`. Triage and analyst executors remain injected interfaces, with bounded Anthropic implementations and deterministic test doubles. The Claude path has no Polymarket authentication, wallet access, approval, token action, deposit, or order-placement capability. V2 registration removes only the human protocol-status block: real Anthropic/runtime admission still fails closed until a fresh fixture satisfies every mapping, capture, lifecycle, timing, and immutable-input gate.
+
+The following work remains before the Phase 4 replay exit criterion is met:
+
+- Expand the analyst's code-assembled evidence bundle and only then evaluate whether bounded read-only evidence tools earn their latency and cost.
+- England-Argentina failed closed after verification. V2 is now registered, but only a fresh capture could reopen admission review; do not append anything to a study ledger or spend Claude tokens on this failed fixture.
+- France-Spain remains blocked after failed verification. Registration does not cure its missing lifecycle evidence.
+- Complete the rendered/incognito dashboard validation on the final public commit; a successful static build is not visual proof.
+- Complete one eligible captured case through real Haiku/Opus, risk, paper result, close, and settlement only after the v2 registration and admission gates are satisfied. The synthetic conductor proof does not satisfy this evidence gate.
+
+## Verification
+
+Preserved pre-suspension engineering verification—not active v2 study evidence—records that `pnpm test` passed under Node `22.23.1` with 102 tests across 27 files; full build and Phase 0 verification are rerun after each integration change. Production composition readiness opened the actual spend, bounty, long-run, and fixture-universe stores with the key configured, made zero API requests, and left all three chain heads unchanged. Both eligible fixture lists were empty. `paper:report` generated `docs/research/paper-study-current.md` from the real one-row v1 lane ledgers: bounty was exploratory at zero observations and long-run was sealed at zero without leaking rows or endpoints. A Haiku 4.5 synthetic triage smoke dropped the non-executable signal for `$0.001641`. An Opus 4.8 adaptive-thinking smoke returned exactly one valid `submit_thesis`, selected `no_trade` for research-only proxy evidence, and cost `$0.0460475`. Successful-call measured cost was `$0.0476885`; the append-only spend ledger conservatively reports `$0.29935725` because the first pre-classification 400 schema rejection was charged at its full `$0.25166875` reservation rather than rewritten after error classification was added. No reservation remains outstanding and the eight-entry chain verifies. These historical calls predate v2 registration and do not authorize a new request by themselves. A public, unauthenticated CLOB smoke resolved a captured condition to fee rate `0.05`, exponent `1`, taker-only fees, minimum order size `5`, and its condition-specific tick size.
+
+An actual Spain-Belgium capture frame normalized to fixture `18218149`, full-time total `1.5`, condition `0xd143...00c9`, and winning asset `138369...93641` (`Over`). Its venue timestamp was 64 ms ahead of local receipt; both timestamps are retained rather than rewritten.
+
+Focused paper-pipeline tests prove:
+
+- unknown analyst sizing fields are rejected;
+- missing, duplicate, wrong, prose-only, and truncated Claude submissions fail closed; budget exhaustion prevents the Anthropic client call;
+- API failure is redacted and conservatively charged at the reserved maximum;
+- ineligible fixtures invoke neither Haiku nor Opus, while both eligible lanes share one bounded spend chain and retain separate decision chains;
+- `signal_received` is durable while a slow triage request is unresolved, measured analysis latency is ledgered, and a book one millisecond before readiness cannot execute;
+- a pre-match signal whose analysis reaches kickoff is terminally closed and cannot become an in-play paper entry;
+- long-run admission requires all three gates: post-start kickoff, verified mapping, and pre-match paired executable books;
+- admitted fixture identities cannot be removed or changed during refresh, and repeated admission is idempotent;
+- evidence rendering refuses sealed long-run output containing rows, endpoints, or guardrails;
+- fee freshness uses processing time during captured replay without changing canonical event timestamps;
+- research-only signals are vetoed again inside deterministic risk, not merely filtered by the scheduler;
+- a `$3` marketable buy walks multiple ask levels and includes the current sports taker-fee formula;
+- malformed fee metadata fails closed inside the adapter itself;
+- legacy fee shapes, wrong condition assets, off-grid books, and sub-minimum orders cannot become fills;
+- the `$20` drawdown stop vetoes the case;
+- the execution intent is present in the verified hash chain before the adapter runs;
+- early books are ignored, signals inside the 15-minute cutoff are rejected, and no-book cases expire with terminal ledger evidence;
+- kickoff midpoint/executable CLV and settlement P&L remain distinct, with settled losses updating the drawdown halt state.
+- regressing source timestamps are observable no-ops, not feature or detector inputs;
+- only a pre-kickoff canonical book can become the kickoff mark, even when capture order regresses;
+- public winning-asset resolution settles marked positions and cannot bypass a missing close;
+- long-run metrics remain sealed below either stopping threshold, while bounty metrics stay explicitly exploratory;
+- match-clustered bootstrap output is deterministic and a positive point estimate cannot override failed guardrails.
diff --git a/docs/UI.md b/docs/UI.md
new file mode 100644
index 0000000..cd88f20
--- /dev/null
+++ b/docs/UI.md
@@ -0,0 +1,610 @@
+# Samaritan UI/UX Specification
+
+*Status: Matchroom v2 dark hybrid approved by Deborah as the binding production direction on July 13, 2026. V1 remains preserved as a comparison baseline. Production frontend implementation may proceed against this contract; real-money gates are unchanged.*
+
+> **July 18 governance status:** paper-study v1 is invalidated, suspended, and preserved only as zero-observation audit history. Deborah registered corrected v2 for forward paper observation only. The dashboard must show registration separately from evidence: v2 has no qualifying observation until a fresh fixture passes every admission gate, and the real-money gate remains closed.
+
+## 1. Product position
+
+Samaritan is a **sports-market intelligence command center**. It is not an exchange, sportsbook, tipster feed, wallet, or generic AI assistant.
+
+The interface is optimized for this sequence:
+
+1. **Detect:** what changed in the sports and market data?
+2. **Judge:** why might the change matter?
+3. **Gate:** what did deterministic risk allow or reject?
+4. **Act:** what paper action was attempted, and was it filled?
+5. **Measure:** did the decision earn executable closing-line value after costs?
+6. **Prove:** can a user verify that the decision and result were not rewritten later?
+
+The public product promise is not "Samaritan predicts winners." The honest promise is:
+
+> Samaritan detects cross-market sports signals, subjects them to bounded machine judgment and deterministic risk, and produces a replayable, measurable, verifiable decision record.
+
+The primary visual object is a **match under analysis**, not a P&L number.
+
+## 2. Binding UX constraints
+
+These constraints come from Samaritan's architecture, evidence protocol, and hackathon terms. They are not optional visual preferences.
+
+1. **Read-only public access.** Judges and public observers must not need an account, wallet, token, payment, or browser wallet extension.
+2. **No public order entry.** The public app never displays a trade button, wallet connection, deposit flow, approval flow, or controls that imply Samaritan holds user funds.
+3. **No raw TXLine redistribution.** Public screens expose derived probabilities, signals, decisions, source labels, message references, validation status, and hashes. They do not expose or reconstruct the licensed raw feed.
+4. **No fake activity.** Live, captured replay, paper, exploratory, sealed, vetoed, expired, and no-trade states must be explicit.
+5. **No profitability claim before evidence.** CLV, fill rate, P&L, Brier score, costs, drawdown, and sample size appear together. Empty or sealed endpoints remain visibly empty or sealed.
+6. **Every action has provenance.** A signal cannot appear as a fill without the intervening triage, thesis, deterministic risk, and execution states.
+7. **Risk state is prominent.** `PAPER MODE` and `REAL-MONEY GATE CLOSED` remain persistent while those conditions are true.
+8. **Replay and live share one presentation model.** Replay controls time; they do not change the meaning, ordering, or shape of decision data.
+9. **Failure is visible.** Feed degradation, stale projections, unavailable books, model errors, and rejected cases are first-class states, not hidden behind blank charts.
+10. **Submission framing remains compliant.** Public copy presents Deborah as the participant/project owner and Claude as a bounded component. The entry is never described as "AI-built."
+
+## 3. Audiences and product surfaces
+
+### Public Observer - build first
+
+The observer is a judge, analyst, potential user, or teammate who needs to understand Samaritan without configuration or credentials.
+
+The observer can:
+
+- inspect live and captured matches;
+- follow a case from signal to result;
+- understand no-trade and risk-veto decisions;
+- review paper-study progress and measured outcomes;
+- inspect ledger integrity and on-chain proof links;
+- replay a verified sequence deterministically.
+
+The observer cannot mutate configuration, reveal credentials, alter the replay source, resume a halted system, or submit an order.
+
+### Private Control - later
+
+The operator surface belongs to Deborah and is separate from the public product. It will eventually contain feed controls, configuration, mapping confirmation, kill-switch state, and system governance. It must not be implemented by placing hidden admin controls inside public components.
+
+### Mobile Companion
+
+Mobile is an observation and alert surface, not a compressed desktop terminal. It prioritizes match state, the active signal, the Decision Rail, study progress, and proof links. Dense comparative analysis remains desktop-first.
+
+## 4. Information architecture
+
+The public navigation contains five destinations.
+
+| Destination | Job |
+|---|---|
+| **Command** | Answer "what is Samaritan watching and doing now?" |
+| **Matchroom** | Explain one fixture, its selected market, probability movement, and active decision lifecycle |
+| **Casebook** | Search and inspect every signal, escalation, veto, no-trade, fill, close, and settlement |
+| **Study** | Explain the invalidated v1 audit, corrected historical signal evidence, and the human gate before any v2 paper observations |
+| **Proof** | Verify ledger hashes, replay identity, source references, validation records, and the exact Solana anchor status |
+
+There is no marketing-only landing page in v1. The root route opens **Command**, with a compact explainer integrated into the product.
+
+## 5. Global shell
+
+Every primary screen uses the same shell:
+
+- compact Samaritan wordmark and section navigation;
+- global search for a fixture or case ID;
+- persistent system strip;
+- main content canvas;
+- last-updated timestamp and projection freshness;
+- direct access to the current demo replay when no live fixture is active.
+
+The system strip displays, at minimum:
+
+- mode: `LIVE`, `CAPTURED REPLAY`, or `OFFLINE ARTIFACT`;
+- execution: `PAPER MODE` or an accurately gated alternative;
+- feed health: TXLine, Polymarket, decision runtime, and proof writer;
+- real-money gate state;
+- last canonical event time;
+- end-to-end signal decision latency when available;
+- Claude spend state as a bounded operational metric, not as a promotional gimmick.
+
+Status must use text and iconography in addition to color.
+
+## 6. Signature components
+
+### Match Masthead
+
+Displays teams, home/away identity, competition text, scheduled kickoff or live clock, current score, market period, and selected exact market line. It must distinguish full-time Match Result from full-time totals and must never imply an extra-time market.
+
+### Probability Field
+
+The main chart compares derived fair consensus with the executable Polymarket market:
+
+- TXLine-derived fair probability;
+- executable best bid and ask, not only midpoint;
+- optional midpoint as secondary context;
+- selected signal direction and threshold;
+- market gap in probability points and basis points;
+- feed outages and unavailable-book intervals;
+- score and material match events when licensed derived context permits;
+- entry, close, and settlement markers;
+- chart time and replay time locked to the same clock.
+
+This is a probability chart, not a crypto candlestick chart.
+
+### Decision Rail
+
+The Decision Rail is Samaritan's signature interaction. It renders the append-only case lifecycle as an ordered, timestamped sequence:
+
+1. `SIGNAL DETECTED`
+2. `TRIAGE DROPPED` or `TRIAGE ESCALATED`
+3. `THESIS SUBMITTED` or `NO TRADE`
+4. `RISK VETOED`, `RISK SHRUNK`, or `RISK APPROVED`
+5. `PAPER INTENT LEDGERED`
+6. `FILLED`, `PARTIALLY FILLED`, `NOT FILLED`, or `EXPIRED`
+7. `CLOSE CAPTURED`
+8. `SETTLED`
+9. `PROOF ANCHORED`
+
+Each stage expands to show only the evidence appropriate to that stage. Claude output appears as a structured thesis with confidence, invalidation, and evidence references. It is never presented as a chatbot conversation.
+
+### Signal Tape
+
+A chronological tape of candidate cases across fixtures. Each row includes time, fixture, market, detector, direction, gap, disposition, and age. Repeated movements that triage deduplicated remain traceable to the surviving case.
+
+### Evidence Stamp
+
+A compact, high-contrast label used on cases and reports:
+
+- `PAPER`
+- `EXPLORATORY`
+- `LONG-RUN SEALED`
+- `VERIFIED REPLAY`
+- `NO TRADE`
+- `RISK VETO`
+- `FEED DEGRADED`
+- `REAL-MONEY GATE CLOSED`
+
+Stamps are semantic states, not decorative badges.
+
+### Study Meter
+
+Shows the stopping rule without revealing sealed endpoints early:
+
+- matches with an eligible signal vs required matches;
+- signals vs required signals;
+- study start time;
+- lane identity: bounty or long-run;
+- sealed/unsealed state;
+- drawdown-stop state;
+- config hash.
+
+### Integrity Panel
+
+Links the visible case to ledger sequence, previous hash, record hash, replay identity, source message references, validation status, and Solana anchor when available. Long hashes truncate visually but remain copyable and fully available in the detail view.
+
+## 7. Screen specifications
+
+### Command
+
+**Purpose:** establish system status and route the user to the most important fixture or case within ten seconds.
+
+Required content:
+
+- current system strip;
+- optional live derived TXLine connectivity pulse, labelled `NOT STUDY EVIDENCE`, with a degraded state when server-side SL12 credentials are absent or invalid;
+- active and upcoming verified fixtures;
+- strongest recent signals, including vetoes and no-trades;
+- current paper-study stopping-rule progress;
+- latest verified evidence artifact;
+- one sentence explaining what Samaritan does;
+- one primary action: `Watch active match` or `Replay verified case`.
+
+The Command screen must not be a grid of equal-weight KPI cards. One active match or verified replay is the visual lead.
+
+Low-fidelity composition:
+
+```text
++-----------------------------------------------------------------------+
+| SAMARITAN | COMMAND | MODE | FEEDS | GATE | LAST EVENT |
++-----------------------------------------------------------------------+
+| |
+| ACTIVE MATCH / FEATURED VERIFIED REPLAY |
+| France 0 - 0 Spain | 42:18 | Full-time O/U 2.5 |
+| |
+| Fair 55.2% | Ask 51.4% | Gap +3.8pp | SIGNAL UNDER REVIEW |
+| [Watch match] mini probability |
+| field |
++--------------------------------------+--------------------------------+
+| RECENT CASES | PAPER STUDY |
+| 12:41 CONSENSUS_MOVE ESCALATED | 8 / 20 matches |
+| 12:32 CONSENSUS_MOVE NO TRADE | 17 / 40 signals |
+| 12:08 FEED DEGRADED HALTED | LONG-RUN SEALED |
+| [Open Casebook] | [Open Study] |
++--------------------------------------+--------------------------------+
+| LATEST PROOF | ledger head | replay identity | anchor status |
++-----------------------------------------------------------------------+
+```
+
+### Matchroom
+
+**Purpose:** make one autonomous decision understandable and auditable.
+
+Desktop composition:
+
+```text
++-----------------------------------------------------------------------+
+| SAMARITAN | MODE | FEEDS | GATE | LATENCY | LAST EVENT |
++-----------------------------------------------------------------------+
+| FRANCE 0 FULL-TIME O/U 2.5 0 SPAIN 42:18 |
++---------------------------------------------+-------------------------+
+| | DECISION RAIL |
+| CONSENSUS VS EXECUTABLE MARKET | |
+| | 12:40 signal detected |
+| TXLINE FAIR 55.2% | 12:40 triage escalated |
+| BEST ASK 51.4% | 12:41 thesis submitted |
+| EXECUTABLE GAP +3.8pp | 12:41 risk approved |
+| | 12:42 paper fill |
++---------------------------------------------+-------------------------+
+| SIGNAL TAPE |
++-----------------------------------------------------------------------+
+| STUDY 8/20 MATCHES | 17/40 SIGNALS | LONG-RUN SEALED | CONFIG HASH |
++-----------------------------------------------------------------------+
+```
+
+The chart receives the largest area. The Decision Rail remains visible while inspecting chart points or replaying the case.
+
+Mobile composition:
+
+```text
++--------------------------------+
+| SAMARITAN | PAPER | GATE CLOSED|
++--------------------------------+
+| FRANCE 0 - 0 SPAIN 42:18 |
+| FULL-TIME O/U 2.5 |
++--------------------------------+
+| ACTIVE SIGNAL |
+| Fair 55.2% Ask 51.4% +3.8pp |
++--------------------------------+
+| COMPACT PROBABILITY FIELD |
+| |
+| |
++--------------------------------+
+| DECISION RAIL |
+| 12:40 Signal detected |
+| 12:40 Triage escalated |
+| 12:41 Thesis submitted |
+| 12:41 Risk approved |
++--------------------------------+
+| STUDY 8/20 | 17/40 | SEALED |
+| [Inspect proof] |
++--------------------------------+
+```
+
+### Casebook
+
+**Purpose:** prove that Samaritan records unfavorable, rejected, and incomplete cases as faithfully as successful ones.
+
+**Current evidence boundary:** the shipped offline Casebook is not yet the future operational decision ledger described below. It projects all 18 goal×market feasibility observations reported by the one verified Spain-Belgium capture replay (12 pre-trigger moves, 6 without a material 30-second move, 0 clean stale windows). A deterministic Match Result observation is the only expanded three-state detail exemplar; the UI labels that selection and does not present it as the whole corpus or as performance evidence.
+
+Filters:
+
+- fixture;
+- market family and exact line;
+- detector;
+- lifecycle disposition;
+- execution outcome;
+- evidence lane;
+- live/replay source;
+- date range.
+
+Default sorting is newest decision time. Success-only filtering is never the default. Case detail opens the complete Decision Rail, executable book evidence, costs, thesis, steelman/invalidation, risk reasons, hashes, and measured outcome.
+
+Low-fidelity composition:
+
+```text
++-----------------------------------------------------------------------+
+| SAMARITAN | CASEBOOK | MODE | FEEDS | GATE |
++-----------------------------------------------------------------------+
+| Search cases... | Fixture | Detector | Disposition | Lane | Date |
++--------------------------------------+--------------------------------+
+| CASE INDEX | SELECTED CASE |
+| | |
+| 12:41 France-Spain | CASE PS-000184 |
+| O/U 2.5 | CONSENSUS_MOVE | France-Spain | O/U 2.5 |
+| ESCALATED -> RISK APPROVED | EXECUTABLE GAP +3.8pp |
+| | |
+| 12:32 England-Argentina | DECISION RAIL |
+| O/U 2.5 | CONSENSUS_MOVE | signal -> triage -> thesis |
+| NO TRADE | -> risk -> fill -> close |
+| | |
+| 12:08 Spain-Belgium | THESIS / INVALIDATION |
+| FEED DEGRADED -> HALTED | structured public evidence |
+| | |
+| [Load older cases] | COSTS / RESULT / PROOF |
++--------------------------------------+--------------------------------+
+```
+
+### Study
+
+**Purpose:** show why v1 was withdrawn, what corrected historical evidence supports, and why no active paper protocol exists until Deborah registers v2.
+
+Required views:
+
+- stopping-rule progress;
+- executable CLV distribution and match-clustered confidence interval;
+- random-direction and no-trade baselines;
+- fill rate and modeled/actual slippage;
+- spread, fees, and total cost decomposition;
+- per-match P&L table;
+- equity and drawdown curve;
+- Brier score where applicable;
+- clear `ACCEPT`, `REJECT`, `INCOMPLETE`, or `SEALED` conclusion generated from deterministic protocol rules.
+
+The page must keep sample size and lane identity visible alongside every headline result.
+
+### Proof
+
+**Purpose:** show that the visible history is append-only and replayable.
+
+Required views:
+
+- ledger head and segment identity;
+- hash-chain verification status;
+- replay event count and identity hash;
+- source-reference validation status;
+- Solana anchor status, with an explorer link only when an actual transaction has been submitted and verified;
+- explicit distinction between local verification, TXLine validation, and on-chain anchoring.
+
+The bounty release is intentionally unanchored and displays `NOT SUBMITTED`; it must not show a placeholder or fake explorer link. A future separately authorized anchor may replace that state only after network verification succeeds.
+
+## 8. Visual direction: Live Intelligence
+
+Live Intelligence is a polished sports-analytics product, not a trading terminal or editorial match report. Matchroom v2 uses approximately **70% SportsTensor spatial hierarchy** for composition, whitespace, chart organization, evidence tables, and modular rhythm, with approximately **30% Live Matches sporting energy** for the score-first masthead, dark operational character, lime active states, and compact match tension.
+
+Neither reference is reproduced directly. Samaritan's Decision Rail, replay identity, evidence provenance, paper state, and deterministic risk boundaries remain the product's signature.
+
+The interface should feel native to an elite match-analysis room: immediate score context, restrained market data, and a visible decision outcome. It must not resemble a casino, crypto exchange, generic admin template, or faux command center. There are no photographic hero sections, oversized editorial headlines, beige report canvases, glowing sci-fi grids, or decorative market theatrics.
+
+### Color tokens
+
+| Token | Value | Use |
+|---|---:|---|
+| `navy-1000` | `#050716` | Sidebar and deepest application surface |
+| `navy-950` | `#080B20` | Global workspace |
+| `navy-900` | `#0D112B` | Primary cards and analytical surfaces |
+| `navy-750` | `#1B244B` | Selected tabs and raised controls |
+| `text` | `#F7F8FC` | Primary text and score data |
+| `text-soft` | `#BBC2DD` | Secondary headings and table values |
+| `muted` | `#7D87AA` | Labels, timestamps, and explanations |
+| `signal` | `#98F244` | Active navigation, verified replay, TXLine fair series |
+| `market` | `#927FF2` | Polymarket ask series and spread region |
+| `context` | `#5B86FF` | Supporting metrics and selected evidence |
+| `risk` | `#FF6B74` | Veto, invalidity, closed execution, or failure |
+| `degraded` | `#FFAF58` | Delayed, partial, outage, or observational warning |
+
+Color meanings are fixed. Green must not mean "profitable" in one view and "feed healthy" in another without an accompanying label. Red is reserved for real risk, failure, invalidity, or veto.
+
+All foreground/background combinations must pass WCAG AA contrast before the palette is frozen in code.
+
+### Typography
+
+- **Manrope:** navigation, scores, teams, probabilities, headings, controls, and explanatory text.
+- **IBM Plex Mono:** timestamps, case IDs, prices, basis points, latency, ledger sequence, and hashes.
+
+Fonts should be self-hosted or packaged with the deployment. Numeric data uses tabular figures. Large score and probability typography may be expressive; evidence text remains restrained.
+
+### Geometry and density
+
+- 12-column desktop grid with a fluid content width up to approximately 1600 px;
+- 20-24 px desktop gutters, 12-16 px compact gutters;
+- 4 px base spacing rhythm;
+- 14-18 px default panel radius;
+- quiet one-pixel blue-gray borders with shallow panel shadows;
+- one dominant analytical panel per screen;
+- limited use of pills, reserved for filters and compact states;
+- no glassmorphism, floating orbs, excessive glow, decorative 3D assets, or editorial poster composition.
+
+### Iconography
+
+Use a consistent outlined icon family for navigation and state. Do not use team or FIFA trademarks without clear rights. Flags, generic team initials, and plain text remain acceptable fallbacks.
+
+## 9. Data visualization rules
+
+1. Probability is the universal display currency and is shown as a percentage at the edge of the UI.
+2. The y-axis never truncates in a way that exaggerates small probability moves without an explicit visual break.
+3. Best bid and ask are visually distinct from midpoint and fair consensus.
+4. Executable CLV is primary; midpoint CLV is secondary and labeled.
+5. Feed outages are gaps or shaded unavailable intervals, never interpolated as unchanged prices.
+6. Entry, close, and settlement markers are tied to ledger timestamps.
+7. Charts expose exact values through keyboard-accessible focus or a data table alternative.
+8. Series use both color and line treatment, marker, or label so color vision is not required.
+9. Confidence intervals show the estimate, lower and upper bound, bootstrap unit, and sample size.
+10. No decorative chart is permitted. Every visual must answer a named product question.
+
+## 10. Motion
+
+Motion communicates state transitions rather than decoration.
+
+Allowed motion:
+
+- probability lines updating with restrained interpolation;
+- a new signal entering the Signal Tape;
+- a case advancing along the Decision Rail;
+- replay cursor movement and event markers;
+- a short page-load reveal that establishes hierarchy.
+
+Avoid constant pulsing, animated gradients, card hover theatrics, confetti, fake terminal typing, and ambient chart motion. Respect `prefers-reduced-motion`; replay must remain usable with motion reduced.
+
+## 11. Content design
+
+Copy is concise, factual, and evidence-led.
+
+Preferred:
+
+- `No executable book was available after decision latency.`
+- `Risk veto: aggregate fixture exposure would exceed $15.`
+- `Long-run endpoints remain sealed until the stopping rule is met.`
+- `Captured replay from July 10, 2026. Not live.`
+
+Avoid:
+
+- `AI found a guaranteed winner.`
+- `High-confidence profit opportunity.`
+- `The model knows Spain will win.`
+- `Connect wallet to continue.`
+
+Claude should be named only where its bounded role matters: `Haiku triage` and `Opus analysis`. Product copy emphasizes the system and evidence, not model personality.
+
+## 12. Responsive behavior
+
+### Desktop
+
+Desktop is the full analytical environment. Matchroom keeps the Probability Field and Decision Rail visible together. Wide tables may use sticky identity columns and horizontal scrolling inside their own region.
+
+### Tablet
+
+The chart remains first. The Decision Rail becomes a fixed-width adjacent panel in landscape and a full-width section below the chart in portrait. Global system health collapses into a labeled summary that expands on demand.
+
+### Mobile
+
+Mobile order:
+
+1. system mode and gate;
+2. match masthead;
+3. active signal summary;
+4. compact probability chart;
+5. Decision Rail;
+6. study progress;
+7. proof link.
+
+The complete signal table and dense study tables may switch to grouped rows, but no evidence field may disappear solely because of viewport size.
+
+## 13. Accessibility
+
+- Target WCAG 2.2 AA.
+- Every interactive element is keyboard reachable with visible focus.
+- Status never depends on color alone.
+- Live updates use restrained, appropriately scoped announcements rather than reading every tick.
+- Replay controls expose play/pause, speed, step backward, step forward, current time, and event list.
+- Charts provide summaries and tabular alternatives.
+- Touch targets are at least 44 by 44 CSS pixels on compact layouts.
+- User-selected zoom to 200% must preserve core workflows.
+
+## 14. Public data and technical boundary
+
+The browser consumes **derived, read-only projections**, never raw ledger files, raw TXLine captures, server credentials, wallet material, or execution adapters.
+
+The server-side projection layer should expose five conceptual view models:
+
+- `SystemProjection`: mode, health, gate, freshness, latency, and spend status;
+- `MatchProjection`: fixture identity, derived match state, selected market, and probability series;
+- `DecisionCaseProjection`: ordered lifecycle stages and public evidence;
+- `StudyProjection`: stopping rule, sealed status, metrics, guardrails, and per-match rows;
+- `ProofProjection`: public hashes, verification results, anchor status, and an explorer link only for a verified submitted transaction.
+
+These names describe frontend contracts; they do not freeze route names or storage schemas.
+
+Rules:
+
+1. Projection generation is deterministic from append-only artifacts.
+2. Secrets remain server-side.
+3. Raw TXLine payloads and reconstructable raw series are never serialized to public clients.
+4. Live updates carry derived projection deltas over a read-only stream.
+5. Replay emits the same projection shapes and lifecycle semantics as live.
+6. The client cannot mutate execution, risk, mapping, study, ledger, or anchor state.
+7. The UI displays server event time and projection-generation time separately when freshness matters.
+8. Unknown enum values fail visibly as `UNKNOWN STATE`; they are not silently mapped to success.
+
+## 15. Replay and demo mode
+
+The replay player is a core product capability because judges may review Samaritan after a match has ended.
+
+Required controls:
+
+- play and pause;
+- 0.5x, 1x, 2x, 5x, and 10x speed where the event density permits;
+- step to previous or next material event;
+- restart;
+- timestamp and original match date;
+- visible `CAPTURED REPLAY` label at all times.
+
+The demo sequence must use real captured or ledger-derived artifacts. A case may demonstrate a no-trade or veto when that is what the evidence produced. Spain-Belgium may honestly demonstrate synchronized capture, feed outages, and rejection of the stale-quote hypothesis; it must not be edited into a winning trade.
+
+Golden demo flow:
+
+1. Command establishes paper mode, evidence class, feed health, and the closed real-money gate.
+2. Matchroom shows the real Spain–Belgium capture and its justified no-trade result; it does not claim Claude or execution ran for that case.
+3. The separate, unmistakably synthetic proof panel emits one invented detector signal through deterministic Haiku-shaped and Opus-shaped stubs.
+4. Deterministic risk authorizes the paper-only synthetic intent after its ledger records are written.
+5. The paper adapter records the synthetic fill, close, settlement, and receipt; the case remains excluded from performance evidence.
+6. Study and Proof distinguish the real research result from the synthetic integrity demonstration.
+
+## 16. Design and implementation sequence
+
+The v1 and v2 comparison checkpoints remain in the private, gitignored `design/` workspace and are intentionally excluded from the public repository. Their approved dark-hybrid direction is implemented in the production dashboard; this public document does not link to unavailable private assets.
+
+The dark hybrid is the launch direction. Light mode is a deferred, non-blocking enhancement; the light style frame is its visual reference and must retain information-architecture and semantic-state parity when implemented.
+
+Both checkpoints use the verified Spain-Belgium capture and deliberately end in `NO TRADE`: the live-lane study found that Polymarket moved before TXLine and produced no clean stale-quote window. They are design prototypes, not production frontend code.
+
+### Production implementation status
+
+The five primary observer routes are implemented in [`apps/dashboard/`](../apps/dashboard/) and served by the read-only projection layer in [`src/dash/`](../src/dash/). Matchroom renders one deterministic exemplar from the verified Spain-Belgium replay, Command derives the operating posture and capture schedule, Casebook projects all 18 reported goal×market feasibility observations from that replay, Study separates registered v2 from preserved v1 audit history, and Proof verifies the synthetic receipt's portable commitments without claiming an external anchor.
+
+Implemented in this slice:
+
+- fail-closed fixture and replay identity validation;
+- derived-only public API with no raw TXLine payloads, asset IDs, source paths, credentials, or wallet controls;
+- responsive Match Masthead, Probability Field, Decision Rail, evidence sequence, and replay proof;
+- manual replay states and deterministic autoplay across `T-5 seconds`, `Goal first seen`, and `T+30 seconds`;
+- explicit captured-replay, paper, no-trade, and real-money-gate-closed states;
+- root-level Command surface with current system state, exact confirmed capture fixtures, the strongest verified case, and direct Matchroom entry;
+- server-side-only TXLine connectivity pulse with a strict seven-field derived response, four-second timeout, response-size cap, malformed-row rejection, GET/HEAD-only API, and explicit exclusion from v2 evidence;
+- fail-closed Command fixture admission against exact TXLine fixture and Polymarket event evidence;
+- searchable Casebook index over the complete current 18-observation feasibility corpus, with fixture, market, detector, disposition, execution, lane, source, and date filters;
+- explicitly selected Casebook exemplar detail with lifecycle, executable evidence, analyst/execution boundaries, observation sequence, and replay hashes;
+- honest no-result behavior and verified-capture admission that prevents scheduled fixtures or empty ledgers from appearing as cases;
+- sealed long-run study progress and compact integrity proof without exposing frozen-protocol profitability endpoints;
+- standalone Study with dual-lane separation, frozen candidate, sample clock, CLV decision rule, guardrails, risk caps, evidence admission, and ledger proof;
+- fail-closed Study result opening that requires the stopping rule, per-match rows, and reconciled guardrails before any long-run endpoint can enter the public contract;
+- deterministic validation against the frozen positive-CI, baseline, settlement, and guardrail requirements, while any unregistered candidate remains visibly non-admissible and the real-money gate remains closed;
+- packaged Manrope and IBM Plex Mono fonts, reduced-motion behavior, keyboard focus, and mobile evidence cards;
+- honest loading and fail-closed error surfaces.
+
+Expanded Proof is a standalone route over the frozen synthetic receipt. It labels the paper approval and settlement separately from the refused real-money path, recomputes the portable receipt commitment in-browser, and provides the exact offline verification command while keeping Solana visibly unsubmitted.
+
+Frontend work follows this order:
+
+1. **Annotated reference board:** capture the exact pattern taken and rejected from each source below.
+2. **Low-fidelity wireframes:** Command, Matchroom, and Casebook on desktop; Matchroom on mobile.
+3. **High-fidelity checkpoint:** one Matchroom with realistic Samaritan data and all major lifecycle states. V2 dark hybrid is approved and unlocks production dashboard implementation. Full paper-signal states follow after eligible ledgers contain evidence; light mode follows later without blocking launch.
+4. **State inventory:** loading, empty, sealed, no-signal, no-trade, veto, partial fill, degraded feed, stale projection, offline artifact, and anchor-not-submitted.
+5. **Clickable replay prototype:** one honest captured sequence using the final information hierarchy.
+6. **Comprehension test:** a new viewer explains what moved, what Samaritan decided, and whether money moved within 30 seconds.
+7. **Implementation:** shared shell, derived projection layer, replay, responsive states, and accessibility.
+8. **Visual QA:** desktop and mobile screenshots, reduced motion, keyboard flow, contrast, long labels, and real empty states.
+
+No implementation should invent placeholder profits, winning percentages, fabricated Claude explanations, or fake live matches for visual completeness.
+
+## 17. Acceptance criteria
+
+The v1 public UI is complete only when:
+
+- a new viewer can state what Samaritan does within 30 seconds;
+- the viewer can trace one case from signal to proof within three minutes;
+- live, captured replay, and offline artifact modes cannot be confused;
+- paper status and real-money gate state are persistent and correct;
+- no account or wallet is required;
+- no raw TXLine data or secret reaches the browser;
+- every case disposition, including no-trade and veto, is inspectable;
+- the Study screen preserves sealing and stopping-rule behavior;
+- the Proof screen verifies ledger/replay identity from generated artifacts;
+- the desktop and mobile golden paths are keyboard and touch usable;
+- the interface passes WCAG AA contrast checks;
+- the bounty demo can run from a verified replay if no match is live;
+- public copy makes no unsupported prediction or profitability claim.
+
+## 18. Reference board
+
+| Reference | Take | Reject |
+|---|---|---|
+| [Polymarket Sports](https://polymarket.com/sports/live) | Match scanning, league grouping, compact probabilities, score/time prominence | Order-entry emphasis and exchange identity |
+| [Nomos Discover](https://pro.nomos.trade/discover) | Persistent search, multi-source discovery, briefing rail, dense navigation | Generic black/blue treatment and excessive filter pills |
+| [Hudl StatsBomb IQ Live](https://statsbomb.com/what-we-do/iq-live/) | Match timeline, race chart, analytical drill-down, meaningful widgets | Professional complexity on the first screen |
+| [Hue & Machine sports dashboard](https://dribbble.com/shots/23905032-Sports-Analytics-Dashboard) | Hard-edged editorial grid and numerical hierarchy | Purple accent and tiny labels |
+| [Dynamic Sports Analytics](https://dribbble.com/shots/25514823-Dynamic-Sports-Analytics-Dashboard) | Controlled acid energy and strong contrast | Bubble cards, pie-chart prominence, and gamified casino tone |
+| [Live Matches & Stats](https://dribbble.com/shots/26818920-Sports-dashboard-Live-Matches-Stats) | **30% sporting-energy reference:** deep-navy character, luminous lime selection, score-first composition, and compact match tension | Betting economics, widget density, and decorative athlete tile |
+| [SportsTensor](https://dribbble.com/shots/26056873-Sportstensor-Sports-Analytics-Dashboard-Design-Betting-Liv) | **70% spatial reference:** analytical hierarchy, clean chart proportions, table organization, modular calm, and breathing room | Washed lavender as the primary skin, low-contrast labels, and generic prediction leaderboard |
+
+Dribbble references are visual inspiration only. Product architecture, error handling, evidence states, and accessibility come from Samaritan's real workflow and artifacts.
diff --git a/docs/proof/decision-receipt.md b/docs/proof/decision-receipt.md
new file mode 100644
index 0000000..0f16305
--- /dev/null
+++ b/docs/proof/decision-receipt.md
@@ -0,0 +1,37 @@
+# Samaritan Decision Receipt v1
+
+The Decision Receipt is Samaritan's portable, offline-verifiable record of one completed paper-decision lifecycle. It is generated only after the local decision ledger passes a full v2 hash-chain verification. The generator rejects mixed v1/v2 ledgers, incomplete or reordered lifecycles, and filled cases without a ledgered position.
+
+## What the receipt commits
+
+- the versioned code and frozen-config hashes;
+- SHA-256 references to private TXLine and public venue source records;
+- licence-safe signal evidence: 25-bps movement buckets, aggregate z/CUSUM state, categorical relative-value direction, and hashes—never an exact TXLine fair level or reconstructive cross-market gap;
+- triage and analyst model, prompt version, prompt/response commitments, token usage, and nano-USD cost evidence;
+- a SHA-256 commitment to the complete private submitted thesis plus its non-price identity/status fields, and the deterministic risk verdict with its fair-value limit kept private;
+- the paper intent, post-analysis/post-venue-delay book evidence, and fill or no-fill result; exact public Polymarket book/fill prices remain visible because they do not disclose a TXLine level;
+- the position, closing mark, and settlement when those lifecycle stages exist;
+- ordered v2 ledger-entry commitments and the full ledger head at generation time;
+- optional Solana memo metadata that must commit the same ledger head.
+
+The receipt's domain-separated canonical SHA-256 detects changes to every disclosed field. The offline verifier also recursively rejects exact fair/consensus probabilities, private deterministic limit probabilities, raw TXLine keys, and exact/reconstructive gap fields. It checks 25-bps bucketing, cross-field identity, lifecycle order, cost arithmetic, timing, fill/position state, source-reference roles, settlement order, and optional anchor consistency.
+
+## Proof boundaries
+
+`receipt:verify` is intentionally offline. It does not fetch private source payloads, reopen the local SQLite ledger, query Polymarket, or call Solana RPC. Its success means the strict schema, canonical receipt hash, disclosed lifecycle relationships, and committed ledger head are internally consistent. It does not independently prove that undisclosed source payloads existed, that the local ledger was honest when generated, that an anchor transaction is on-chain, or that a strategy is profitable.
+
+An unanchored receipt hash is tamper-evident, not an external timestamp or identity signature. Even when anchor metadata is present, the v1 verifier always reports `solanaNetworkVerificationPerformed: false`. Network verification belongs in a separate RPC-backed step.
+
+That separate offline-prepare / human-submit / read-only-verify workflow is documented in [Solana devnet Decision Receipt anchoring](solana-devnet-anchor.md). It leaves the source receipt immutable and reports network verification separately, so offline verification never overclaims an on-chain proof.
+
+## Synthetic proving fixture
+
+[`proof/fixtures/decision-receipt.synthetic.v1.json`](../../proof/fixtures/decision-receipt.synthetic.v1.json) is deterministic and prominently classified as synthetic. It represents no real match, feed observation, Claude invocation, order, fill, settlement, or performance. Its `performanceUse` is permanently `excluded_synthetic`.
+
+Verify it locally:
+
+```bash
+pnpm receipt:verify -- proof/fixtures/decision-receipt.synthetic.v1.json
+```
+
+The generator lives in `src/proof/decision-receipt.ts`; the strict schema and independent verifier live in `src/proof/decision-receipt-schema.ts`.
diff --git a/docs/proof/solana-devnet-anchor.md b/docs/proof/solana-devnet-anchor.md
new file mode 100644
index 0000000..44d4601
--- /dev/null
+++ b/docs/proof/solana-devnet-anchor.md
@@ -0,0 +1,125 @@
+# Solana devnet Decision Receipt anchoring
+
+Status: tooling implemented and unit-tested; **no transaction has been submitted by this implementation work**. A prepared intent is not an on-chain proof. Only a signature that passes the read-only verifier is an externally timestamped devnet anchor.
+
+This workflow anchors one immutable Samaritan Decision Receipt without exposing private TXLine payloads. The Solana Memo commits:
+
+- Decision Receipt schema version;
+- the receipt's canonical SHA-256 hash;
+- the verified decision-ledger head sequence and SHA-256 hash;
+- the commitment type and the literal `devnet` network.
+
+The canonical Memo is domain-separated as `samaritan.decision-receipt.anchor/v1` and encoded with Samaritan stable JSON. The transaction uses the official Solana Memo program, `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`.
+
+## Safety boundary
+
+The workflow deliberately separates three operations:
+
+1. `anchor:prepare` is offline. It reads a receipt, runs the receipt verifier, and emits a deterministic unsigned instruction/transaction intent. It has no fee payer, blockhash, signature, signer-loading path, or RPC call.
+2. `anchor:submit` is the only write-capable command. It is devnet-only and fails unless all human gates below are explicit.
+3. `anchor:verify` is read-only. It checks the RPC genesis hash, fetches one supplied signature at confirmed commitment, and compares the canonical Memo with the exact receipt and ledger head. It writes and signs nothing.
+
+The source receipt is never edited by these commands. Network verification is a separate JSON sidecar/output. `receipt:verify` remains an offline verifier and correctly continues to report `solanaNetworkVerificationPerformed: false`.
+
+## 1. Verify and prepare offline
+
+First verify the receipt:
+
+```bash
+pnpm receipt:verify -- path/to/decision-receipt.json
+```
+
+Then create the unsigned intent. The default network is devnet; passing mainnet is refused.
+
+```bash
+pnpm anchor:prepare -- path/to/decision-receipt.json > path/to/anchor-intent.json
+```
+
+Preparation output explicitly says:
+
+```json
+{
+ "transaction": {
+ "feePayer": "supplied_only_at_submit",
+ "recentBlockhash": "fetched_only_at_submit",
+ "signatures": "none"
+ },
+ "preparation": {
+ "offline": true,
+ "signerAccessed": false,
+ "networkAccessed": false
+ }
+}
+```
+
+The same receipt produces the same Memo and intent hash. Inspect and record these intent fields before continuing:
+
+- `.network` must be `devnet`;
+- `.commitment.receipt.hash` must equal the receipt's `.integrity.receiptHash`;
+- `.commitment.ledgerHead.hash` and `.sequence` must equal the receipt ledger head;
+- `.integrity.intentHash` identifies the exact unsigned intent reviewed.
+
+## 2. HUMAN APPROVAL REQUIRED before submission
+
+> **STOP — DEBORAH MUST APPROVE THIS DEVNET WRITE.** Do not run `anchor:submit` as an unattended agent step. Deborah must inspect the receipt hash, ledger head, unsigned intent, signer path, and RPC network, then deliberately provide both the exact hash and `--approve-devnet-write`.
+
+Place the devnet keypair under the repository's ignored `.wallet/` directory, or provide another path that `git check-ignore` confirms is ignored. Never paste a secret key into a command, environment variable, document, log, receipt, or intent. The environment variable contains a **path only**:
+
+```bash
+export SAMARITAN_SOLANA_ANCHOR_KEYPAIR_PATH="$PWD/.wallet/devnet-anchor.json"
+```
+
+After human approval, the explicit write command is:
+
+```bash
+pnpm anchor:submit -- \
+ --receipt path/to/decision-receipt.json \
+ --intent path/to/anchor-intent.json \
+ --network devnet \
+ --confirm-receipt-hash \
+ --approve-devnet-write
+```
+
+An optional devnet RPC URL may be supplied with `--rpc-url` or `SAMARITAN_SOLANA_DEVNET_RPC_URL`. Before reading the keypair, the command:
+
+- refuses every network string except `devnet`;
+- re-verifies the receipt and unsigned intent;
+- compares the exact confirmation hash;
+- checks that the signer path is gitignored;
+- queries and verifies the devnet genesis hash.
+
+It then creates a one-instruction legacy transaction with `@solana/web3.js`, signs locally, sends with preflight, confirms it, and immediately runs the same read-only Memo verifier. Mainnet submission is unconditionally refused in code.
+
+## 3. Independently verify the anchor
+
+Anyone can verify the resulting signature without a wallet:
+
+```bash
+pnpm anchor:verify -- \
+ --receipt path/to/decision-receipt.json \
+ --signature \
+ --network devnet
+```
+
+The verifier requires all of the following:
+
+- RPC genesis hash is the canonical Solana devnet genesis hash;
+- the fetched transaction exists at `confirmed` commitment and succeeded;
+- the requested signature belongs to that transaction;
+- exactly one Memo instruction exists under the official Memo program;
+- the Memo is domain-separated, strict-schema-valid, and canonically encoded;
+- network, receipt schema, receipt hash, ledger sequence, ledger hash, and commitment type all match the supplied receipt.
+
+It only calls `getGenesisHash` and `getParsedTransaction`. It does not access a signer, send a transaction, request an airdrop, change the receipt, or persist a verification result.
+
+## Honest proof claims
+
+- Offline receipt verification proves disclosed schema/hash/lifecycle consistency, not network inclusion.
+- An unsigned anchor intent proves only what would be submitted.
+- A transaction signature by itself is not enough; it must pass `anchor:verify` against the exact public receipt.
+- A successful devnet verification externally timestamps the receipt hash and its committed local ledger head. It does not prove source-payload truth or trading profitability.
+- Judges require no wallet, tokens, or paid service. They only need the public receipt, signature, and read-only verifier.
+
+## Tests
+
+`test/solana-decision-anchor.test.ts` covers deterministic Memo canonicalization, non-canonical encoding, wrong RPC network, wrong receipt hash, wrong ledger head, missing signer, missing exact confirmation, mainnet refusal, and the rule that RPC genesis is checked before signer loading or sending.
diff --git a/docs/research/capture-readiness.md b/docs/research/capture-readiness.md
new file mode 100644
index 0000000..90bef68
--- /dev/null
+++ b/docs/research/capture-readiness.md
@@ -0,0 +1,10 @@
+# Paired Capture Readiness
+
+Generated 2026-07-12T10:40:36.521Z. Public Polymarket discovery plus the local TXLine fixture snapshot; no authenticated refresh was performed.
+
+| Match | Kickoff UTC | TXLine fixture | Status | Recommended capture start | Latest registered signal time |
+|---|---:|---:|---|---:|---:|
+| France vs Spain | 2026-07-14T19:00:00.000Z | 18237038 | ready_for_human_confirmation | 2026-07-14T16:00:00.000Z | 2026-07-14T18:45:00.000Z |
+| England vs Argentina | 2026-07-15T19:00:00.000Z | 18241006 | ready_for_human_confirmation | 2026-07-15T16:00:00.000Z | 2026-07-15T18:45:00.000Z |
+
+A capture remains blocked until TXLine fixture ID, teams, and kickoff match an exact Polymarket event family and Deborah confirms the mapping. Recommended start is three hours before kickoff so the frozen feature windows warm up before the 15-minute signal cutoff.
diff --git a/docs/research/final-weekend-capture-schedule.md b/docs/research/final-weekend-capture-schedule.md
new file mode 100644
index 0000000..87976e6
--- /dev/null
+++ b/docs/research/final-weekend-capture-schedule.md
@@ -0,0 +1,16 @@
+# Final-Weekend Paired Capture Schedule
+
+Prepared and authorized for **capture only** by Deborah on July 18, 2026. Every mapping remains `tradeable: false`; v2 registration does not bypass evidence or settlement review.
+
+| Match | TXLine fixture | Polymarket events | Absolute capture window | Current status |
+|---|---:|---|---|---|
+| France vs England | `18257865` | `708596`, `708643` | `2026-07-18T18:00:00Z`–`2026-07-19T00:00:00Z` | **Supervisor armed; scheduled** |
+| Spain vs Argentina | `18257739` | `708597`, `708641` | `2026-07-19T16:00:00Z`–`2026-07-19T22:00:00Z` | **Backup supervisor armed; scheduled** |
+
+The public Gamma identities are `fifwc-fra-eng-2026-07-18` and `fifwc-fra-eng-2026-07-18-more-markets`. Exact teams and the `2026-07-18T21:00:00Z` kickoff matched the refreshed official TXLine fixture snapshot and the public market metadata before scheduling. Full-time totals descriptions still specify the first 90 minutes plus stoppage time.
+
+The persistent launchd jobs `dev.samaritan.capture.france-england` and `dev.samaritan.capture.spain-argentina` started Node 22 supervisors at `2026-07-18T07:16:05Z` and `2026-07-18T07:23:19Z`. Their initial states are `scheduled`; each supervisor will preflight exact event identity and credential validity again at its absolute start before launching the paired recorder.
+
+Success requires completed Polymarket and TXLine terminal manifests, synchronized odds/scores/books coverage, usable exact-fixture odds, completed score state, public resolution, immutable input hashes, a causal total line, and later exact mapping/rules review. Nonempty files alone are insufficient. Any watchdog failure, identity change, feed stall, missing lifecycle stage, or unresolved mapping makes the capture inadmissible.
+
+No Polymarket authentication, wallet, token approval, deposit, real order, money movement, or Solana submission is authorized by this schedule.
diff --git a/docs/research/historical-gate-study-causal-economic-v3.md b/docs/research/historical-gate-study-causal-economic-v3.md
new file mode 100644
index 0000000..d054959
--- /dev/null
+++ b/docs/research/historical-gate-study-causal-economic-v3.md
@@ -0,0 +1,96 @@
+# Historical Detector Gate Study
+
+Generated: 2026-07-14T10:30:34.346Z
+Protocol: `historical-gate-causal-economic-v3-2026-07-14`
+Configuration SHA-256: `6c2b5cb517611628b2af7e6f93b18c9bcc7cb28315745d4cb9335cdacd32e968`
+
+> Research evidence only. Historical Polymarket points are sampled prices without bid/ask or depth. Results below are signal research, not executable-fill proof. The real-money gate remains closed.
+> Audit status: the causal selector and binary Total Goals economic-case normalization are repaired. The result still requires Deborah's human gate review and must not be described as executable alpha.
+> Source SHA-256: archive `eb6b09d339cca91e64a99730be352e4b1860db52af40b0d3d749372c1e09dc40`; mappings `39e2c8e3b4eccd463d5c4994db27054c0ac9285ce4480e813cc7c79c3b51e673`; causal selector evidence `228105f1f857b11ed0c7685652fd40483ae311f0f6191e5f574127fa2651064a`.
+
+## Design
+
+- Chronological train/test split: 68 / 30 fixtures
+- Training range: 2026-06-11T19:05:00.000Z to 2026-06-27T21:00:00.000Z
+- Held-out range: 2026-06-27T23:31:00.000Z to 2026-07-12T01:00:00.000Z
+- Split cutoff: 2026-06-27T21:00:00.000Z; identical kickoff timestamps never cross partitions
+- Replay window: final 3.0 hours through kickoff; in-running TXLine rows excluded
+- Forward label: 15 minutes with 25.0 bps minimum gap closure
+- Historical cost proxy: 100.0 probability bps per signal
+- Training selection: labeled snapshot F1, guarded by at least 30 normalized economic cases; raw emissions cannot satisfy the minimum-n gate
+- Uncertainty: 10,000 fixture-clustered bootstrap iterations, seed 20260714
+- Model operating cost: $0.00 (0 model calls; deterministic historical replay only)
+
+## Coverage
+
+- Training groups with both sources: 136/136; 164,542 events; 227,142 snapshots
+- Held-out groups with both sources: 52/60; 60,092 events; 77,445 snapshots
+- Causal diagnostics across 590 selector rows: 0 future probabilities; 0 future coverage rows; 0 late selector cutoffs; 0 nonzero untimestamped liquidity/volume rows
+- Held-out missing groups: `18209181:match_result:full_time:none`, `18209181:total_goals:full_time:2500`, `18218149:match_result:full_time:none`, `18218149:total_goals:full_time:2500`, `18213979:match_result:full_time:none`, `18213979:total_goals:full_time:2500`, `18222446:match_result:full_time:none`, `18222446:total_goals:full_time:2500`
+
+## Dynamic Total Candidate
+
+The candidate rule freezes each exact mapped full-time total 3.0 hours before TXLine kickoff—before the first detector snapshot—then selects the line closest to 50/50. Both probability and coverage are bounded by that same as-of timestamp. It requires at least 1,000 as-of history points and fails closed beyond 1500.0 bps from even. It selected 98 fixtures with 0 failures. Distribution: O/U 2.5: 75, O/U 4.5: 1, O/U 1.5: 5, O/U 3.5: 17.
+
+Status: `candidate_pending_human_gate_review`. Volume and liquidity are zeroed and forbidden as selector inputs because no timestamped historical as-of evidence exists for them.
+
+## Held-Out Results
+
+| Detector | Train-selected parameters | Train raw emissions | Train normalized cases | Train snapshot precision | Train snapshot recall | Test raw emissions | Test normalized cases | Test CLV n | Fixtures | Test snapshot precision | Test snapshot recall | Mean signal CLV bps | Mean after-cost bps | Fixture-clustered 95% CI bps | Match Result net bps | Total net bps | Necessary evidence status |
+|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|---:|---:|---|
+| XMARKET_DIVERGENCE | gap=100.0bps, persist=0s, stableZ=1 | 1762 | 934 | 65.9% | 2.2% | 372 | 331 | 331 | 25 | 50.0% | 1.9% | 92.4 | -7.6 | [-44.1, 27.8] | -15.2 | 45.0 | nonpositive_net_signal_clv |
+| CONSENSUS_MOVE | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 702 | 301 | 62.8% | 0.7% | 107 | 72 | 72 | 23 | 50.5% | 0.6% | 138.4 | 38.4 | [-67.1, 120.8] | -67.0 | 132.7 | clustered_interval_not_positive |
+| FADER_CANDIDATE | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 412 | 268 | 6.1% | 0.1% | 72 | 56 | 56 | 16 | 5.6% | 0.1% | 54.5 | -45.5 | [-109.1, 35.9] | -50.3 | -32.2 | nonpositive_net_signal_clv |
+
+`human_review_required` means only that the predeclared sample, after-cost point estimate, and fixture-clustered interval checks passed. `clustered_interval_not_positive` means the point estimate was not strong enough to exclude zero after fixture clustering. Neither status is permission to trade or a substitute for Deborah's gate decision, settlement verification, risk caps, or executable-book evidence.
+
+For binary totals, `buy Over + sell Under` is one Over exposure and `buy Under + sell Over` is one Under exposure. An actual BUY is retained; duplicates and complementary sell expressions collapse into it. Sell-only totals are excluded because detector inputs do not prove an executable complementary-token ask. Three-way Match Result signals are not economically normalized.
+
+### Held-Out Normalization Audit
+
+| Detector | Raw | Normalized | Executable total buys | Match Result pass-through | Complementary sells collapsed | Duplicate buys collapsed | Sell-only totals dropped | Invalid total outcomes dropped |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| XMARKET_DIVERGENCE | 372 | 331 | 42 | 289 | 35 | 0 | 6 | 0 |
+| CONSENSUS_MOVE | 107 | 72 | 38 | 34 | 29 | 0 | 6 | 0 |
+| FADER_CANDIDATE | 72 | 56 | 15 | 41 | 9 | 0 | 7 | 0 |
+
+## Cost Sensitivity
+
+| Detector | 50 bps proxy | 100 bps proxy | 150 bps proxy | 200 bps proxy |
+|---|---:|---:|---:|---:|
+| XMARKET_DIVERGENCE | 42.4 | -7.6 | -57.6 | -107.6 |
+| CONSENSUS_MOVE | 88.4 | 38.4 | -11.6 | -61.6 |
+| FADER_CANDIDATE | 4.5 | -45.5 | -95.5 | -145.5 |
+
+Values are mean directional signal CLV after subtracting each proxy. This is a sensitivity check, not a claim about actual historical execution costs.
+
+## Training Leaderboard
+
+| Detector | Rank | Parameters | Raw emissions | Normalized cases | Snapshot precision | Snapshot recall | F1 | Minimum n met | Cost floor met |
+|---|---:|---|---:|---:|---:|---:|---:|---|---|
+| XMARKET_DIVERGENCE | 1 | gap=100.0bps, persist=0s, stableZ=1 | 1762 | 934 | 65.9% | 2.2% | 0.042 | yes | yes |
+| XMARKET_DIVERGENCE | 2 | gap=100.0bps, persist=0s, stableZ=0.5 | 1522 | 674 | 65.9% | 1.5% | 0.030 | yes | yes |
+| XMARKET_DIVERGENCE | 3 | gap=100.0bps, persist=60s, stableZ=1 | 434 | 263 | 60.2% | 0.5% | 0.010 | yes | yes |
+| XMARKET_DIVERGENCE | 4 | gap=150.0bps, persist=0s, stableZ=1 | 355 | 132 | 85.5% | 0.5% | 0.009 | yes | yes |
+| XMARKET_DIVERGENCE | 5 | gap=150.0bps, persist=0s, stableZ=0.5 | 285 | 116 | 84.0% | 0.4% | 0.007 | yes | yes |
+| CONSENSUS_MOVE | 1 | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 702 | 301 | 62.8% | 0.7% | 0.013 | yes | yes |
+| CONSENSUS_MOVE | 2 | z=1, cusum=10.0bps, gap=100.0bps, updates=3 | 702 | 301 | 62.8% | 0.7% | 0.013 | yes | yes |
+| CONSENSUS_MOVE | 3 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=3 | 596 | 259 | 63.2% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 4 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=5 | 596 | 259 | 63.2% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 5 | z=2, cusum=10.0bps, gap=100.0bps, updates=5 | 478 | 228 | 65.5% | 0.5% | 0.011 | yes | yes |
+| FADER_CANDIDATE | 1 | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 412 | 268 | 6.1% | 0.1% | 0.003 | yes | yes |
+| FADER_CANDIDATE | 2 | pmZ=1, gap=100.0bps, persist=0s, stableZ=0.5 | 319 | 163 | 8.9% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 3 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=1 | 359 | 238 | 5.4% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 4 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=0.5 | 281 | 142 | 6.6% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 5 | pmZ=1.5, gap=150.0bps, persist=0s, stableZ=0.5 | 65 | 30 | 18.6% | 0.1% | 0.001 | yes | yes |
+
+## Guardrails
+
+- No held-out result influenced configuration selection.
+- Total-line selection uses only prices and coverage observable no later than the first detector snapshot.
+- Training and held-out minimum-sample checks use normalized economic cases, never raw detector emissions.
+- Sell-only Total Goals cases fail closed; sampled history cannot prove a complementary token was buyable at that moment.
+- Sampled Polymarket history cannot establish executable spread, slippage, or fill probability; the cost proxy is deliberately conservative but remains a proxy.
+- Candidate mappings remain non-tradeable and no settlement review is inferred from research alignment.
+- Thresholds in this report are exploratory until the human gate review explicitly freezes or rejects them.
+- STALE_QUOTE remains disabled based on the synchronized live-lane result; this historical study does not evaluate it.
diff --git a/docs/research/historical-gate-study-causal-economic-v4.md b/docs/research/historical-gate-study-causal-economic-v4.md
new file mode 100644
index 0000000..8c7d8eb
--- /dev/null
+++ b/docs/research/historical-gate-study-causal-economic-v4.md
@@ -0,0 +1,104 @@
+# Historical Detector Gate Study
+
+Generated: 2026-07-14T10:34:05.545Z
+Protocol: `historical-gate-causal-economic-v4-2026-07-14`
+Configuration SHA-256: `9a4eeff928f697fc55ab5147a4dc07f611c40bb749501fc3bd92b211f24b2e54`
+
+> Research evidence only. Historical Polymarket points are sampled prices without bid/ask or depth. Results below are signal research, not executable-fill proof. The real-money gate remains closed.
+> Audit status: the causal selector and binary Total Goals economic-case normalization are repaired. The result still requires Deborah's human gate review and must not be described as executable alpha.
+> Source SHA-256: archive `eb6b09d339cca91e64a99730be352e4b1860db52af40b0d3d749372c1e09dc40`; mappings `39e2c8e3b4eccd463d5c4994db27054c0ac9285ce4480e813cc7c79c3b51e673`; causal selector evidence `228105f1f857b11ed0c7685652fd40483ae311f0f6191e5f574127fa2651064a`.
+
+## Design
+
+- Chronological train/test split: 68 / 30 fixtures
+- Training range: 2026-06-11T19:05:00.000Z to 2026-06-27T21:00:00.000Z
+- Held-out range: 2026-06-27T23:31:00.000Z to 2026-07-12T01:00:00.000Z
+- Split cutoff: 2026-06-27T21:00:00.000Z; identical kickoff timestamps never cross partitions
+- Replay window: final 3.0 hours through kickoff; in-running TXLine rows excluded
+- Forward label: 15 minutes with 25.0 bps minimum gap closure
+- Historical cost proxy: 100.0 probability bps per signal
+- Training selection: labeled snapshot F1, guarded by at least 30 normalized economic cases; raw emissions cannot satisfy the minimum-n gate
+- Uncertainty: 10,000 fixture-clustered bootstrap iterations, seed 20260714
+- Model operating cost: $0.00 (0 model calls; deterministic historical replay only)
+
+## Coverage
+
+- Training groups with both sources: 136/136; 164,542 events; 227,142 snapshots
+- Held-out groups with both sources: 52/60; 60,092 events; 77,445 snapshots
+- Causal diagnostics across 590 selector rows: 0 future probabilities; 0 future coverage rows; 0 late selector cutoffs; 0 nonzero untimestamped liquidity/volume rows
+- Held-out missing groups: `18209181:match_result:full_time:none`, `18209181:total_goals:full_time:2500`, `18218149:match_result:full_time:none`, `18218149:total_goals:full_time:2500`, `18213979:match_result:full_time:none`, `18213979:total_goals:full_time:2500`, `18222446:match_result:full_time:none`, `18222446:total_goals:full_time:2500`
+
+## Dynamic Total Candidate
+
+The candidate rule freezes each exact mapped full-time total 3.0 hours before TXLine kickoff—before the first detector snapshot—then selects the line closest to 50/50. Both probability and coverage are bounded by that same as-of timestamp. It requires at least 1,000 as-of history points and fails closed beyond 1500.0 bps from even. It selected 98 fixtures with 0 failures. Distribution: O/U 2.5: 75, O/U 4.5: 1, O/U 1.5: 5, O/U 3.5: 17.
+
+Status: `candidate_pending_human_gate_review`. Volume and liquidity are zeroed and forbidden as selector inputs because no timestamped historical as-of evidence exists for them.
+
+## Forward Paper Candidate Assessment
+
+The prespecified paper family is **Total Goals only**. Its frozen `CONSENSUS_MOVE` configuration was chosen from training data before held-out Total Goals results were inspected. A family-specific training audit found 135 normalized Total Goals cases, so the training minimum was not borrowed from Match Result.
+
+Held-out Total Goals: 38 normalized buy cases across 18 fixtures; mean after the 100.0 bps cost proxy 132.7 bps; fixture-clustered 95% CI [14.3, 243.9] bps.
+
+Status at generation: `historical_signal_candidate_for_forward_paper_review`. This is **historical signal evidence**, not alpha, profitability, or executable-fill evidence. Inputs are sampled Polymarket prices without bid/ask depth. Deborah subsequently registered the exact v2 candidate for forward paper observation only on July 18; this historical artifact remains unchanged evidence and does not itself count as a v2 observation.
+
+## Held-Out Results
+
+| Detector | Train-selected parameters | Train raw emissions | Train normalized cases | Train snapshot precision | Train snapshot recall | Test raw emissions | Test normalized cases | Test CLV n | Fixtures | Test snapshot precision | Test snapshot recall | Mean signal CLV bps | Mean after-cost bps | Fixture-clustered 95% CI bps | Match Result net bps | Total net bps | Necessary evidence status |
+|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|---:|---:|---|
+| XMARKET_DIVERGENCE | gap=100.0bps, persist=0s, stableZ=1 | 1762 | 934 | 65.9% | 2.2% | 372 | 331 | 331 | 25 | 50.0% | 1.9% | 92.4 | -7.6 | [-44.1, 27.8] | -15.2 | 45.0 | nonpositive_net_signal_clv |
+| CONSENSUS_MOVE | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 702 | 301 | 62.8% | 0.7% | 107 | 72 | 72 | 23 | 50.5% | 0.6% | 138.4 | 38.4 | [-67.1, 120.8] | -67.0 | 132.7 | clustered_interval_not_positive |
+| FADER_CANDIDATE | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 412 | 268 | 6.1% | 0.1% | 72 | 56 | 56 | 16 | 5.6% | 0.1% | 54.5 | -45.5 | [-109.1, 35.9] | -50.3 | -32.2 | nonpositive_net_signal_clv |
+
+`human_review_required` means only that the predeclared sample, after-cost point estimate, and fixture-clustered interval checks passed. `clustered_interval_not_positive` means the point estimate was not strong enough to exclude zero after fixture clustering. Neither status is permission to trade or a substitute for Deborah's gate decision, settlement verification, risk caps, or executable-book evidence.
+
+For binary totals, `buy Over + sell Under` is one Over exposure and `buy Under + sell Over` is one Under exposure. An actual BUY is retained; duplicates and complementary sell expressions collapse into it. Sell-only totals are excluded because detector inputs do not prove an executable complementary-token ask. Three-way Match Result signals are not economically normalized.
+
+### Held-Out Normalization Audit
+
+| Detector | Raw | Normalized | Executable total buys | Match Result pass-through | Complementary sells collapsed | Duplicate buys collapsed | Sell-only totals dropped | Invalid total outcomes dropped |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| XMARKET_DIVERGENCE | 372 | 331 | 42 | 289 | 35 | 0 | 6 | 0 |
+| CONSENSUS_MOVE | 107 | 72 | 38 | 34 | 29 | 0 | 6 | 0 |
+| FADER_CANDIDATE | 72 | 56 | 15 | 41 | 9 | 0 | 7 | 0 |
+
+## Cost Sensitivity
+
+| Detector | 50 bps proxy | 100 bps proxy | 150 bps proxy | 200 bps proxy |
+|---|---:|---:|---:|---:|
+| XMARKET_DIVERGENCE | 42.4 | -7.6 | -57.6 | -107.6 |
+| CONSENSUS_MOVE | 88.4 | 38.4 | -11.6 | -61.6 |
+| FADER_CANDIDATE | 4.5 | -45.5 | -95.5 | -145.5 |
+
+Values are mean directional signal CLV after subtracting each proxy. This is a sensitivity check, not a claim about actual historical execution costs.
+
+## Training Leaderboard
+
+| Detector | Rank | Parameters | Raw emissions | Normalized cases | Snapshot precision | Snapshot recall | F1 | Minimum n met | Cost floor met |
+|---|---:|---|---:|---:|---:|---:|---:|---|---|
+| XMARKET_DIVERGENCE | 1 | gap=100.0bps, persist=0s, stableZ=1 | 1762 | 934 | 65.9% | 2.2% | 0.042 | yes | yes |
+| XMARKET_DIVERGENCE | 2 | gap=100.0bps, persist=0s, stableZ=0.5 | 1522 | 674 | 65.9% | 1.5% | 0.030 | yes | yes |
+| XMARKET_DIVERGENCE | 3 | gap=100.0bps, persist=60s, stableZ=1 | 434 | 263 | 60.2% | 0.5% | 0.010 | yes | yes |
+| XMARKET_DIVERGENCE | 4 | gap=150.0bps, persist=0s, stableZ=1 | 355 | 132 | 85.5% | 0.5% | 0.009 | yes | yes |
+| XMARKET_DIVERGENCE | 5 | gap=150.0bps, persist=0s, stableZ=0.5 | 285 | 116 | 84.0% | 0.4% | 0.007 | yes | yes |
+| CONSENSUS_MOVE | 1 | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 702 | 301 | 62.8% | 0.7% | 0.013 | yes | yes |
+| CONSENSUS_MOVE | 2 | z=1, cusum=10.0bps, gap=100.0bps, updates=3 | 702 | 301 | 62.8% | 0.7% | 0.013 | yes | yes |
+| CONSENSUS_MOVE | 3 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=3 | 596 | 259 | 63.2% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 4 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=5 | 596 | 259 | 63.2% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 5 | z=2, cusum=10.0bps, gap=100.0bps, updates=5 | 478 | 228 | 65.5% | 0.5% | 0.011 | yes | yes |
+| FADER_CANDIDATE | 1 | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 412 | 268 | 6.1% | 0.1% | 0.003 | yes | yes |
+| FADER_CANDIDATE | 2 | pmZ=1, gap=100.0bps, persist=0s, stableZ=0.5 | 319 | 163 | 8.9% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 3 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=1 | 359 | 238 | 5.4% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 4 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=0.5 | 281 | 142 | 6.6% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 5 | pmZ=1.5, gap=150.0bps, persist=0s, stableZ=0.5 | 65 | 30 | 18.6% | 0.1% | 0.001 | yes | yes |
+
+## Guardrails
+
+- No held-out result influenced configuration selection.
+- Total-line selection uses only prices and coverage observable no later than the first detector snapshot.
+- Training and held-out minimum-sample checks use normalized economic cases, never raw detector emissions.
+- Sell-only Total Goals cases fail closed; sampled history cannot prove a complementary token was buyable at that moment.
+- Sampled Polymarket history cannot establish executable spread, slippage, or fill probability; the cost proxy is deliberately conservative but remains a proxy.
+- Candidate mappings remain non-tradeable and no settlement review is inferred from research alignment.
+- Thresholds in this report are exploratory until the human gate review explicitly freezes or rejects them.
+- STALE_QUOTE remains disabled based on the synchronized live-lane result; this historical study does not evaluate it.
diff --git a/docs/research/historical-gate-study-v1-invalidation.md b/docs/research/historical-gate-study-v1-invalidation.md
new file mode 100644
index 0000000..53cf600
--- /dev/null
+++ b/docs/research/historical-gate-study-v1-invalidation.md
@@ -0,0 +1,63 @@
+# Historical Gate Study V1 — Invalidation Record
+
+**Declared:** July 14, 2026
+**Affected protocol:** historical dynamic-total selector and paper-study v1 upstream evidence
+**Decision:** Invalidated for strategy promotion, profitability claims, and paper admission
+**Real-money gate:** **CLOSED**
+
+## Summary
+
+An independent causal audit found that the v1 historical study chose each fixture's “main” full-time total using Polymarket information that was not available during the eligible signal window.
+
+The selector used the latest price up to kickoff minus five minutes. Historical signals were eligible only through kickoff minus fifteen minutes. In the frozen v1 artifact:
+
+- 95 of 98 selected lines used a selector observation later than the last eligible signal;
+- 81 of 98 selected lines used an observation within approximately six minutes of kickoff;
+- `coverage_points` counted history without applying the selector cutoff;
+- the totals CLV sample contained 38 buys and 35 sells, while the current paper executor cannot open sell positions without inventory;
+- 58 emissions were 29 same-timestamp complementary Over/Under expressions of the same economic move.
+
+The defect does not prove that `CONSENSUS_MOVE` has no edge. It means the reported positive v1 result cannot establish one.
+
+## Why this is invalid
+
+A live system cannot ask a future market observation which line it should have monitored earlier. Choosing the line at T−5 minutes and evaluating decisions that ended at T−15 minutes leaks future market state into the test universe.
+
+Chronological train/test separation does not cure this defect because the look-ahead occurs independently inside every fixture in both partitions.
+
+The unbounded coverage count creates a second admission leak: observations after the selector time can make a line appear sufficiently covered when it was not sufficiently covered at decision time.
+
+Finally, raw detector emissions are not necessarily executable or independent economic decisions. Unsupported sells and complementary token expressions must not inflate the primary sample.
+
+## Affected claims
+
+The following v1 claims are withdrawn:
+
+- that the dynamic-total result is valid held-out edge evidence;
+- that its positive probability-bps estimate supports paper promotion;
+- that 73 detector emissions represent 73 executable opportunities;
+- that paper-study v1 may admit new cases under the old selector.
+
+The original machine-readable and Markdown artifacts remain preserved for audit traceability. Their numbers must be labelled invalidated wherever displayed.
+
+## Unaffected evidence
+
+The synchronized Spain-Belgium stale-quote feasibility study does not use the historical dynamic-total selector. Its narrow conclusion remains: that one captured match produced no clean post-TXLine stale window in the measured cases. It does not establish a universal absence of stale quotes.
+
+The system's architectural boundaries, normalization tests, replay/live canonical contracts, strict Claude schemas, and real-money gate are also unaffected by this methodological correction.
+
+## Required v2 repair
+
+Before any detector can be promoted again:
+
+1. Freeze one deterministic selector time no later than detector evaluation start.
+2. Apply that same as-of cutoff to probability, coverage, volume, liquidity, and market availability.
+3. Fail closed when no line was eligible at that time.
+4. Prove that changing all post-cutoff data cannot change selection.
+5. Define unique economic cases rather than counting raw complementary emissions.
+6. Convert sells into verified complementary-token buys or exclude them from executable results.
+7. Rerun training and held-out evaluation under a new protocol/config hash.
+8. Report model cost separately from probability CLV.
+9. Create and sign a new paper preregistration only if the corrected result merits it.
+
+No repair may rewrite or retroactively validate paper-study v1.
diff --git a/docs/research/historical-gate-study-v3-to-v4-methodology-note.md b/docs/research/historical-gate-study-v3-to-v4-methodology-note.md
new file mode 100644
index 0000000..1dc6fe0
--- /dev/null
+++ b/docs/research/historical-gate-study-v3-to-v4-methodology-note.md
@@ -0,0 +1,17 @@
+# Historical Gate V3 → V4 Methodology Note
+
+**Dated:** July 14, 2026
+**Real-money gate:** closed
+
+The causal/economic v3 rerun repaired the future-informed total-line selector, normalized binary Total Goals emissions into executor-expressible buy cases, and added fixture-clustered uncertainty. It is preserved at `historical-gate-study-causal-economic-v3.md`.
+
+Review of that generated report exposed one remaining reporting gap: Samaritan's proposed forward paper family is **Total Goals only**, while v3 showed the training minimum for the selected detector configuration only in aggregate across Total Goals and Match Result. The held-out report already separated both market families, but it did not prove that the prespecified Total Goals family independently met the training sample floor.
+
+V4 therefore adds one audit field without changing any detector threshold, selector rule, train/test boundary, cost proxy, label, or held-out row:
+
+- count normalized Total Goals cases for the already training-selected configuration using training fixtures only;
+- assess the prespecified held-out Total Goals slice against the same minimum sample, positive after-cost point estimate, and fixture-clustered 95% interval rule;
+- keep the aggregate detector result visible;
+- label any passing Total Goals result only as a historical signal candidate for a fresh forward paper review.
+
+No v3 artifact was overwritten. V4 uses a new protocol ID, configuration hash, JSON path, and Markdown path. This refinement did not itself activate the paper study. Deborah later registered corrected v2 for forward paper observation only on July 18; v4 remains historical source evidence and is not retroactively relabelled as a qualifying observation.
diff --git a/docs/research/historical-gate-study.md b/docs/research/historical-gate-study.md
new file mode 100644
index 0000000..95bbf81
--- /dev/null
+++ b/docs/research/historical-gate-study.md
@@ -0,0 +1,76 @@
+# Historical Detector Gate Study — Invalidated V1 Audit Record
+
+Generated: 2026-07-11T06:57:14.119Z
+
+> **Invalidated July 14, 2026:** this generated v1 artifact selected the dynamic total with information later than the eligible signal window for 95 of 98 fixtures, counted coverage without the same as-of cutoff, and mixed unsupported sells with complementary expressions of the same economic move. Preserve these values for traceability only. They are not valid evidence of held-out edge, alpha, or executable opportunities. The real-money gate remains closed. See `historical-gate-study-v1-invalidation.md`.
+
+## Design
+
+- Chronological train/test split: 68 / 30 fixtures
+- Training range: 2026-06-11T19:05:00.000Z to 2026-06-27T21:00:00.000Z
+- Held-out range: 2026-06-27T23:31:00.000Z to 2026-07-12T01:00:00.000Z
+- Split cutoff: 2026-06-27T21:00:00.000Z; identical kickoff timestamps never cross partitions
+- Replay window: final 3.0 hours through kickoff; in-running TXLine rows excluded
+- Forward label: 15 minutes with 25.0 bps minimum gap closure
+- Historical cost proxy: 100.0 probability bps per signal
+- Training selection: labeled F1, guarded by at least 30 predicted-positive cases
+
+## Coverage
+
+- Training groups with both sources: 136/136; 164,655 events; 227,368 snapshots
+- Held-out groups with both sources: 52/60; 60,092 events; 77,445 snapshots
+- Held-out missing groups: `18209181:match_result:full_time:none`, `18209181:total_goals:full_time:2500`, `18218149:match_result:full_time:none`, `18218149:total_goals:full_time:2500`, `18213979:match_result:full_time:none`, `18213979:total_goals:full_time:2500`, `18222446:match_result:full_time:none`, `18222446:total_goals:full_time:2500`
+
+## Dynamic Total Candidate
+
+The candidate rule selects the exact mapped full-time total closest to 50/50 at least five minutes before kickoff, requires at least 1,000 history points, and fails closed beyond 1500.0 bps from even. It selected 98 fixtures with 0 failures. Distribution: O/U 2.5: 77, O/U 4.5: 1, O/U 1.5: 5, O/U 3.5: 15.
+
+Status: `candidate_pending_human_gate_review`. Volume and liquidity are not weighted because captured Gamma liquidity is post-close and zero for most lines.
+
+## Held-Out Results
+
+| Detector | Train-selected parameters | Train signals | Train precision | Train recall | Test CLV n | Fixtures | Test precision | Test recall | Mean signal CLV bps | Mean after-cost bps | Match Result net bps | Total net bps | Necessary evidence status |
+|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
+| XMARKET_DIVERGENCE | gap=100.0bps, persist=0s, stableZ=1 | 1226 | 65.4% | 2.2% | 372 | 25 | 50.0% | 1.9% | 97.9 | -2.1 | -15.2 | 43.6 | nonpositive_net_signal_clv |
+| CONSENSUS_MOVE | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 450 | 62.2% | 0.7% | 107 | 23 | 50.5% | 0.6% | 159.9 | 59.9 | -67.0 | 119.0 | human_review_required |
+| FADER_CANDIDATE | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 314 | 6.1% | 0.1% | 72 | 16 | 5.6% | 0.1% | 54.0 | -46.0 | -50.3 | -40.2 | nonpositive_net_signal_clv |
+
+`human_review_required` means only that the predeclared necessary sample/after-cost checks passed. It is not permission to trade and does not substitute for Deborah's gate decision, settlement verification, risk caps, or executable-book evidence.
+
+## Cost Sensitivity
+
+| Detector | 50 bps proxy | 100 bps proxy | 150 bps proxy | 200 bps proxy |
+|---|---:|---:|---:|---:|
+| XMARKET_DIVERGENCE | 47.9 | -2.1 | -52.1 | -102.1 |
+| CONSENSUS_MOVE | 109.9 | 59.9 | 9.9 | -40.1 |
+| FADER_CANDIDATE | 4.0 | -46.0 | -96.0 | -146.0 |
+
+Values are mean directional signal CLV after subtracting each proxy. This is a sensitivity check, not a claim about actual historical execution costs.
+
+## Training Leaderboard
+
+| Detector | Rank | Parameters | Signals | Precision | Recall | F1 | Minimum n met | Cost floor met |
+|---|---:|---|---:|---:|---:|---:|---|---|
+| XMARKET_DIVERGENCE | 1 | gap=100.0bps, persist=0s, stableZ=1 | 1226 | 65.4% | 2.2% | 0.042 | yes | yes |
+| XMARKET_DIVERGENCE | 2 | gap=100.0bps, persist=0s, stableZ=0.5 | 858 | 64.5% | 1.5% | 0.029 | yes | yes |
+| XMARKET_DIVERGENCE | 3 | gap=100.0bps, persist=60s, stableZ=1 | 322 | 59.0% | 0.5% | 0.010 | yes | yes |
+| XMARKET_DIVERGENCE | 4 | gap=150.0bps, persist=0s, stableZ=1 | 195 | 85.1% | 0.4% | 0.009 | yes | yes |
+| XMARKET_DIVERGENCE | 5 | gap=150.0bps, persist=0s, stableZ=0.5 | 158 | 83.5% | 0.4% | 0.007 | yes | yes |
+| CONSENSUS_MOVE | 1 | z=1, cusum=10.0bps, gap=100.0bps, updates=5 | 450 | 62.2% | 0.7% | 0.014 | yes | yes |
+| CONSENSUS_MOVE | 2 | z=1, cusum=10.0bps, gap=100.0bps, updates=3 | 450 | 62.2% | 0.7% | 0.014 | yes | yes |
+| CONSENSUS_MOVE | 3 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=3 | 398 | 62.3% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 4 | z=1.5, cusum=10.0bps, gap=100.0bps, updates=5 | 398 | 62.3% | 0.6% | 0.012 | yes | yes |
+| CONSENSUS_MOVE | 5 | z=2, cusum=10.0bps, gap=100.0bps, updates=5 | 348 | 64.4% | 0.5% | 0.011 | yes | yes |
+| FADER_CANDIDATE | 1 | pmZ=1, gap=100.0bps, persist=0s, stableZ=1 | 314 | 6.1% | 0.1% | 0.003 | yes | yes |
+| FADER_CANDIDATE | 2 | pmZ=1, gap=100.0bps, persist=0s, stableZ=0.5 | 196 | 8.7% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 3 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=1 | 276 | 5.4% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 4 | pmZ=1.5, gap=100.0bps, persist=0s, stableZ=0.5 | 168 | 6.5% | 0.1% | 0.002 | yes | yes |
+| FADER_CANDIDATE | 5 | pmZ=2, gap=150.0bps, persist=0s, stableZ=0.5 | 36 | 22.2% | 0.1% | 0.001 | yes | yes |
+
+## Guardrails
+
+- No held-out result influenced configuration selection.
+- Sampled Polymarket history cannot establish executable spread, slippage, or fill probability; the cost proxy is deliberately conservative but remains a proxy.
+- Candidate mappings remain non-tradeable and no settlement review is inferred from research alignment.
+- Thresholds in this report are exploratory until the human gate review explicitly freezes or rejects them.
+- STALE_QUOTE remains disabled based on the synchronized live-lane result; this historical study does not evaluate it.
diff --git a/docs/research/paired-spain-belgium-2026-07-10-live-lane.md b/docs/research/paired-spain-belgium-2026-07-10-live-lane.md
new file mode 100644
index 0000000..4a5c3fa
--- /dev/null
+++ b/docs/research/paired-spain-belgium-2026-07-10-live-lane.md
@@ -0,0 +1,91 @@
+# Spain-Belgium Paired Live-Lane Evidence
+
+Generated: 2026-07-14T23:13:44.692Z
+
+> Research evidence only. The mapping is capture-confirmed but not settlement-verified, every in-play result remains paper-only, and the material-move threshold is exploratory rather than an approved detector threshold.
+
+## Scope
+
+- Fixture: `18218149`
+- Canonical target assets: 13 across 6 exact market groups
+- Material midpoint move: 50 probability bps
+- Polymarket messages scanned through the last response window: 1,986,443
+- Canonical Polymarket events normalized: 2,262,950
+
+## Feed Health
+
+- TXLine odds receive latency p50/p90/p99: 142 / 721 / 8875 ms
+- TXLine score receive latency p50/p90/p99: 39 / 362 / 50772 ms
+- Polymarket non-book venue-timestamp age outside reconnect recovery p50/p90/p99: 119 / 9399 / 158965 ms
+- Polymarket all-event venue-timestamp age p50/p90/p99: 121 / 9066 / 158965 ms
+- Polymarket payload timestamps show event age, not a pure network-latency clock; stale/repeated venue timestamps remain visible outside reconnect windows and must be treated as feed-health evidence rather than transport timing alone.
+- Public WebSocket outages: 8; total 91171 ms; max 25375 ms
+
+| Outage start | Reconnected | Duration ms | Code |
+|---|---|---:|---:|
+| 2026-07-10T19:42:56.902Z | 2026-07-10T19:42:58.713Z | 1811 | 1006 |
+| 2026-07-10T20:27:07.908Z | 2026-07-10T20:27:10.460Z | 2552 | 1006 |
+| 2026-07-10T20:32:22.564Z | 2026-07-10T20:32:27.317Z | 4753 | 1006 |
+| 2026-07-10T20:36:04.809Z | 2026-07-10T20:36:14.354Z | 9545 | 1006 |
+| 2026-07-10T20:41:09.121Z | 2026-07-10T20:41:24.612Z | 15491 | 1006 |
+| 2026-07-10T20:50:17.844Z | 2026-07-10T20:50:43.219Z | 25375 | 1006 |
+| 2026-07-10T20:52:35.358Z | 2026-07-10T20:52:51.491Z | 16133 | 1006 |
+| 2026-07-10T23:32:55.720Z | 2026-07-10T23:33:11.231Z | 15511 | 1006 |
+
+| Polymarket observation | Events | Age p50 ms | Age p90 ms | Age p99 ms |
+|---|---:|---:|---:|---:|
+| best_bid_ask | 56,942 | 102 | 11852 | 158792 |
+| book | 37,723 | 123 | 13024 | 158812 |
+| last_trade | 25,793 | 122 | 14657 | 158848 |
+| price_change | 2,142,492 | 121 | 8857 | 158965 |
+
+## Goal Delivery
+
+| Goal | First seen | Participant | Match clock sec | TXLine receive latency ms | Confirmation delay ms |
+|---:|---|---:|---:|---:|---:|
+| 1 | 2026-07-10T19:30:00.389Z | 1 | 1761 | 11 | 74416 |
+| 2 | 2026-07-10T19:40:50.195Z | 2 | 2411 | -8 | 162426 |
+| 3 | 2026-07-10T20:49:37.081Z | 1 | 5244 | 49 | 92191 |
+
+## Repricing Evidence
+
+A pre-trigger move compares the book at T-5s with the last state before TXLine delivered the goal. Material movement before T0 means the venue had already reacted before Samaritan received the score event; that is evidence against a post-TXLine stale-quote window for that market instance.
+
+| Goal | Clock sec | Market | First quote update ms | First material move ms | Pre-trigger move pp | Classification | Outage overlap |
+|---:|---:|---|---:|---:|---:|---|---|
+| 1 | 1761 | match_result | 24 | 228 | 7.750 | polymarket_moved_before_txline | no |
+| 1 | 1761 | total_goals 0.5 | 24 | 243 | 1.625 | polymarket_moved_before_txline | no |
+| 1 | 1761 | total_goals 1.5 | 24 | 546 | 5.375 | polymarket_moved_before_txline | no |
+| 1 | 1761 | total_goals 2.5 | 24 | 243 | 8.375 | polymarket_moved_before_txline | no |
+| 1 | 1761 | total_goals 3.5 | 24 | 3257 | 6.000 | polymarket_moved_before_txline | no |
+| 1 | 1761 | total_goals 4.5 | 24 | 1671 | 8.000 | polymarket_moved_before_txline | no |
+| 2 | 2411 | match_result | 0 | 1284 | 5.750 | polymarket_moved_before_txline | no |
+| 2 | 2411 | total_goals 0.5 | 1900 | n/a | 0.000 | no_material_reprice_in_window | no |
+| 2 | 2411 | total_goals 1.5 | 0 | 3042 | 5.375 | polymarket_moved_before_txline | no |
+| 2 | 2411 | total_goals 2.5 | 0 | 1781 | 5.750 | polymarket_moved_before_txline | no |
+| 2 | 2411 | total_goals 3.5 | 0 | 162 | 6.375 | polymarket_moved_before_txline | no |
+| 2 | 2411 | total_goals 4.5 | 31 | 1611 | 8.250 | polymarket_moved_before_txline | no |
+| 3 | 5244 | match_result | 1 | 1368 | 0.500 | polymarket_moved_before_txline | no |
+| 3 | 5244 | total_goals 0.5 | n/a | n/a | 0.000 | no_material_reprice_in_window | no |
+| 3 | 5244 | total_goals 1.5 | 5656 | n/a | 0.000 | no_material_reprice_in_window | no |
+| 3 | 5244 | total_goals 2.5 | 1 | n/a | 0.000 | no_material_reprice_in_window | no |
+| 3 | 5244 | total_goals 3.5 | 1 | n/a | 0.125 | no_material_reprice_in_window | no |
+| 3 | 5244 | total_goals 4.5 | 6781 | n/a | 0.000 | no_material_reprice_in_window | no |
+
+## Gate Readout
+
+- Market-event cases measured: 18
+- Polymarket moved at least the exploratory threshold before TXLine first delivery: 12
+- Material post-TXLine repricing observed without a prior move: 0
+- No material move inside the 30-second response window: 6
+- Result for STALE_QUOTE: `not_supported_by_this_match`
+
+For this match, Polymarket was already repricing before the first TXLine goal event arrived whenever a material move was visible. The capture therefore does not support a post-TXLine stale-order edge. STALE_QUOTE remains disabled and paper-only; more synchronized matches may add evidence but cannot reverse this result by assumption.
+
+## Interpretation Guardrails
+
+- These are three event instances from one match, not a fitted latency distribution.
+- A code-1006 reconnect interval is unavailable data, never evidence that a quote stayed unchanged.
+- Match Result is grouped from the three canonical Yes tokens; complement No tokens are not double-counted.
+- Every mapped totals line is reported. No O/U line is hard-coded as the production main total.
+- No trading, fill simulation, or real-money claim is authorized by this report.
diff --git a/docs/research/paper-fixture-universe.md b/docs/research/paper-fixture-universe.md
new file mode 100644
index 0000000..a269e4e
--- /dev/null
+++ b/docs/research/paper-fixture-universe.md
@@ -0,0 +1,13 @@
+# Paper Fixture Evidence Universe
+
+Generated 2026-07-12T09:50:09.161Z. This is a derived metadata inventory; it does not redistribute raw TXLine data.
+
+| Fixture | Match | Kickoff UTC | Frozen total | Evidence grade | Bounty lane | Long-run lane |
+|---|---|---:|---:|---|---|---|
+| 18213979 | Norway vs England | 2026-07-11T21:00:00.000Z | O/U 2.5 | sampled_price_history | signal_research_only | no (predates_long_run_lane_start) |
+| 18218149 | Spain vs Belgium | 2026-07-10T19:00:00.000Z | O/U 2.5 | paired_order_books | book_lifecycle_replay | no (predates_long_run_lane_start) |
+| 18222446 | Argentina vs Switzerland | 2026-07-12T01:00:00.000Z | O/U 2.5 | sampled_price_history | signal_research_only | no (predates_long_run_lane_start) |
+
+Summary: 1 paired-book replay (0 strategy-executable; 1 lifecycle-only), 2 sampled-history research fixtures, 0 unavailable.
+
+Sampled-price fixtures may demonstrate signal generation but cannot produce registered paper fills, executable CLV, or profitability evidence. A paired capture that starts inside the 15-minute signal cutoff may prove book/close/resolution lifecycle behavior but cannot create a registered strategy fill. Every listed fixture predates the fresh long-run lane and is excluded from its stopping count.
diff --git a/docs/research/paper-study-current.md b/docs/research/paper-study-current.md
new file mode 100644
index 0000000..19cfc4a
--- /dev/null
+++ b/docs/research/paper-study-current.md
@@ -0,0 +1,43 @@
+# Samaritan Paper Study Evidence
+
+> **Invalidated/suspended July 14, 2026:** this is a preserved zero-observation v1 ledger report, not the current study. It cannot admit cases. The corrected v2 protocol was registered separately on July 18 and begins from fresh, isolated ledgers.
+
+Generated: 2026-07-12T16:48:13.459Z
+Protocol: `paper-study-v1-2026-07-12`
+Frozen config SHA-256: `7dcb4c20ac05195bdb4a6c8803432079eeefb8cc747c186f576084dc772fd2a2`
+Fixture universe generated: 2026-07-12T09:50:09.161Z
+Real-money gate: **CLOSED**
+
+This report is reconstructed from append-only decision ledgers. Bounty results are exploratory. Long-run rows and endpoints remain sealed until the registered stopping rule is met.
+
+| Lane | Chain rows | Chain head |
+|---|---:|---|
+| Bounty | 1 | `1ff24c63635535548d17200c478ad895df929a38e43e662325f6a26aad949487` |
+| Long-run | 1 | `488ed79a089e35ab2d70800352c49d3587e0284ad64bb0eda32d848874194c92` |
+
+## Bounty Demonstration Lane
+
+Status: **EXPLORATORY**
+Reason: Complete close and settlement evidence is required before endpoints are computed
+Counts: 0 matches / 0 signals / 0 filled matches / 0 fills / 0 settled fills.
+
+| Fixture | Kickoff UTC | Total | Signals | Fills | Fill rate | Half-spread bps | Slippage bps | Gross CLV bps | Net CLV bps | Settlement P&L ($) | Net return bps |
+|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
+
+Endpoints are unavailable until close and settlement evidence is complete.
+
+### Guardrails
+
+| Fill rate | Slippage | Drawdown | Depth complete | Close complete | Settlement complete |
+|---:|---:|---:|---|---|---|
+| 0.0% (fail) | - bps (fail) | $0.00 (pass) | no | no | no |
+
+## Long-Run Profitability Lane
+
+Status: **SEALED**
+Reason: Long-run endpoints sealed until 20 filled matches and 40 fills
+Counts: 0 matches / 0 signals / 0 filled matches / 0 fills / 0 settled fills.
+
+Primary endpoints and per-match rows are sealed until both registered stopping thresholds are met.
+
+No result in this artifact authorizes real-money execution.
diff --git a/docs/research/paper-study-ledger-initialization.md b/docs/research/paper-study-ledger-initialization.md
new file mode 100644
index 0000000..2ef2c7d
--- /dev/null
+++ b/docs/research/paper-study-ledger-initialization.md
@@ -0,0 +1,14 @@
+# Paper Study Ledger Initialization
+
+> **Preserved v1 audit record — invalidated and suspended July 14, 2026.** These zero-observation ledgers must not be reopened or relabelled as v2.
+
+Protocol: `paper-study-v1-2026-07-12`
+Frozen config SHA-256: `7dcb4c20ac05195bdb4a6c8803432079eeefb8cc747c186f576084dc772fd2a2`
+Real-money gate: **closed**
+
+- Bounty ledger start: 2026-07-12T09:45:17.212Z; chain rows: 1; status: exploratory.
+- Long-run ledger start: 2026-07-12T09:45:17.212Z; chain rows: 1; report status: sealed.
+- Long-run stopping counts at initialization: 0 filled matches / 0 fills.
+- Fixtures before the long-run start timestamp are excluded from its stopping count.
+
+No wallet, venue authentication, token, deposit, approval, order, or money movement is part of either ledger initialization.
diff --git a/docs/research/semifinal-capture-schedule.md b/docs/research/semifinal-capture-schedule.md
new file mode 100644
index 0000000..00bad44
--- /dev/null
+++ b/docs/research/semifinal-capture-schedule.md
@@ -0,0 +1,32 @@
+# Semifinal Paired Capture Schedule
+
+Confirmed by Deborah on July 12, 2026 for public-data capture only. Both mappings remain `tradeable: false`; confirmation does not authorize orders or money movement.
+
+| Match | TXLine fixture | Polymarket events | Absolute capture window | Verification | Current status |
+|---|---:|---|---:|---:|---|
+| France vs Spain | `18237038` | `691040`, `691131` | `2026-07-14T16:00:00Z`–`2026-07-14T22:00:00Z` | 2026-07-14 23:15 UTC / 2026-07-15 00:15 Lagos | **Failed closed** before kickoff; inadmissible |
+| England vs Argentina | `18241006` | `694581`, `694786` | `2026-07-15T16:00:00Z`–`2026-07-15T22:00:00Z` | 2026-07-15 23:15 UTC / 2026-07-16 00:15 Lagos | **Failed closed** after a Polymarket stall; inadmissible |
+
+The England-Argentina one-shot supervisor reruns strict config and credential-scope validation, refuses duplicate/output-colliding processes, records status/log/PID evidence, and enforces the configured absolute window. It allows 180 seconds of startup grace, rejects launch skew beyond 120 seconds, and marks the run failed if any required stream stops advancing for 300 seconds. Terminal verification requires the capture manifest and required artifacts to agree with the configured fixture, slugs, and window; nonempty files alone do not prove successful coverage.
+
+Capture and verification are evidence-only. V2 registration does not retroactively admit either failed semifinal or permit incomplete evidence into a study. A fresh fixture must pass every admission gate before persistent Claude execution or paper observation. Capture code may not initiate Polymarket authentication, refresh or activate market tokens, access a wallet, approve, deposit, trade, place orders, or move money.
+
+A successful England-Argentina capture would still be only a candidate input. Post-run admission requires verified paired timing, synchronized pre-cutoff overlap, explicit lifecycle evidence, exact mapping/rules review, and Deborah's prior registration of a corrected v2 protocol. Candidate mappings are never promoted automatically.
+
+Verified July 15, 2026 00:23 UTC: the France-Spain capture failed closed. The recorded artifacts stop at `2026-07-14T18:18:03Z`, before the `2026-07-14T19:00:00Z` kickoff, the run log has no completion marker, the PID file remained behind, no public `market_resolved` event was captured, and `data/live/gamma-discovery/candidate-mappings.json` still contains zero evidence-bearing records for fixture `18237038`. The capture is not admissible for study or lifecycle replay; the later v2 registration does not retroactively admit it.
+
+Verified July 16, 2026 00:17 UTC: the England-Argentina capture also failed closed. The supervisor launched before the `2026-07-15T18:45:00Z` signal cutoff (`startedAt` `2026-07-15T16:00:05.162Z` on the Polymarket side and `2026-07-15T16:00:07.504Z` on the TXLine side), but it marked the run failed at `2026-07-15T20:16:33.299Z` after logging `polymarket capture stream stalled for 302214.22314453125ms`. The raw symlinked files contain traffic through roughly `2026-07-15T20:16:23Z`, yet both terminal manifests remained `status: "running"` with `endedAt: null`; the Polymarket terminal manifest reported zero messages/connects/in-scope books despite a nonempty capture file, the TXLine terminal manifest never summarized its streams, no public `market_resolved` event was captured, the score stream never reached a `game_finalised` frame, and the local candidate registry still contains zero evidence-bearing records. Canonical capture-order replay remains blocked as well: the live candidate registry has zero records, and replay against the research registry fails immediately with `UnmappedPolymarketAssetError` on the captured totals assets. The ignored local analysis manifest therefore remains `failed_closed`; the later v2 registration does not retroactively admit it.
+
+## Deterministic post-capture bridge
+
+Run this only after the one-shot supervisor has written and verified all terminal artifacts; it is deliberately not part of the armed supervisor:
+
+```sh
+pnpm capture:analyze -- --capture-config config/captures/england-argentina-2026-07-15.json
+```
+
+The command atomically writes `data/live/paired-england-argentina-2026-07-15/analysis-manifest.json`. It hashes every input and derives only licensed-safe metadata: the exact fixture and causally selected full-time total, sorted outcome assets, selected-condition book depth, exact TXLine odds and completed score evidence, kickoff close, public resolution, and the bounded canonical ingress profile. `checkedAt` is excluded from the deterministic proof commitments.
+
+The status layers are intentional. `verified_capture` proves completed capture/microstructure only and carries no admission authority. Only a schema-v2 `verified` record has an exact causal selected-total binding, and its `admission` still remains `failed_closed` until the mapping has Deborah's settlement review plus selected close/resolution and pre-cutoff overlap. Missing or tampered inputs produce a durable `failed_closed` manifest; capture-only confirmation never promotes a mapping.
+
+If the protocol is later registered and all admission gates are satisfied, the real-Claude replay command must explicitly name both `--run-label` and `--fixture`. It defaults to finite causal speed `1`; speeds above `1` and `Infinity` are refused. Before environment or ledger mutation, preflight hashes the exact replay bytes while parsing them, freezes only the admitted canonical events under hard count and byte limits, and sizes a bounded queue for that finite selected-market snapshot (plus 25% headroom), failing closed above the in-process ceiling. The model-facing runtime replays that frozen snapshot without reopening mutable capture files. The allowlist passes only exact selected-market odds/books/prices/resolution, exact-fixture scores, and feed-health events.
diff --git a/docs/submission/demo-script-v1.md b/docs/submission/demo-script-v1.md
new file mode 100644
index 0000000..8d8c13b
--- /dev/null
+++ b/docs/submission/demo-script-v1.md
@@ -0,0 +1,81 @@
+# Samaritan Demo Script v1 — Target 4:30–4:45
+
+> **Narrator and owner:** Deborah. She must rehearse, personally review every visible artifact, rewrite the narration in her natural voice, and submit the final video herself. Do not present Claude as the participant or builder of record.
+
+## Recording setup
+
+- Record one continuous 1080p browser/terminal walkthrough; target **4:30–4:45** to retain at least 15 seconds of screening margin below the bounty's hard 5:00 limit.
+- Use the final public commit and public URL. Hide notifications, bookmarks, usernames, local private paths, credentials, wallet software, and raw capture files.
+- Pre-open: Command, Matchroom, Study, Proof, a terminal at the public repo, and the frozen receipt.
+- Use only derived dashboard output. Do not expose exact TXLine probability levels or raw TXLine payloads.
+- Run all commands once before recording. Do not fake output or splice separate evidence into one purported live trade.
+
+## Exact shot list and narration
+
+| Time | Screen and action | Deborah narration |
+|---|---|---|
+| **0:00–0:20** | **Command** hero: “The trade Samaritan refused.” Keep the Paper and Real-money gate closed badges visible. | “I’m Deborah, and this is Samaritan: my auditable sports-market agent built with official TXLine data and captured mainnet evidence. It does not promise winning bets. It proves why it acted—or, just as importantly, why it refused. This bounty build is paper-only and the real-money gate is closed.” |
+| **0:20–0:50** | Scroll through **System posture** and the integrity panel, then briefly show the architecture diagram from the technical overview. | “TXLine odds and scores become canonical probability events. Live and captured-replay adapters feed the same runPaperSession conductor, with no mode flag for the strategy. Deterministic code detects a case; Claude can return only a strict thesis; deterministic risk decides; the paper adapter simulates against executable depth; and every stage is hash-chained before the next action. This is a shared runtime contract, not a claim that the public dashboard is a live 24/7 trader.” |
+| **0:50–1:40** | Open **Matchroom** for the Spain–Belgium replay. Play or scrub across the first-goal marker, book chart, decision rail, and authoritative `NO TRADE` verdict. | “This is not synthetic. It is a synchronized, read-only Spain–Belgium capture pairing TXLine mainnet SL12 delivery with public Polymarket books. Here, the public book had already repriced before the goal reached Samaritan. Across all 18 measured market-event cases in this capture, zero showed a clean post-TXLine stale window. Samaritan kept STALE_QUOTE disabled and made no order. The dashboard exposes only a 25-basis-point TXLine movement bucket; the exact licensed level stays private.” |
+| **1:40–2:15** | Open **Study**. Show the invalidated v1 audit, corrected v4 Total Goals panel, registered-forward-paper badge, and the visually separate **Synthetic full-lifecycle proving fixture** panel. Keep `Zero qualifying v2 observations`, `Performance use: Excluded`, and `Solana anchor: Not submitted` visible. | “We also caught our own bad result. The first total-line selector used future information, so I preserved and invalidated v1 instead of hiding it. After repairing causality and economic-case counting, the held-out Total Goals candidate had 38 normalized buy cases across 18 fixtures, plus 132.7 probability basis points after a 100-basis-point proxy, with a fixture-clustered 95 percent interval from plus 14.3 to plus 243.9. These are sampled-price signal results—not alpha, fills, or profitability. I registered the exact corrected configuration for forward paper observation on July 18, before any qualifying v2 observation. The green panel below is a different evidence class: synthetic wiring proof, permanently excluded from those results.” |
+| **2:15–3:20** | Terminal: run `pnpm demo`. Pause on the top boundary label, then the runtime and receipt sections. Highlight `20` canonical events, `12` ledger rows, `filled_settled`, both `synthetic_stub` agent records, and all external-call counters at zero. | “Now I’ll prove the whole lifecycle without pretending it is performance evidence. This synthetic fixture sends 20 invented canonical events through the same runPaperSession conductor, with deterministic stubs at the two model boundaries: signal, triage, thesis, deterministic risk, execution intent, paper fill, kickoff close, settlement, and receipt. It produced 12 version-two ledger records. It made zero TXLine, Polymarket, Anthropic, wallet, Solana RPC, or real-order calls, and it is permanently excluded from performance results.” |
+| **3:20–4:00** | Terminal: run `pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json`. Hold on `valid: true`, `filled_settled`, `synthetic: true`, the `synthetic_stub` invocation classes, and both Solana fields. | “A judge can verify the frozen receipt offline. This checks its strict schema, canonical hash, disclosed lifecycle relationships, disclosed agent-run records, and committed ledger head. Here both model steps are explicitly synthetic stubs. For an admitted real API run, Samaritan verifies its invocation-evidence chain locally before generating a receipt reference; the portable receipt does not independently prove membership, and no Anthropic-side attestation or signed billing proof is claimed. This receipt does not replay private source payloads or prove profitability. Solana anchor metadata and network verification are both false.” |
+| **4:00–4:25** | Terminal: run `pnpm public:audit`, then return to **Command → Proof**. | “The judge bundle also passes a public-artifact audit. It contains only a strict allowlist of derived artifacts, with no credentials or wallet controls, no raw TXLine response, and no exact TXLine probability series. Judges can open the dashboard and run the proof without a subscription, API key, or wallet.” |
+| **4:25–4:45** | Keep **Proof** and the closed-gate badge visible. Briefly show `Solana anchor: Not submitted`; do not show a placeholder or fake explorer link. | “The devnet prepare, human-gated submit, and read-only verify tools are implemented, but this bounty release intentionally submits no anchor transaction. The receipt is honestly unanchored. Samaritan’s edge is disciplined autonomy: fast deterministic detection, bounded judgment, code-owned risk, and evidence you can challenge. I’m Deborah, the owner and submitter. Thank you.” |
+
+## Commands to have ready
+
+```bash
+pnpm demo
+pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json
+pnpm public:audit
+```
+
+Expected proof markers on the final commit:
+
+- `synthetic: true`
+- `performanceUse: "excluded_synthetic"`
+- `protocolStatus: "registered"`
+- `realMoneyGate: "closed"`
+- `canonicalEventsPublished: 20`
+- `ledgerRows: 12`
+- `finalLifecycleStatus: "filled_settled"`
+- `valid: true`
+- `solanaAnchorMetadataPresent: false`
+- `solanaNetworkVerificationPerformed: false`
+
+Re-record if any expected marker differs; do not narrate a stale number.
+
+## Non-negotiable wording guardrails
+
+Never say:
+
+- “profitable,” “proof of alpha,” “winning strategy,” or “guaranteed edge”;
+- “real trade,” “real fill,” or “live Polymarket execution”;
+- “v2 already has results,” “v2 produced this synthetic fill,” or “registration proves alpha”;
+- “the receipt is on-chain,” “anchored,” or “externally timestamped” without a verified transaction;
+- “Claude placed/approved/sized the trade”;
+- “this synthetic lifecycle used live TXLine, Claude, or Polymarket”;
+- “the invocation evidence is Anthropic-signed/provider-attested”; or
+- “raw TXLine data is in the public replay.”
+
+Use instead:
+
+- “sampled-price historical signal evidence”;
+- “synthetic full-lifecycle proving fixture, excluded from performance evidence”;
+- “deterministic paper execution”;
+- “Claude submits a bounded thesis; code owns risk and execution”;
+- “v2 is registered for forward paper observation only; zero qualifying observations existed at registration”;
+- “local invocation-evidence chain verified at generation; portable receipt carries a reference, not an offline membership proof or provider attestation”; and
+- “offline receipt verified; Solana not submitted or network-verified.”
+
+## Final recording gate
+
+- [ ] Total runtime is 4:30–4:45 and never exceeds 5:00 after platform processing.
+- [ ] Every screen comes from the final public commit or deployed public artifact.
+- [ ] The captured refusal and synthetic lifecycle are visually and verbally separated.
+- [ ] The v4 number is immediately limited to sampled-price signal evidence.
+- [ ] The receipt visibly reports no Solana anchor/network verification.
+- [ ] No placeholder or explorer link appears in the intentionally unanchored release.
+- [ ] No secret, private path, raw TXLine record, wallet UI, or unverified link appears.
+- [ ] Deborah has rewritten and delivered the narration in her own voice.
diff --git a/docs/submission/public-release-scope.md b/docs/submission/public-release-scope.md
new file mode 100644
index 0000000..46aa0c8
--- /dev/null
+++ b/docs/submission/public-release-scope.md
@@ -0,0 +1,75 @@
+# Samaritan Public Release Scope
+
+**Status:** release-candidate allowlist, July 18, 2026
+**Owner and final approver:** Deborah
+**Purpose:** prevent private licensed data, credentials, local creative exports, and unreviewed utilities from entering the bounty repository or hosted judge surface.
+
+This is an inclusion allowlist, not a claim that the current dirty working tree is already a public release. The final public commit must be assembled deliberately and rerun through every gate below.
+
+In this document, **public artifacts** means the hosted UI/API responses, frozen downloadable evidence, receipt, screenshots, video, and submission payload. Reproducibility-oriented Git source and `tradeable: false` capture configuration may contain opaque, non-secret official API request identifiers; those selectors are not raw feed responses and cannot reconstruct the feed. They remain excluded from every public artifact until sponsor approval, together with all raw rows, exact probability series, captured files, and credentials.
+
+## Include in the public submission
+
+- Root product metadata and instructions: `AGENTS.md`, `README.md`, `plan.md`, `.nvmrc`, `package.json`, `pnpm-lock.yaml`, and `pnpm-workspace.yaml`.
+- CI: `.github/workflows/ci.yml`.
+- Product code: `src/`, including canonical ingestion contracts, replay, features, detectors, bounded agent harness, deterministic paper risk/execution, ledger, receipt, dashboard server, public audit, and devnet anchor tooling.
+- Judge UI: `apps/dashboard/` source and build configuration, excluding generated `dist/`.
+- Tests: `test/` and the Phase 0 deadline test under `phase0/test/`.
+- Licence-safe frozen proof: `public/artifacts/dashboard/` and the synthetic receipt fixture under `proof/fixtures/`.
+- Documentation: architecture, research methodology and limitations, invalidation record, submission package, proof contracts, and API feedback under `docs/`.
+- Capture-only configuration: reviewed JSON files under `config/captures/`; every public copy must remain `tradeable: false`.
+- Phase 0 source and examples needed to explain official TXLine integration, but never its local secrets, tokens, wallets, captured output, or logs.
+
+## Exclude from the public submission
+
+The following are enforced by `.gitignore` and must remain absent from both Git history and the deployed image:
+
+- `.env` and credential-bearing `.env.*` files (the placeholder-only tracked `phase0/.env.example` is permitted), wallet/keypair directories, session tokens, JWTs, and credentials;
+- `data/`, SQLite/DuckDB files, `samples/`, raw SSE frames, captured replay archives, logs, and PID files;
+- `content/` social-video projects and renders, which are not part of the judge runtime or final bounty video;
+- `design/` explorations and screenshots, some of which predate the derived-only public data contract;
+- one-off `.spain-belgium-verify*.ts` local verification helpers;
+- `node_modules/`, `dist/`, coverage output, OS metadata, and local collaboration notes.
+
+The public artifact auditor is the second line of defence. It rejects raw/reconstructive TXLine field names, likely secrets, private filesystem paths, unsafe links, and oversized artifacts inside the frozen bundle.
+
+## Dependency isolation and Phase 0 advisory
+
+The judge/runtime workspace and the Phase 0 subscription utility are intentionally separate install graphs:
+
+- Root `pnpm install --frozen-lockfile` does not install `phase0` dependencies.
+- The root judge/runtime graph pins the `@solana/web3.js` → `jayson` `uuid` edge to patched `uuid@11.1.1`. The exact release lockfile passes `pnpm security:audit` at the moderate-or-greater threshold with no known production vulnerability; rerun that command after any dependency change.
+- Phase 0 still carries `@solana/spl-token` → `bigint-buffer@1.1.5`, which has the high-severity availability advisory [GHSA-3gc7-fjrx-p6mg](https://github.com/advisories/GHSA-3gc7-fjrx-p6mg) and no patched package release.
+
+The affected Phase 0 module is a local, human-invoked TXLine token-account/subscription helper. It is not imported by the dashboard, frozen demo, receipt verifier, capture process, or hosted read-only API. Do not expose its auth/subscription commands in the hosted image. Replacing its associated-token helpers changes a wallet/token path and therefore requires Deborah's explicit review and authorization under `AGENTS.md`; until then, this is an isolated and disclosed release exception, not a silently accepted production claim.
+
+## Final public-commit gate
+
+Run from a clean clone of the intended public commit:
+
+```bash
+corepack enable
+pnpm install --frozen-lockfile
+pnpm security:audit
+pnpm judge:check
+pnpm --dir phase0 install --frozen-lockfile
+pnpm --dir phase0 exec tsc -p tsconfig.json --noEmit
+pnpm --dir phase0 test
+```
+
+Then verify all of the following before pushing or deploying:
+
+- `git status --short` is empty after the intentional commit.
+- `git ls-files` contains no path excluded above.
+- Regenerate the manifest after the final export, record the exact public-bundle and receipt hashes from that frozen commit, and make the video/submission text match them; do not reuse an earlier snapshot's counts or hashes.
+- The dashboard works signed out on desktop and mobile without a wallet, credential, subscription, or private archive.
+- No exact TXLine probability series or raw response is visible.
+- The receipt says unanchored, no explorer transaction is shown, and no network-backed proof is implied; the bounty release intentionally omits a devnet anchor.
+- Deborah has reviewed every public claim and submits the entry herself.
+
+## Human release gates still open
+
+1. The approved GitHub remote is `Pavilion-devs/samaritan`; freeze and merge the exact release commit to default `main`. The [hosted deployment](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/) has passed signed-out desktop/mobile route and API-method checks and must remain publicly reachable.
+2. The bounty release is intentionally unanchored. Do not submit a devnet transaction or add an explorer link unless Deborah explicitly changes that scope in a separate authorization.
+3. Deborah sends and retains the sponsor clarification request; silence is not approval.
+4. Deborah rewrites/rehearses the narration, records the final video, verifies every public URL, and submits through Superteam Earn.
diff --git a/docs/submission/sponsor-clarification-request.md b/docs/submission/sponsor-clarification-request.md
new file mode 100644
index 0000000..a061681
--- /dev/null
+++ b/docs/submission/sponsor-clarification-request.md
@@ -0,0 +1,41 @@
+# Sponsor Clarification Request
+
+**Status:** Ready for Deborah to send
+**Sender:** Deborah, the human participant and project owner
+**Suggested recipient:** TxODDS hackathon support / TxLINEChat
+**Subject:** Eligibility and public derived-data clarification for Samaritan
+
+Hello TxODDS team,
+
+I am Deborah, the human participant, project owner, and submitter of Samaritan, an entry for the Trading Tools and Agents bounty. I remain responsible for the product decisions, submission, demo, and any judge interview. Samaritan uses Claude as a constrained runtime component: it can produce a structured research thesis, but deterministic code owns eligibility, risk, sizing, and execution. Claude has no wallet or order-placement access.
+
+Before submission, could you please confirm two points in writing?
+
+1. **Human participation and agent products.** The bounty invites trading-agent products, while the hackathon terms say entries must be created, developed, and submitted by human participants and the listing metadata identifies the opportunity as `HUMAN_ONLY`. Is Samaritan eligible under the structure above, with me as the human participant and submitter and Claude disclosed only as a constrained component of the product? Is there any additional disclosure wording you would like us to use?
+
+2. **Public derived TXLine output.** We will not publish raw TXLine captures, raw API responses, or a reconstructable copy of the feed. May our public, no-login judge demo display the following outputs derived from TXLine?
+
+ - detector labels and decisions;
+ - changes, velocities, z-scores, CUSUM values, cross-market gaps, and latency aggregates;
+ - aggregated or rounded probability movements rather than raw probability series;
+ - fixture and market identifiers needed to explain a decision;
+ - hashes or proof references that let a judge verify provenance without recovering the source feed;
+ - a small frozen replay bundle containing only those derived values and decisions.
+
+Please also confirm whether showing an exact normalized `Pct / 100` value or time series would still count as publishing TXLine Data. Unless you approve that expressly, we will omit exact normalized series and expose only non-reconstructive transformations and aggregates.
+
+If the public judge demo must be removed or changed when the hackathon data licence ends, please tell us what availability window you expect for judging.
+
+Thank you. We want the submission to demonstrate TXLine deeply while respecting both the human-participation rule and the data licence.
+
+Deborah
+Human participant and project owner, Samaritan
+
+## Internal handling notes
+
+- Deborah must send this request herself and retain the complete written response.
+- Do not paraphrase silence as approval.
+- Until an affirmative response is received, keep exact normalized TXLine probability series and reconstructive replay data out of public artifacts.
+- Until fixture/market-identifier display is affirmatively approved, omit TXLine-specific identifiers from hosted UI/API output, downloadable evidence, receipts, screenshots, video, and submission copy unless their public provenance is independently documented; keep the judge story on derived decisions, buckets, aggregates, and hashes. Opaque request selectors may remain in reproducibility-oriented source/capture configuration because they are not raw or reconstructive feed payloads.
+- If the answer narrows permissible use, update the dashboard, repository bundle, video, and submission copy before publication.
+- This draft is an operational clarification request, not legal advice.
diff --git a/docs/submission/submission-claims-matrix.md b/docs/submission/submission-claims-matrix.md
new file mode 100644
index 0000000..cdd5e5a
--- /dev/null
+++ b/docs/submission/submission-claims-matrix.md
@@ -0,0 +1,112 @@
+# Samaritan Submission Claims Matrix
+
+**Purpose:** Single source of truth for README, dashboard, video, bounty form, and judge-interview claims
+**Owner:** Deborah, the human participant and submitter
+**Rule:** A claim may move to “implemented and verified” only after its evidence gate passes on the exact public submission commit.
+
+## Status vocabulary
+
+| Status | Meaning | Public treatment |
+|---|---|---|
+| **Implemented and verified** | Present in the current system and supported by a named test, artifact, or captured observation | May be stated narrowly, with its limitation |
+| **Planned / conditional** | Designed, in progress, or dependent on an unmet gate | Roadmap only; never use present tense |
+| **Prohibited / withdrawn** | False, legally unsafe, invalidated, unsupported, or outside the bounty build | Must not appear as a positive claim |
+
+This matrix records the evidence available in the Phase status documents. It is not permission to publish a dirty or unverified workspace. Re-run the release checks and update the matrix before the final submission is frozen.
+
+## Implemented and verified claims
+
+| Claim area | Safe public formulation | Evidence and boundary |
+|---|---|---|
+| Human ownership | “Samaritan is Deborah's submission. Claude is a constrained component of the product.” | Deborah remains participant, project owner, submitter, narrator, and interview representative. Do not call Claude the participant or submitter. Sponsor clarification remains required. |
+| TXLine integration | “Samaritan consumes official TXLine fixture, odds, and score surfaces and normalizes captured field shapes into canonical events.” | Phase 1 reports mainnet live and historical integration. `Pct` is divided by 100; `Prices` remains integer ×1000; missing values are not coerced to zero. See [Phase 1](../PHASE-1-STATUS.md). |
+| Mainnet real-time evidence | “The integration has consumed TXLine mainnet SL12 live data.” | A read-only live smoke and synchronized capture are documented. The public deployment also exposes a point-in-time, aggregate-only SL12 connectivity pulse; this does not make every public case live. |
+| Shared replay/live conductor | “Live and captured-replay adapters feed the same `runPaperSession` conductor with canonical events; no replay/live mode field is exposed to strategies.” | The captured adapter is exercised through the conductor; live fan-in/cleanup and the shared typed source contract are tested separately. Session tests cover chain verification, abort, and fail-closed source/runtime errors. Claim one implemented conductor and contract parity—not a deployed 24/7 autonomous service or a completed live trading lifecycle. |
+| SSE resilience | “TXLine ingestion supports gzip, `Last-Event-ID`, reconnect, snapshot backfill, and event-ID deduplication.” | Implemented and tested in Phase 1. Do not claim zero data loss under every outage unless a release test proves it. |
+| Storage integrity | “Raw ingress, canonical events, and decision lifecycle records are append-only in SQLite and locally hash-chained.” | Update/delete triggers and chain verification are documented in Phases 1 and 4. Do not call the current local chain externally timestamped, fully canonical, or Solana-anchored. |
+| Strict LLM boundary | “Claude can submit only a strict research thesis. It cannot size a trade, access a wallet, authenticate to a venue, or place an order.” | Strict Zod/tool contracts and negative-path tests are documented in Phase 4. The analyst output is bound to the source signal. |
+| Deterministic paper risk | “Code—not the model—rechecks eligibility, evidence, freshness, fee metadata, limits, exposure, drawdown, and inventory before paper execution.” | Implemented for the Phase 4 paper slice. Current sizing is a fixed `$3` paper stake under a `$50` paper bankroll, not full Kelly/correlation production risk. |
+| Paper execution | “The paper adapter walks captured executable depth, supports partial/no fills, applies market metadata, and never calls a trading venue.” | Phase 4 documents depth walking, tick/minimum checks, fee handling, slippage, close, and settlement logic. The public proving case is synthetic and cannot support a fill-rate or profitability claim. |
+| Fail-closed behavior | “Schema failures, mismatches, stale evidence, malformed fee metadata, ineligible research signals, and execution errors terminate without a paper order.” | Covered by the Phase 4 focused tests. Avoid the absolute “all failures” formulation. |
+| Pre-action lifecycle recording | “The current orchestrator records the risk verdict and execution intent before invoking the paper adapter.” | Verified by Phase 4 tests. This is a local ordering property, not yet externally timestamped proof. |
+| Model-spend control | “Samaritan reserves model spend before each API call and enforces a code-owned `$300` project ceiling.” | Phase 4 documents an append-only spend ledger and pre-network budget check. Do not present the ceiling as trading bankroll or economic profitability. |
+| Claude invocation local audit | “For an admitted Anthropic API run, Samaritan can hash prompt/response/usage metadata into a separate append-only chain and verify the exact record locally before receipt generation.” | The live ledger validates chain continuity, run identity, usage/cost fields, and `anthropic_api` classification created by the real-client factory. A portable receipt carries a generation-time reference; its offline verifier does not independently prove membership. This is not provider attestation, an Anthropic-signed receipt, independently verified billing, or proof of response quality. V2 is registered, but no current fixture has passed admission; do not imply that an eligible run exists. |
+| Historical Claude smoke | “A bounded Haiku triage and Opus thesis path completed under the strict schemas during engineering verification.” | Phase 4 records a research-only smoke from preserved engineering history. It is not a live trade, performance sample, registered-v2 observation, provider-attested invocation, or end-to-end paper fill. V2 registration does not convert it into one. |
+| Synthetic lifecycle proof | “A deterministic, closed-world proving fixture sends 20 invented canonical events through `runPaperSession`, traversing detection, triage and analyst stubs, deterministic risk, pre-action intent, paper depth execution, close, settlement, ledger verification, and receipt generation.” | `pnpm demo` completes at `filled_settled` with 12 v2 ledger rows and zero Anthropic, TXLine API, Polymarket API, wallet, Solana RPC, or real-order calls. Both agent records are `synthetic_stub`. It is permanently `excluded_synthetic` and proves conductor wiring—not live-agent behavior, autonomy quality, alpha, fill rate, or profitability. |
+| Restart reconstruction | “The paper runtime can rebuild dedupe state, pending/terminal cases, positions, exposure, realized P&L, peak equity, and drawdown from the append-only ledger before resuming.” | Restart parity and inconsistent-history rejection are covered by the state-rehydrator and ledger tests. This does not make the overall system a deployed 24/7 service. |
+| Canonical decision hash | “V2 decision-ledger hashes commit to event kind, case identity, event and insertion timestamps, canonical payload, sequence, and prior hash.” | Tamper tests cover changed kinds, IDs, timestamps, payloads, ordering, omissions, and prior hashes. V1 records remain preserved and separately identifiable. |
+| Offline Decision Receipt | “A judge can verify the frozen synthetic receipt's strict schema, canonical hash, disclosed lifecycle relationships, and committed ledger head without private data.” | `pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json` passes. It does not replay private source payloads and does not perform Solana network verification. |
+| Licence-safe public bundle | “The dashboard reads a strict, hash-verified frozen bundle containing derived evidence only.” | `pnpm public:audit` rejects raw/reconstructive TXLine shapes, secret patterns, private paths, unsafe links, and oversized artifacts. The hosted API runs without ignored `data/` or `samples/`. Written sponsor clarification remains required before expanding the current narrow derived-field boundary. |
+| Research-only sampled history | “Sampled Polymarket price history is research-only and cannot authorize execution.” | Enforced at detector eligibility, scheduler admission, and deterministic risk according to Phases 2 and 4. |
+| Honest strategy rejection | “Current evidence rejects XMARKET_DIVERGENCE, FADER_CANDIDATE, and Match Result CONSENSUS_MOVE for v1.” | Phase 2 explicitly records these no-go decisions. A rejected strategy is not evidence that it can never work. |
+| Stale-quote feasibility case | “In one synchronized Spain–Belgium study, no clean post-TXLine stale window appeared in the measured cases.” | Narrow result only: 0/18 measured market-event cases met that exploratory condition. It is not proof that stale quotes never occur. |
+| Historical study invalidation | “Samaritan's causal audit invalidated its v1 dynamic-total result and withdrew it from strategy promotion.” | The defect and affected claims are preserved in [the invalidation record](../research/historical-gate-study-v1-invalidation.md). This is evidence of falsification discipline, not alpha. |
+| Corrected historical Total Goals signal | “Under the unchanged chronological boundary, the corrected Total Goals candidate produced 38 normalized held-out buy cases across 18 fixtures, `+132.7` probability bps after the 100 bps proxy, with a fixture-clustered 95% interval of `+14.3` to `+243.9` bps.” | [Corrected v4 report](../research/historical-gate-study-causal-economic-v4.md). Must immediately add: sampled prices lack executable depth; the result justified human registration for forward paper observation but is not itself a v2 observation, alpha, fill, or profitability evidence. |
+| Registered v2 governance record | “Deborah registered the exact corrected totals-only v2 configuration on July 18 for forward paper observation only, before any qualifying v2 observation.” | [V2 registration record](../10-paper-study-v2-registration.md). Fresh versioned ledgers remain separate from v1. Registration is not a fixture admission, trade, performance result, or real-money permission. |
+| Real-money boundary | “The bounty system is paper-only and the real-money gate is closed.” | Explicit in Phase 4 and the generated paper report. Repeat this in every judge-facing description of execution. |
+| Preserved v1 ledger state | “The invalidated v1 paper ledgers are retained for audit history and remain sealed at zero filled matches and zero fills.” | These are suspended historical artifacts, not active preregistered evidence. See [current v1 paper evidence](../research/paper-study-current.md) and the invalidation record. Do not imply that an active long-run study or performance endpoint exists. |
+| Candidate mappings | “Market mappings fail closed and remain non-tradeable until explicit verification.” | Phase 1 records candidate mapping coverage but all research-registry rows remain `tradeable: false`. Do not turn candidate counts into execution coverage. |
+| Failed semifinal captures | “Both semifinal paired-capture attempts failed closed and were not admitted.” | France–Spain stopped before kickoff. England–Argentina hit a watchdog-detected Polymarket stall and lacked complete terminal manifests, final score state, public resolution, and verified mapping. These are failure-handling records, not completed replays, v2 observations, or live-Claude cases. |
+| Release verification harness | “`pnpm judge:check` runs the documented typecheck, test, demo, receipt, artifact, and dashboard gates.” | Development snapshots have passed the component gates. Do not claim that the exact public release commit passes, or publish counts, until the frozen commit is rerun and its final results are recorded. |
+| Hosted judge MVP | “The public, read-only Samaritan dashboard works without login, wallet, subscription, API key, or local archives.” | [Hosted judge MVP](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/) passed signed-out desktop/mobile route checks. This is a judge-facing observer deployment, not a 24/7 autonomous trader. |
+| Public live-connectivity pulse | “The hosted Command surface reports point-in-time evidence of TXLine mainnet SL12 connectivity through a derived-only operational pulse.” | [`/api/v1/txline/pulse`](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/api/v1/txline/pulse) returned `connected`/`current` during release verification. Its seven allowlisted fields expose only network, tier, check time, status, rounded latency, aggregate fixture count, and coarse freshness. It is not a raw feed, signal, study observation, lifecycle, or uptime guarantee. |
+
+## Planned or conditional claims
+
+| Feature or claim | Required gate before promotion | Allowed public treatment now |
+|---|---|---|
+| Live-model complete lifecycle | One eligible captured case completes event → signal → real Haiku → real Opus thesis → risk → paper result → close → settlement under a registered protocol | The current complete proving case uses deterministic model stubs. Do not describe it as a live Claude trading case or performance sample. |
+| Solana decision anchor | A future, separately authorized chain head is anchored on Solana devnet with an explorer transaction and exact public receipt | The bounty release is intentionally unanchored. No “on-chain decisions,” “every decision anchored,” or explorer-link claim is allowed. |
+| Network-backed anchor verification | A future submitted devnet transaction, explorer URL, and receipt metadata all commit to the same receipt and ledger head | Offline receipt verification and read-only anchor-verifier tooling are implemented; the bounty release makes no network-backed verification claim. |
+| Expansion of public derived replay fields | Sponsor approves any broader displayed field set and the exact public commit passes the artifact audit | Keep the current narrow, non-reconstructive bundle; do not publish private archives, raw rows, fixture-level pulse data, or exact normalized TXLine series. |
+| Economic model gate | Case-level inference cost and expected decision value are recorded; expensive analysis fails closed when uneconomic | State measured model cost where relevant, but do not claim net-positive economics. |
+| Analyst evidence tools | Implement bounded, read-only tools; test authority, latency, cost, prompt-injection, and failure handling | The current implemented analyst exit is `submit_thesis`; do not advertise unimplemented search/query tools. |
+| Full deterministic risk suite | Implement and exhaustively test quarter-Kelly, correlation/global exposure, kill switch, and durable drawdown state | Current fixed-stake paper controls may be described; the complete planned suite may not. Any money-touching judgment requires Deborah's explicit decision. |
+| Strategy tournament/personas | Four isolated paper bankrolls, scoring, and deterministic reallocation exist and are demonstrated | Architecture/roadmap only. Not part of the winning critical path. |
+| MODELER / Data Doctor | Fitted/calibrated model certificate or implemented feed-health agent and halt boundary | Roadmap only. Never imply these agents currently run. |
+| Real Polymarket execution | Phase 3 gate passes, Deborah explicitly authorizes money-touching work, deterministic controls complete, and V2 integration is independently verified | Post-bounty roadmap at most. It is intentionally outside the submission's proof. |
+| Post-hackathon operation | Appropriate commercial TXLine licence and venue/legal requirements are in place | State that continued operation requires the appropriate licence. |
+
+## Prohibited or withdrawn claims
+
+| Prohibited wording or implication | Why it is prohibited | Required replacement |
+|---|---|---|
+| “Proof of alpha” / “profitable strategy” | Corrected historical signal evidence is sampled-price research, and no qualifying forward-paper sample exists | “Falsification-first governed paper system; corrected historical signal evidence exists, v2 is registered for forward paper observation, and no profitability claim is made.” |
+| “+119 bps held-out edge,” “+219 bps raw edge,” or any v1 totals profitability number | V1 used future-informed market selection and unbounded coverage | “V1 was invalidated by causal audit; the corrected totals-only candidate is registered for paper observation only, not approved as alpha or for real money.” |
+| “73 executable trades” | Emissions included unsupported sells and complementary duplicates | Report unique executable economic cases only after v2. |
+| “Every decision is anchored on-chain” | Current decision chain is local and the bounty release intentionally submits no anchor transaction | “Lifecycle records are locally append-only and hash-chained; this release is unanchored, with optional anchoring left to a separately authorized post-bounty path.” |
+| “Production real-money trader” / “Samaritan executes real Polymarket orders” | Real-money gate is closed and no production adapter is demonstrated | “Paper execution only.” |
+| “Production-ready autonomous daemon” | The judge observer is deployed and restart reconstruction is tested, but no admitted authentic-Claude lifecycle or continuously operated autonomous service is claimed | “Deployed, read-only judge observer for the implemented paper runtime; authentic-Claude lifecycle and 24/7 autonomous operation remain unclaimed.” |
+| “AI-built submission,” “Claude's entry,” or “autonomously submitted” | Deborah is the human participant and submitter; terms require human participation | “Deborah built and submits Samaritan; Claude is a constrained product component.” |
+| Raw TXLine API responses, captures, replay files, or a reconstructable feed | Hackathon data terms prohibit redistribution/publishing of Data | Publish only sponsor-approved, non-reconstructive derived output. |
+| Exact normalized TXLine probability time series as automatically licence-safe | Dividing `Pct` by 100 may be too close to source data to constitute non-reconstructive output | Omit unless TxODDS approves it in writing; prefer rounded aggregates, movements, features, decisions, and hashes. |
+| “TXLine gives us individual bookmaker lead/lag in the free World Cup feed” | Captures exposed StablePrice consensus rows rather than a bookmaker panel | “TXLine provides a de-vigged consensus price in the captured tier.” |
+| “STALE_QUOTE is validated” | One live feasibility study found no supporting clean post-TXLine window | “STALE_QUOTE remains disabled and paper-only.” |
+| “Historical sampled prices prove executable fills” | Sampled histories lack bid, ask, size, spread, and executable depth | Use paired canonical books and post-readiness execution evidence. |
+| “All five detectors / all four personas are running” | Only three detector types are implemented in the documented Phase 2 slice; tournament/personas are unfinished | Name only the exact implemented detectors and their current evidence status. |
+| “Claude performs autonomous web/book/series research” | Current bounded production path exposes strict submissions, not the planned tool suite | “Claude evaluates a code-assembled evidence bundle and returns a strict thesis.” |
+| “The Claude invocation is provider-attested,” “Anthropic signed this receipt,” or “the portable receipt independently proves local-ledger membership” | The invocation-evidence chain is created and verified locally by Samaritan; the receipt carries only a generation-time reference and has no provider signature or independent billing oracle | “Samaritan verified the exact invocation record in its local chain before generating this reference; offline membership and provider attestation are not claimed.” |
+| “Risk uses quarter-Kelly and full portfolio correlation controls” | Current Phase 4 paper slice uses fixed stake and bounded exposure/drawdown checks | State the exact current paper risk configuration. |
+| “The proof is provably un-cherry-picked” | V2 is registered before observations, but no external timestamp alone proves completeness or prevents every selection choice | “The exact v2 config was registered before observations and fresh ledgers are append-only”; do not strengthen to universal proof without the full evidence chain. |
+| “V2 already has results,” “v2 produced a live Claude trade,” or any equivalent present-tense performance claim | Registration exists, but there were zero qualifying observations at registration and no authentic admitted lifecycle yet | “V1 is preserved as invalidated audit history; v2 is registered for forward paper observation and currently awaits eligible evidence.” |
+| “SL1 always means a 60-second delay” | The observed devnet on-chain row reported a conflicting sampling interval | Name the network/tier and current on-chain metadata explicitly. |
+| “World Cup data licence continues automatically after the hackathon” | The hackathon licence terminates; future operation requires the appropriate normal licence | Disclose the post-hackathon licensing dependency. |
+| FIFA branding, logos, or implied affiliation | Hackathon terms prohibit the implication and trademark use is unsafe | Use neutral “World Cup data via TxODDS” copy. |
+| Any guarantee of winnings, safety, correctness, or zero loss | Neither research nor deterministic controls eliminate market or implementation risk | Describe bounded controls and measured evidence, never guarantees. |
+
+## Final claim-release checklist
+
+Before freezing any README, video, dashboard, or submission copy:
+
+- [ ] Deborah has received and retained sponsor clarification on human participation.
+- [ ] Deborah has received and retained sponsor clarification on publicly displayed derived TXLine fields.
+- [ ] Every implemented claim points to evidence produced by the exact public commit.
+- [x] Every planned feature uses future tense and is visibly separated from the judge path.
+- [x] The v1 edge numbers and 73-emission framing are absent except inside the labelled invalidation history.
+- [x] V1 ledgers are described only as suspended audit history; v2 registration is dated after Deborah's explicit authorization and is never confused with an observation or result.
+- [x] Paper-only and real-money-gate-closed language appears wherever execution is described.
+- [x] No public artifact contains raw TXLine Data, secrets, private paths, or reconstructive series.
+- [x] No public surface requires a judge to pay, subscribe, authenticate, or create a wallet.
+- [x] The bounty release is explicitly unanchored, contains no explorer link, and makes no network-backed Solana verification claim.
+- [ ] The final test/typecheck/build counts were rerun and recorded rather than copied from an earlier phase report.
+- [ ] Deborah can explain every claim, limitation, and rejected strategy in her own words.
diff --git a/docs/submission/submission-form-draft.md b/docs/submission/submission-form-draft.md
new file mode 100644
index 0000000..cad0106
--- /dev/null
+++ b/docs/submission/submission-form-draft.md
@@ -0,0 +1,88 @@
+# Samaritan — Superteam Submission Form Draft
+
+> **Not ready for copy/paste without human review.** Deborah is the participant, owner, narrator, and submitter. She must personally verify the final build and links, rewrite this draft in her own voice, remove any claim that is not true on the final public commit, and submit it herself. Claude is a constrained component of Samaritan; it is not the participant or submitter.
+
+Listing checked: [TxODDS — Trading Tools and Agents](https://superteam.fun/earn/listing/trading-tools-and-agents/) on July 18, 2026. The listing requires a public repository, public working MVP, public demo video of at most five minutes, technical documentation with the exact TXLine endpoints, and API feedback. Its five criteria are **Core Functionality & Data Ingestion**, **Autonomous Operation**, **Clean, deterministic, defensible logic/code**, **Innovation/novelty**, and **Production readiness**.
+
+## Project Title
+
+**Samaritan — The Auditable, Risk-Gated Sports Trading Agent**
+
+## One-liner
+
+Samaritan turns official TXLine evidence into deterministic signals and fail-closed paper decisions, with a bounded Claude-thesis boundary and independently verifiable decision receipts.
+
+## Briefly explain your Project
+
+Sports-trading agents are easy to demo badly: send odds to a model, accept confident prose, and hide the timing, risk, or failed hypotheses. Samaritan is built around the opposite idea—**detect deterministically, reason within a strict boundary, authorize in code, and prove the entire decision lifecycle**.
+
+Samaritan consumes official TXLine fixture, odds, and score APIs, including live mainnet SL12 SSE. Live and captured-replay adapters emit the same canonical event types into one conductor, so strategy code receives no mode flag. Deterministic detectors escalate only selected cases. In an eligible registered session, Claude can return a strict thesis but cannot size a position, access a wallet, or place an order. Code-owned checks revalidate eligibility, timing, evidence, mapping, fees, exposure, and drawdown before a depth-aware paper simulator acts. Every stage is appended to a v2 hash-chained ledger before the next action and can be exported as a licence-safe Decision Receipt. Corrected v2 is registered for forward paper observation, but the current real capture did not enter Claude and the complete public lifecycle still uses deterministic model stubs.
+
+The demo proves two different things honestly. A synchronized Spain–Belgium capture shows a real no-trade: across 18 measured market-event cases, zero produced a clean post-TXLine stale window, so the detector stays disabled. A separate, prominently labelled synthetic fixture runs the full paper lifecycle—signal, triage, thesis, deterministic risk, intent, fill, close, settlement, and receipt—without any external model, venue, wallet, or network call and is excluded from performance evidence.
+
+The corrected historical Total Goals candidate produced 38 normalized held-out buy cases across 18 fixtures, `+132.7` probability bps after a 100-bps proxy, with a fixture-clustered 95% interval of `+14.3` to `+243.9` bps. This is sampled-price **signal evidence only**: it is not alpha, executable-fill evidence, or profitability. The v2 paper protocol is registered for forward observation only, the bounty build is paper-only, and the real-money gate is closed.
+
+## Problem
+
+An autonomous market tool must answer more than “what would the model bet?” It must prove which evidence was available at decision time, whether the venue had already moved, whether the position was allowed, and whether the displayed result came from the same logic used live. Without those controls, agent autonomy becomes an unreviewable risk surface.
+
+## Solution
+
+Samaritan provides one end-to-end control plane:
+
+- official TXLine live and historical ingestion with reconnect/backfill;
+- probability normalization and deterministic signal detection;
+- bounded Claude triage/thesis schemas;
+- code-owned paper-risk and execution rules;
+- identical live/replay event contracts;
+- restart-safe append-only lifecycle state;
+- a derived-only public observer dashboard; and
+- offline-verifiable Decision Receipts.
+
+## Innovation and novelty
+
+The novelty is not “AI predicts football.” It is the separation of market-speed detection, model judgment, deterministic authorization, and verifiable evidence. Samaritan treats a refusal as a first-class output, makes invalidated research visible instead of deleting it, and exposes proof boundaries precisely: a local receipt is not called an on-chain anchor, and sampled prices are not called executable fills.
+
+## Business and technical fit
+
+Samaritan is designed as an auditable decision layer for professional trading teams, market operators, and B2B intermediaries. The reusable product is the harness—feed normalization, replay parity, agent boundaries, risk authorization, execution adapters, and receipts—rather than one tournament-specific prediction. It can expand to other competitions and venues under the appropriate TXLine commercial licence and applicable legal/venue approvals.
+
+## Current status and limits
+
+- **Execution:** paper only; no real Polymarket orders.
+- **Gate:** real-money gate closed.
+- **Study:** v1 invalidated and preserved; corrected v2 registered July 18 for forward paper observation only, with zero qualifying observations at registration.
+- **Performance:** no profitability or alpha claim.
+- **Solana:** devnet prepare/submit/verify tooling exists, but the bounty release intentionally submits no transaction; the current receipt is unanchored and has no explorer link.
+- **Public data:** derived, non-reconstructive output only; no raw TXLine feed or exact TXLine probability series is redistributed.
+- **Ownership:** Deborah is the human participant and submitter; Claude is a constrained runtime component.
+
+## Required links
+
+The MVP links below passed signed-out desktop/mobile and API-method checks. Replace the remaining video placeholder only after public playback is watched end to end.
+
+- **Live and working MVP:** [samaritan-txline-2026.asaborodaniel.chatgpt.site](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/)
+- **Public judge-evidence endpoint:** [samaritan-txline-2026.asaborodaniel.chatgpt.site/api/judge/evidence](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/api/judge/evidence)
+- **Live demo video (≤5:00, publicly viewable):** `[YOUTUBE_OR_LOOM_URL]`
+- **Public repository:** [github.com/Pavilion-devs/samaritan](https://github.com/Pavilion-devs/samaritan)
+- **Technical documentation:** [Samaritan technical overview](https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md)
+
+The bounty release intentionally includes no Solana explorer link because no decision-receipt anchor is submitted. If Deborah separately changes that scope later, add a link only after the public `anchor:verify` command passes against the exact receipt.
+
+## TxLINE API experience
+
+What worked best was the combination of normalized de-vigged `Pct`, live odds and score SSE, historical interval access, rich score/action envelopes, stable fixture/market identities, and TXLine's documented validation direction. The integrated fixture/odds/score surfaces made a single live/replay event contract possible instead of forcing a demo-only architecture. Validation remains researched feedback, not a Merkle/on-chain verification claim for Samaritan's current receipt.
+
+The main friction was contract precision rather than product scope: timestamp units and snapshot omission behavior, quote-array/no-quote semantics, service-level capability metadata, score/VAR revision lifecycle, retention/empty-result semantics, auth expiry/reactivation, and end-to-end proof-verification examples. Our highest-value requests are explicit numeric units in OpenAPI, a versioned score/action state machine, authoritative per-network service-level metadata, a documented auth/SSE resume lifecycle, and fixed validation vectors with a first-party TypeScript verifier.
+
+Full draft feedback: [TXLine API feedback](https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/txline-api-feedback.md).
+
+## Final human release checklist
+
+- [ ] Deborah has rewritten the submission in her own voice and can explain every sentence.
+- [ ] The MVP, video, repository, and documentation links work while signed out.
+- [ ] The video targets 4:30–4:45, never exceeds five minutes, and shows the working product, TXLine's backend role, and the exact proof commands.
+- [ ] The final public commit passes install, checks, build, demo, receipt verification, and public-artifact audit.
+- [ ] Paper-only, closed real-money gate, registered-v2-without-observations, unanchored-receipt, and sampled-price limitations remain visible.
+- [ ] No raw TXLine data, secret, private path, FIFA branding, or unsupported result is public.
+- [ ] Deborah—not an agent—submits the entry through Superteam Earn.
diff --git a/docs/submission/technical-overview.md b/docs/submission/technical-overview.md
new file mode 100644
index 0000000..6937d7d
--- /dev/null
+++ b/docs/submission/technical-overview.md
@@ -0,0 +1,113 @@
+# Samaritan — Technical Overview
+
+> **Submission-owner note:** Samaritan is Deborah's project and submission. Claude is a constrained component of the product, not the participant or submitter. Deborah must personally review this document, rewrite it in her own voice where needed, verify every link against the final public commit, and submit it herself.
+
+## What Samaritan is
+
+Samaritan is an auditable, risk-gated sports-market decision system built around TXLine World Cup odds and scores. Live and captured-replay adapters feed the same canonical conductor; deterministic TypeScript computes probability-space features and raises typed market signals. For an eligible registered session, Claude can be invoked only at a judgment boundary to return a strict thesis; it cannot size a position, access a wallet, or place an order. Code-owned risk checks make the final decision, a depth-aware simulator executes **paper orders only**, and an append-only hash chain records the lifecycle before each action.
+
+The evidence boundary is explicit: the real Spain–Belgium capture is a retrospective no-trade feasibility corpus and never entered the Claude/execution runtime. The complete lifecycle fixture uses deterministic stubs at both model boundaries. Corrected v2 was registered for forward paper observation on July 18, but no real Anthropic case is claimed until a fresh fixture passes every admission gate.
+
+This directly targets the bounty's five published criteria: **Core Functionality & Data Ingestion**, **Autonomous Operation**, **Clean, deterministic, defensible logic/code**, **Innovation/novelty**, and **Production readiness**. The current bounty build is deliberately **paper-only**: the real-money gate is closed, corrected v2 is registered for forward observation only, and no profitability claim is made.
+
+## Architecture
+
+```text
+TXLine odds + scores SSE Public Polymarket books
+ \ /
+ normalized canonical events
+ |
+ one serialized event bus
+ |
+ rolling features + deterministic detectors
+ |
+ eligible admitted case: Haiku triage -> Opus strict thesis
+ public lifecycle proof: deterministic stubs at both boundaries
+ |
+ deterministic eligibility + risk rules
+ |
+ paper execution adapter
+ |
+ append-only v2 decision ledger -> public receipt
+```
+
+The high-frequency path is deterministic. The model sees only escalated cases and its only analyst exit is `submit_thesis`, validated by a strict schema. Risk and paper execution recheck identity, evidence class, timing, market mapping, book freshness, venue metadata, exposure, and drawdown without trusting model prose. The same canonical event types feed live and replay operation; strategies receive no replay/live mode flag.
+
+### Specific TXLine endpoints used
+
+Mainnet origin: `https://txline.txodds.com`; devnet was also used for integration tests.
+
+| Method and endpoint | Use in Samaritan |
+|---|---|
+| `POST /auth/guest/start` | Start the documented guest-authentication flow. |
+| `POST /api/token/activate` | Activate a wallet-backed TXLine subscription and obtain the API token used server-side. |
+| `GET /api/fixtures/snapshot` | Discover fixtures and exact fixture/kickoff identities. |
+| `GET /api/odds/stream` | Consume live odds SSE, optionally scoped by `fixtureId`; gzip and `Last-Event-ID` are supported. |
+| `GET /api/scores/stream` | Consume live score/action SSE on the same canonical bus. |
+| `GET /api/odds/snapshot/{fixtureId}?asOf=` | Reconnect/backfill the current odds state before resuming a fixture stream. |
+| `GET /api/scores/snapshot/{fixtureId}` | Reconnect/backfill the current score state. |
+| `GET /api/odds/updates/{epochDay}/{hourOfDay}/{interval}` | Build the five-minute historical replay archive used for detector research. |
+| `GET /api/scores/updates/{epochDay}/{hourOfDay}/{interval}` | Build the aligned historical score archive. |
+| `GET /api/scores/historical/{fixtureId}` | Recover the score/action sequence for a fixture where retained. |
+
+Captured TXLine `Pct` is parsed as a de-vigged percentage and divided by 100; `Prices` is retained as integer odds multiplied by 1000. Empty quote transitions are not converted to zero. Public artifacts do not contain raw TXLine responses, exact TXLine probability levels, or reconstructive time series.
+
+The Command surface supports a server-side-only connectivity check against the official fixture snapshot. The deployment enables it only with the JWT, API token, and an explicit `TXLINE_SERVICE_LEVEL_ID=12` runtime assertion; otherwise it reports `degraded`. Its strict public response contains only mainnet/SL12, check time, connected/degraded status, rounded latency, aggregate fixture count, and coarse HTTP-response freshness. A timeout, oversized body, non-JSON response, non-array response, or malformed fixture identity row fails closed. The deployed endpoint returned `connected` and `current` during July 18 release verification. That is point-in-time operational metadata—not a v2 paper-study observation, a trading signal, a raw-feed view, or an uptime guarantee.
+
+## Live and replay are one path
+
+Both live SSE envelopes and captured historical records pass through the same strict normalizers into the same `CanonicalEvent` union and serialized bus. Features, detectors, schedulers, risk, and the decision ledger consume those events without a mode switch. Replay therefore exercises the product path rather than a separate backtest implementation.
+
+The live evidence case in the public dashboard is a synchronized, read-only Spain–Belgium capture pairing TXLine mainnet SL12 delivery with public Polymarket books. Across 18 measured market-event cases, **zero showed a clean post-TXLine stale window**. Samaritan keeps `STALE_QUOTE` disabled and presents the refusal as the correct result; it does not manufacture a trade from an unsupported hypothesis.
+
+## Risk boundary
+
+- Claude can classify or submit a research thesis only. Its schemas contain no stake, bankroll, order, wallet, credential, or venue-authentication field.
+- Deterministic code can veto; model output cannot override the implemented paper caps, enlarge a position, create an order, or bypass the closed real-money gate. A global manual kill switch belongs to the unimplemented real-money roadmap, not this release.
+- Research-only sampled prices cannot authorize execution. Only correctly mapped, post-readiness executable-book evidence can reach the paper adapter.
+- The paper adapter walks displayed depth, handles partial/no fills, applies validated fee/tick/minimum metadata, and never calls an order endpoint.
+- Every signal, triage result, thesis, risk verdict, execution intent, paper result, close, and settlement is appended before its downstream action. Restart reconstruction verifies the chain before restoring pending cases, positions, exposure, P&L, and drawdown.
+- The bounty build has no real-money execution path enabled. The real-money gate is closed and requires a separate human decision outside this submission.
+
+## Evidence and proof
+
+1. **Captured refusal:** the Spain–Belgium case demonstrates real synchronized input and a defensible no-trade result. Only derived, non-reconstructive TXLine movement buckets are public.
+2. **Corrected historical signal evidence:** under a fixed chronological split, the Total Goals `CONSENSUS_MOVE` candidate produced 38 normalized held-out buy cases across 18 fixtures, with mean `+132.7` probability bps after a 100-bps proxy and a fixture-clustered 95% interval of `+14.3` to `+243.9` bps. These sampled-price results justified Deborah's July 18 registration decision for forward paper observation; they are not themselves a v2 observation, alpha, fills, or profitability. See the [v4 report](../research/historical-gate-study-causal-economic-v4.md) and [v2 registration record](../10-paper-study-v2-registration.md).
+3. **Falsification record:** Samaritan preserved and withdrew its earlier result after discovering a future-informed selector, repaired the selector and economic-case semantics, and reran the unchanged boundary. See the [v1 invalidation record](../research/historical-gate-study-v1-invalidation.md).
+4. **Synthetic full lifecycle:** `pnpm demo` runs 20 invented canonical events through the production paper components, creating one signal, paper fill, kickoff close, settlement, and 12 v2 ledger records. The two model boundaries use deterministic stubs; there are zero external API, model, wallet, RPC, or real-order calls. The fixture is permanently marked synthetic and excluded from performance evidence.
+5. **Decision Receipt:** the public receipt commits to build/config hashes, source-evidence hashes, bucketed derived signals, model-run metadata, lifecycle ordering, and the final ledger head. `receipt:verify` checks the strict schema, canonical receipt hash, disclosed lifecycle consistency, and committed ledger head offline. It does not replay private source data or prove profitability. See the [receipt contract](../proof/decision-receipt.md).
+6. **Current Solana status:** offline prepare, human-gated devnet submit, and read-only verify tooling is implemented and tested, but the bounty release intentionally submits **no anchor transaction**. The public receipt is unanchored: `solanaAnchorMetadataPresent: false` and `solanaNetworkVerificationPerformed: false`; no explorer link exists. See the [devnet anchor boundary](../proof/solana-devnet-anchor.md).
+
+## Run the judge path locally
+
+Requirements: Node 22, pnpm 11, and a clean clone of the final public repository.
+
+```bash
+corepack enable
+pnpm install --frozen-lockfile
+pnpm judge
+```
+
+Open `http://127.0.0.1:4173`. The command runs the clean-clone verification gate before starting the server. The frozen observer bundle requires no TXLine credential, Anthropic key, wallet, RPC write, paid subscription, or private archive.
+
+In another terminal, run the reproducible proof path:
+
+```bash
+pnpm demo
+pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json
+pnpm public:audit
+```
+
+For engineering verification:
+
+```bash
+pnpm check
+```
+
+The public audit rejects raw/reconstructive TXLine fields, common secret patterns, unsafe links, private paths, and oversized artifacts. Maintainers regenerate the frozen bundle only from the private licensed workspace; judges do not need those inputs.
+
+## Business and production fit
+
+Samaritan is a decision-control and evidence layer for a professional trading team, market operator, or B2B intermediary—not a tip generator. Its reusable value is the harness: normalized live evidence, identical replay behavior, bounded judgment, deterministic authorization, failure-safe paper execution, and portable receipts. New competitions or venues can sit behind adapters while the risk and audit boundaries remain unchanged. Continued operation after the hackathon requires the appropriate TXLine commercial licence and all applicable legal/venue approvals.
+
+Further detail: [architecture](../02-architecture.md), [strategy playbook](../03-strategy-playbook.md), [API feedback](txline-api-feedback.md), and the [submission claims matrix](submission-claims-matrix.md).
diff --git a/docs/submission/third-party-notices.md b/docs/submission/third-party-notices.md
new file mode 100644
index 0000000..624f09c
--- /dev/null
+++ b/docs/submission/third-party-notices.md
@@ -0,0 +1,20 @@
+# Samaritan Third-Party Notices
+
+This file covers the judge-facing bounty repository and dashboard. Local ignored design/video experiments are outside the public release scope.
+
+## Data and services
+
+- **TXLine / TxODDS:** official fixture, odds, and score APIs are used under the World Cup Hackathon terms. TXLine validation surfaces were researched but are not integrated into the current Samaritan receipt, so no Merkle or on-chain source-verification claim is made. The release-candidate bundle is limited to Samaritan-derived, non-reconstructive signals, decisions, aggregates, and hashes; it does not redistribute raw TXLine data. Reproducibility-oriented source/configuration contains only opaque API request selectors, never source payloads; those identifiers remain absent from the hosted UI/API, evidence bundle, receipt, video, and submission pending written sponsor clarification. Continued post-hackathon operation requires the appropriate TxODDS licence.
+- **Polymarket:** public market metadata, sampled prices, and public order-book data are used for research and paper simulation. Samaritan places no real order in the bounty build and does not require a judge wallet or account.
+- **Anthropic Claude:** Haiku and Opus are constrained runtime components. The zero-network public proving fixture uses deterministic stubs at those two boundaries and does not call Anthropic.
+- **Solana:** `@solana/web3.js` and the public Memo program are used for offline devnet anchor preparation and human-gated submission/verification tooling. The bounty release intentionally submits no anchor transaction; the current receipt is unanchored and has no explorer link.
+
+## Fonts and visual assets
+
+- **Manrope**, distributed through `@fontsource/manrope`, is licensed under SIL Open Font License 1.1.
+- **IBM Plex Mono**, distributed through `@fontsource/ibm-plex-mono`, is licensed under SIL Open Font License 1.1.
+- The Samaritan wordmark/logo and dashboard interface artwork in the public repository are project-owned assets. No FIFA logo, team crest, tournament emblem, or endorsement mark is used.
+
+## Software
+
+Runtime and development dependencies are declared in `package.json`, with exact resolved versions and integrity hashes in `pnpm-lock.yaml`. Their respective licences remain with their authors. The public judge graph is checked with `pnpm security:audit`; the separately installed Phase 0 graph and its known upstream advisory are documented in `public-release-scope.md`.
diff --git a/docs/submission/txline-api-feedback.md b/docs/submission/txline-api-feedback.md
new file mode 100644
index 0000000..6d9fa68
--- /dev/null
+++ b/docs/submission/txline-api-feedback.md
@@ -0,0 +1,252 @@
+# TxLINE API Feedback Draft
+
+**Status:** Submission draft refreshed July 18; verify links and current service metadata immediately before sending
+**Project:** Samaritan
+**Participant:** Deborah
+**Basis:** Phase 0 through Phase 4 integration work using the OpenAPI specification, mainnet SL12, devnet subscription metadata, historical endpoints, snapshots, SSE, and score data, plus a documentation review of validation surfaces. Samaritan does not integrate validation proofs into its current receipt.
+
+## Executive summary
+
+TxLINE gave Samaritan an unusually strong foundation for building a reproducible sports-market system. Its most valuable combination is not merely low-latency odds: it is normalized de-vigged probabilities, historical access, live SSE, rich score events, and documented on-chain validation surfaces under one data model. The integrated data surfaces make it possible to test the same deterministic decision logic in replay and live operation and to retain local evidence for why a system acted or refused to act; Samaritan does not claim that its current receipt verifies a TXLine Merkle proof.
+
+The main integration cost came from places where the documented contract did not fully describe observed behavior: timestamp requirements, service-level metadata, empty quote transitions, array alignment, retention windows, score-state semantics, and authentication renewal. None is a request for more product scope. A tighter executable contract, explicit lifecycle semantics, and a small official SDK/conformance suite would remove most of the defensive reverse engineering required by a production consumer.
+
+## What worked especially well
+
+### 1. The de-vigged probability representation is excellent
+
+`Pct` is a high-value primitive for quantitative products. It lets a consumer work in probability space without independently rebuilding bookmaker-margin removal for every quote. The captured values behaved as percentages on a 0–100 scale and summed near 100, so Samaritan normalizes them into 0–1 probabilities at ingestion.
+
+This substantially reduced ambiguity in feature engineering. It also made movements across Match Result, Over/Under, and Handicap markets comparable under one internal convention.
+
+### 2. Live and historical surfaces support reproducible research
+
+The combination of:
+
+- live odds and scores via SSE;
+- latest snapshots;
+- interval-based historical odds and scores;
+- full score history by fixture; and
+- stable fixture, market, period, parameter, and message identities
+
+allowed Samaritan to build canonical events that can be consumed by the same downstream feature and detector code in replay and live operation. Historical access was large enough to support serious research rather than a hand-built demo.
+
+### 3. SSE has the right production primitives
+
+Standard `id`, `event`, `data`, and `retry` frames, heartbeats, `Last-Event-ID`, and gzip support are the right building blocks. Gzip is particularly valuable for high-volume odds streams. Resumability also makes it possible for a consumer to distinguish reconnect delivery from new market evidence and to deduplicate safely.
+
+### 4. The score feed is richer than a scoreboard
+
+The observed score envelope included clock, possession, score, possible-event state, kickoff state, player statistics, lineups, and actions such as shots, VAR, penalties, goals, corners, and cards. This is useful for building event-aware market diagnostics and for testing whether a venue repriced before or after a material match event.
+
+### 5. Validation endpoints are a meaningful differentiator
+
+Fixture, odds, score/stat, and batch-validation endpoints create the foundation for externally verifiable decision evidence. For an autonomous system, proving the provenance of the input is almost as important as recording the output. This is a stronger product surface than an ordinary centralized odds API.
+
+### 6. The free World Cup access enabled a real integration
+
+Mainnet SL12 real-time access and historical availability allowed Samaritan to test real event flow, reconnect behavior, replay, data normalization, and synchronized market observation. The product was able to exercise the actual service rather than rely on mocks alone.
+
+## Friction encountered and suggested fixes
+
+### 1. Make timestamp units and omission behavior explicit
+
+**Observed friction:** `GET /api/odds/snapshot/{fixtureId}` required `asOf` to be supplied as an epoch-millisecond value in practice. Omitting it returned only a partial snapshot during a July 10 mainnet smoke. The contract did not make that operational consequence sufficiently clear.
+
+**Suggestion:** Specify the unit and required/optional behavior in both OpenAPI and prose:
+
+- `asOf`: Unix epoch milliseconds;
+- what “omitted” means;
+- whether the result is latest-known, partial, or bounded by a server default;
+- inclusive/exclusive cutoff semantics; and
+- an example response for each case.
+
+An OpenAPI `format`, minimum/maximum example, and a conformance test would prevent seconds-versus-milliseconds failures.
+
+### 2. Publish one authoritative service-level matrix per network
+
+**Observed friction:** Mainnet SL1 was described as 60-second delayed and mainnet SL12 as real-time. The current devnet SL1 on-chain pricing row reported `samplingIntervalSec = 0`, which contradicted the earlier blanket association between SL1 and a 60-second delay.
+
+**Suggestion:** Make the on-chain subscription row the explicitly documented source of truth and publish a generated table containing, for every network and service level:
+
+- sampling interval;
+- permitted competitions;
+- historical availability;
+- program ID and mint identity;
+- activation requirements; and
+- the metadata version/effective time.
+
+Clients should be told to inspect the subscribed row instead of inferring latency from a service-level name.
+
+### 3. Define quote-array invariants and no-quote transitions
+
+**Observed friction:** Observed market rows sometimes retained `PriceNames` while both `Prices` and `Pct` were empty. Samaritan interpreted a row with both value arrays empty as an explicit no-quote transition and treated partially populated or misaligned arrays as invalid. That distinction was necessary for safety but was not fully specified.
+
+**Suggestion:** Document the legal combinations of:
+
+- `PriceNames`;
+- `Prices`;
+- `Pct`; and
+- market availability/status.
+
+Provide an explicit `quoteStatus` or `isAvailable` field if possible. At minimum, specify that empty `Prices` and `Pct` with retained names means “no current quote,” and define whether unequal non-empty array lengths are always malformed. Include examples for open, suspended, removed, and resumed markets.
+
+### 4. Document units beside every numeric field
+
+**Observed friction:** `Prices` were observed as integer odds multiplied by 1000, while `Pct` contained three-decimal strings on a 0–100 percentage scale. Both are usable, but the conventions are easy to misread and have materially different failure modes.
+
+**Suggestion:** Put units directly into OpenAPI field descriptions and examples:
+
+- `Prices`: decimal odds ×1000, integer;
+- `Pct`: de-vigged probability percentage on 0–100, decimal string;
+- timestamps: seconds or milliseconds, named per field;
+- line parameters: representation and precision rules.
+
+Consider adding machine-readable extension metadata such as `x-unit` and `x-scale` so generated clients can preserve the contract.
+
+### 5. Clarify consensus coverage versus constituent bookmaker coverage
+
+**Observed friction:** The free World Cup captures exposed `TXLineStablePriceDemargined` consensus rows rather than a multi-bookmaker panel, even though the object model includes `BookmakerId` and `Bookmaker`. That is a valid and useful product, but consumers can otherwise design a bookmaker-lead/lag strategy that the subscribed feed cannot support.
+
+**Suggestion:** For every tier, state whether odds rows are:
+
+- StablePrice consensus only;
+- individual bookmaker observations; or
+- both.
+
+Expose a capability descriptor in subscription metadata so software can fail closed before assuming constituent-book availability.
+
+### 6. Specify score/action lifecycle and revisions
+
+**Observed friction:** Goal-related score messages included initial deliveries and later confirmation revisions. The feed also exposes `PossibleEvent`, `Action`, score counters, and match-state fields, but the semantic relationship between provisional, confirmed, corrected, and reversed events needs a stronger contract for VAR-sensitive systems.
+
+**Suggestion:** Document a state machine for material events:
+
+```text
+possible → provisional → confirmed
+ ↘ corrected / cancelled
+```
+
+Define which identity remains stable across revisions, which timestamp denotes source occurrence versus publication, and how a consumer should recognize a correction or reversal. Include worked VAR and penalty examples.
+
+### 7. Keep the score parser contract open and publish the full taxonomy
+
+**Observed friction:** Captured `Stats` keys used period prefixes 0 through 7, while the earlier documentation summary did not describe the full observed range. Observed match-state IDs included 1–13 and 100, while the documentation listed a wider 1–19 family. Odds rows also frequently had blank `GameState` values.
+
+**Suggestion:** Publish versioned enumerations for:
+
+- period prefixes and their meanings;
+- stat base keys;
+- match-state IDs;
+- action names;
+- which fields may be blank on odds versus score events; and
+- forward-compatibility behavior for unknown values.
+
+An `unknown` rule is important: consumers should retain new actions and states instead of rejecting an entire event when the taxonomy expands.
+
+### 8. State retention and empty-result semantics per endpoint
+
+**Observed friction:** Interval endpoints returned data over a longer observed range than a consumer might infer from the prose documentation. Score interval coverage differed by network and date. `GET /api/scores/historical/{fixtureId}` returned successful empty bodies for some older fixtures rather than a not-found/expired distinction.
+
+**Suggestion:** Publish a per-endpoint retention table with:
+
+- guaranteed versus best-effort retention;
+- network differences;
+- current earliest available time;
+- whether an empty 200 means no events, expired data, unavailable competition, or an unknown fixture; and
+- whether retention can change by subscription tier.
+
+Response metadata such as `coverageStart`, `coverageEnd`, `isComplete`, and `emptyReason` would make research admission deterministic.
+
+### 9. Document authentication expiry and reactivation as a complete lifecycle
+
+**Observed friction:** Authentication uses both a guest JWT and activated API token. No proven refresh endpoint was available to the integration, so Samaritan detects expiry and invokes a separately supplied reactivation path rather than inventing renewal behavior.
+
+**Suggestion:** Provide an official lifecycle sequence covering:
+
+- JWT expiry and renewal;
+- API-token expiry and renewal;
+- subscription expiration;
+- whether a wallet signature may be reused;
+- error codes for each expired component;
+- retry safety and idempotency; and
+- how an open SSE connection behaves when either credential expires.
+
+A tiny official TypeScript session helper would eliminate substantial repeated work across submissions.
+
+### 10. Define event ordering and resume guarantees
+
+**Observed friction:** A safe consumer must distinguish source time, server publication time, local receipt time, replay order, and reconnect redelivery. `Last-Event-ID` and message IDs help, but the ordering guarantees across odds, scores, snapshots, and reconnect backfill need to be explicit.
+
+**Suggestion:** State:
+
+- whether IDs are unique per fixture, stream, endpoint, or globally;
+- whether IDs are monotonic;
+- whether reconnect may redeliver or reorder frames;
+- the retention window for resume IDs;
+- how gaps are signalled; and
+- the supported snapshot-plus-stream handoff procedure.
+
+An official “lossless reconnect” example should demonstrate snapshot backfill, `Last-Event-ID`, deduplication, and gap detection together.
+
+### 11. Keep hosted validation docs and repository examples version-aligned
+
+**Observed friction:** A July 18 review of the official [`txodds/tx-on-chain`](https://github.com/txodds/tx-on-chain) repository found runnable `validateStatV3` examples using `/api/scores/stat-validation-v3` and compressed multiproof fields, while the hosted/OpenAPI-facing validation material still centered the legacy and V2 shapes. The repository, rather than one versioned hosted entry point, exposed that newer shape. Samaritan did not adopt the undocumented surface.
+
+**Suggestion:** Publish a version matrix linking each API proof route to its matching on-chain method, IDL release, network program address, payload schema, deprecation state, and runnable example. Mark V3 explicitly in the OpenAPI and hosted navigation when production-supported, and retain a migration note from V2's per-stat proofs to V3's `multiproof.indices` / `multiproof.hashes` representation.
+
+### 12. Provide a first-party validation verifier and canonical proof example
+
+**Observed friction:** The validation endpoints are strategically important, but consumers still need to understand canonical leaf serialization, proof ordering, on-chain account selection, network identity, and how to verify a returned proof independently.
+
+**Suggestion:** Publish:
+
+- one fixed fixture, odds, and score validation vector;
+- the exact canonical bytes and resulting leaf hash;
+- a TypeScript verifier;
+- the expected root/account and transaction reference; and
+- clear failure examples for the wrong network, stale root, malformed path, or mismatched payload.
+
+This would turn validation from an advanced integration surface into a feature every hackathon entry could demonstrate correctly.
+
+### 13. Add an executable conformance pack and changelog
+
+**Observed friction:** The OpenAPI specification is a strong starting point, but several important behaviors were discovered only through captured traffic. In a money-adjacent system, documentation drift forces every client to build its own interpretation layer.
+
+**Suggestion:** Ship a small versioned conformance pack containing sanitized examples and assertions for:
+
+- authentication and activation;
+- snapshots with and without `asOf`;
+- live quote, no-quote, and resumed-quote rows;
+- score revisions and VAR cancellation;
+- SSE reconnect and deduplication;
+- retention-empty responses; and
+- validation proof verification.
+
+Tie it to a public schema changelog with effective dates. A generated strict TypeScript client would be helpful, but the conformance vectors are the higher-value first step.
+
+## Prioritized recommendations
+
+If only five improvements can be made, Samaritan recommends:
+
+1. Specify quote-array/no-quote invariants and numeric units in OpenAPI.
+2. Publish the score/action revision state machine, especially for VAR.
+3. Make service-level capability metadata authoritative and machine-readable per network.
+4. Document the complete auth-expiry and SSE-resume lifecycle.
+5. Release fixed validation vectors with a first-party TypeScript verifier.
+
+## Closing feedback
+
+TxLINE's strongest differentiator is the combination of normalized real-time evidence and independent validation, not any one endpoint. Samaritan was able to build a strict canonical event layer, replay research, score-aware diagnostics, and provenance-bearing decision records because those surfaces exist together.
+
+The requested improvements are primarily contract clarity. Making the implicit lifecycle rules executable would shorten integration time, reduce unsafe assumptions, and make it much easier for developers to demonstrate TXLine's on-chain verification advantage accurately.
+
+## Internal verification checklist
+
+Before this feedback is submitted:
+
+- Recheck current endpoint names and service-level rows against the live official documentation.
+- Keep all examples aggregate or schematic; attach no raw response or private capture.
+- Remove any observation that is no longer reproducible under the current API version.
+- Submit the feedback in Deborah's name as the human participant.
+- Keep Polymarket-specific feedback out of the TxLINE section.
diff --git a/package.json b/package.json
index 9233e2e..4e51c52 100644
--- a/package.json
+++ b/package.json
@@ -3,28 +3,65 @@
"version": "0.1.0",
"private": true,
"type": "module",
- "packageManager": "pnpm@10.32.1",
+ "packageManager": "pnpm@11.13.0",
"engines": {
"node": ">=22 <23",
- "pnpm": ">=10 <11"
+ "pnpm": ">=11 <12"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
+ "dash:dev": "vite --config apps/dashboard/vite.config.ts",
+ "dash:typecheck": "tsc -p apps/dashboard/tsconfig.json --noEmit",
+ "dash:clean": "node -e \"require('node:fs').rmSync('apps/dashboard/dist', { recursive: true, force: true })\"",
+ "dash:build": "pnpm dash:typecheck && pnpm dash:verify-edge && pnpm dash:clean && vite build --config apps/dashboard/vite.config.ts && vite build --config apps/dashboard/vite.worker.config.ts && pnpm dash:verify-build",
+ "dash:serve": "tsx src/dash/server.ts",
+ "dash:export-public": "tsx src/dash/export-public-bundle.ts && tsx src/dash/export-edge-bundle.ts",
+ "dash:verify-edge": "tsx src/dash/verify-edge-bundle.ts",
+ "dash:verify-build": "tsx src/dash/verify-sites-build.ts",
+ "public:audit": "tsx src/public/audit-public-artifacts.ts --bundle public/artifacts/dashboard --bundle public/artifacts/edge",
+ "security:audit": "pnpm audit --prod --audit-level moderate",
+ "demo": "tsx src/demo/run-synthetic-judge-case.ts",
+ "judge:check": "pnpm check && pnpm demo && pnpm receipt:verify -- public/artifacts/dashboard/synthetic-decision-receipt.json && pnpm public:audit && pnpm dash:build",
+ "judge": "pnpm judge:check && pnpm dash:serve",
"test": "vitest run",
"test:watch": "vitest",
- "check": "pnpm typecheck && pnpm test",
+ "check": "pnpm typecheck && pnpm dash:typecheck && pnpm test",
"smoke:replay": "tsx src/replay/smoke.ts",
"smoke:live": "tsx src/replay/live-smoke.ts",
"smoke:discovery": "tsx src/replay/discovery-smoke.ts",
+ "smoke:claude": "tsx src/agents/haiku-smoke.ts",
+ "smoke:claude:analyst": "tsx src/agents/opus-smoke.ts",
"import:mappings": "tsx src/import/build-mapping-registry.ts",
"import:archives": "tsx src/import/import-archives.ts",
"import:txline-metadata": "tsx src/import/backfill-txline-metadata.ts",
- "research:total-evidence": "tsx src/research/build-total-line-evidence.ts"
+ "research:total-evidence": "tsx src/research/build-total-line-evidence.ts",
+ "research:paired-live": "tsx src/research/analyze-paired-capture.ts",
+ "research:historical-gate": "tsx src/research/run-historical-gate.ts",
+ "paper:fixture-universe": "tsx src/harness/build-paper-fixture-universe.ts",
+ "paper:init-ledgers": "tsx src/harness/initialize-paper-study-ledgers.ts",
+ "paper:register-v2": "tsx src/harness/initialize-paper-study-ledgers.ts --register paper-study-v2-2026-07-18",
+ "paper:claude-readiness": "tsx src/harness/verify-claude-paper-runtime.ts",
+ "paper:run-captured": "tsx src/harness/run-captured-paper-session.ts",
+ "paper:report": "tsx src/harness/write-paper-study-report.ts",
+ "capture:readiness": "tsx src/harness/build-capture-readiness.ts",
+ "capture:validate": "tsx src/harness/validate-capture-configs.ts",
+ "capture:run-scheduled": "tsx src/harness/run-scheduled-capture.ts",
+ "capture:analyze": "tsx src/harness/build-paired-capture-analysis.ts",
+ "receipt:verify": "tsx src/proof/verify-decision-receipt.ts",
+ "anchor:prepare": "tsx src/chain/prepare-decision-anchor.ts",
+ "anchor:submit": "tsx src/chain/submit-decision-anchor.ts",
+ "anchor:verify": "tsx src/chain/verify-decision-anchor.ts"
},
"dependencies": {
+ "@anthropic-ai/sdk": "^0.111.0",
"@duckdb/node-api": "1.5.4-r.1",
+ "@fontsource/ibm-plex-mono": "^5.2.6",
+ "@fontsource/manrope": "^5.2.6",
+ "@solana/web3.js": "^1.98.4",
"better-sqlite3": "^12.11.1",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
"stream-json": "^3.5.0",
"ws": "^8.21.0",
"zod": "^4.4.3"
@@ -32,18 +69,14 @@
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.20.1",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
"@types/stream-json": "^1.7.8",
"@types/ws": "^8.18.1",
+ "@vitejs/plugin-react": "^5.0.4",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
+ "vite": "^7.1.7",
"vitest": "^4.1.10"
- },
- "pnpm": {
- "onlyBuiltDependencies": [
- "better-sqlite3",
- "bufferutil",
- "esbuild",
- "utf-8-validate"
- ]
}
}
diff --git a/phase0/package.json b/phase0/package.json
index 3ba7f5e..9f9d12d 100644
--- a/phase0/package.json
+++ b/phase0/package.json
@@ -3,6 +3,7 @@
"private": true,
"type": "module",
"scripts": {
+ "test": "tsx --test test/*.test.ts",
"airdrop:devnet": "tsx src/devnet-airdrop.ts",
"auth:devnet": "tsx src/txline-auth.ts --network devnet",
"auth:mainnet": "tsx src/txline-auth.ts --network mainnet",
diff --git a/phase0/pnpm-workspace.yaml b/phase0/pnpm-workspace.yaml
new file mode 100644
index 0000000..afbfa64
--- /dev/null
+++ b/phase0/pnpm-workspace.yaml
@@ -0,0 +1,8 @@
+packages:
+ - .
+
+allowBuilds:
+ bigint-buffer: true
+ bufferutil: true
+ esbuild: true
+ utf-8-validate: true
diff --git a/phase0/src/capture-polymarket-ws.ts b/phase0/src/capture-polymarket-ws.ts
index 95ece42..1f996a4 100644
--- a/phase0/src/capture-polymarket-ws.ts
+++ b/phase0/src/capture-polymarket-ws.ts
@@ -1,7 +1,13 @@
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
+import { pathToFileURL } from "node:url";
import WebSocket from "ws";
+import {
+ captureStartupFailure,
+ parseAbsoluteCaptureWindow,
+ type AbsoluteCaptureWindow
+} from "./capture-window.js";
import {
appendJsonl,
boolArg,
@@ -17,6 +23,7 @@ import {
} from "./lib.js";
import {
type AnyRecord,
+ discoverEventsByExactSlugs,
discoverWorldCupEvents,
type GammaEvent,
type GammaMarket,
@@ -29,7 +36,7 @@ import {
writeAtomicText
} from "./polymarket-lib.js";
-type AssetRecord = {
+export type AssetRecord = {
assetId: string;
outcome: string;
eventId: string;
@@ -61,6 +68,9 @@ type CaptureStats = {
reconnects: number;
forcedReconnects: number;
parseErrors: number;
+ transportHeartbeats: number;
+ inScopeBookEvents: number;
+ inScopeBookSnapshots: number;
discoveredAssets: Map;
subscribedAssets: Set;
};
@@ -88,20 +98,56 @@ function eventTeams(event: GammaEvent): string[] {
return parts.length === 2 ? parts : [];
}
-function activeAssetRecords(
+function marketSortKey(event: GammaEvent, market: GammaMarket): string {
+ const typePriority = market.sportsMarketType === "moneyline" ? "0" : "1";
+ const line = typeof market.line === "number" ? String(market.line + 1_000).padStart(8, "0") : "00000000";
+ return [
+ typePriority,
+ line,
+ String(event.slug ?? ""),
+ String(market.id ?? market.conditionId ?? ""),
+ String(market.question ?? "")
+ ].join("|");
+}
+
+export function selectSupportedAssetRecords(
events: GammaEvent[],
eventSlugs: Set,
maxAssets: number
): AssetRecord[] {
const now = Date.now();
const records: AssetRecord[] = [];
- for (const { event, market } of relevantMarkets(events)) {
+ const supported = relevantMarkets(events)
+ .filter(({ event, market }) => {
+ if (eventSlugs.size > 0 && !eventSlugs.has(String(event.slug ?? ""))) return false;
+ return market.closed !== true && market.active !== false &&
+ market.acceptingOrders !== false && market.enableOrderBook !== false &&
+ marketKickoffMs(market, event) >= now - 6 * 60 * 60_000;
+ })
+ .sort((left, right) => marketSortKey(left.event, left.market).localeCompare(marketSortKey(right.event, right.market)));
+ if (eventSlugs.size > 0) {
+ const [matchResultSlug, totalsSlug] = [...eventSlugs];
+ const matchResultMarkets = supported.filter(({ event, market }) =>
+ event.slug === matchResultSlug && market.sportsMarketType === "moneyline"
+ );
+ const totalsMarkets = supported.filter(({ event, market }) =>
+ event.slug === totalsSlug && market.sportsMarketType === "totals"
+ );
+ if (matchResultMarkets.length !== 3) {
+ throw new Error(`Exact capture requires three active Match Result conditions; found ${matchResultMarkets.length}`);
+ }
+ if (totalsMarkets.length === 0) {
+ throw new Error("Exact capture requires at least one active full-time totals condition");
+ }
+ }
+ for (const { event, market } of supported) {
if (eventSlugs.size > 0 && !eventSlugs.has(String(event.slug ?? ""))) continue;
- if (market.closed === true || market.active === false || market.acceptingOrders === false || market.enableOrderBook === false) continue;
const kickoffMs = marketKickoffMs(market, event);
- if (kickoffMs < now - 6 * 60 * 60_000) continue;
const tokenIds = parseStringArray(market.clobTokenIds);
const outcomes = parseStringArray(market.outcomes);
+ if (tokenIds.length !== 2 || outcomes.length !== 2) {
+ throw new Error(`Supported market ${String(market.id ?? market.conditionId ?? "")} lacks its required outcome pair`);
+ }
for (const [index, assetId] of tokenIds.entries()) {
records.push({
assetId,
@@ -117,9 +163,20 @@ function activeAssetRecords(
sportsMarketType: String(market.sportsMarketType ?? ""),
line: market.line ?? null
});
- if (records.length >= maxAssets) return records;
}
}
+ records.sort((left, right) => {
+ const type = (left.sportsMarketType === "moneyline" ? 0 : 1) - (right.sportsMarketType === "moneyline" ? 0 : 1);
+ if (type !== 0) return type;
+ const line = Number(left.line ?? -1) - Number(right.line ?? -1);
+ if (line !== 0) return line;
+ const market = left.marketId.localeCompare(right.marketId);
+ if (market !== 0) return market;
+ return left.assetId.localeCompare(right.assetId);
+ });
+ if (records.length > maxAssets) {
+ throw new Error(`Required supported strategy assets (${records.length}) exceed --max-assets ${maxAssets}`);
+ }
return records;
}
@@ -162,13 +219,31 @@ async function discoverAssets(options: {
runLogPath: string;
eventSlugs: Set;
maxAssets: number;
+ deadlineTsMs?: number;
}): Promise {
- const discovery = await discoverWorldCupEvents({
- outDir: join(options.outDir, "discovery"),
- manifestLogPath: options.runLogPath,
- openOnly: true
- });
- return activeAssetRecords(discovery.matchEvents, options.eventSlugs, options.maxAssets);
+ const discoveryOutDir = join(options.outDir, "discovery");
+ const events = options.eventSlugs.size > 0
+ ? (await discoverEventsByExactSlugs({
+ outDir: discoveryOutDir,
+ eventSlugs: options.eventSlugs,
+ manifestLogPath: options.runLogPath,
+ deadlineTsMs: options.deadlineTsMs
+ })).events
+ : (await discoverWorldCupEvents({
+ outDir: discoveryOutDir,
+ manifestLogPath: options.runLogPath,
+ openOnly: true
+ })).matchEvents;
+ return selectSupportedAssetRecords(events, options.eventSlugs, options.maxAssets);
+}
+
+const REAL_BOOK_EVENT_TYPES = new Set(["book", "price_change", "best_bid_ask"]);
+
+export function isInScopeRealBookItem(item: AnyRecord, subscribedAssets: ReadonlySet): boolean {
+ const eventType = String(item.event_type ?? "");
+ if (!REAL_BOOK_EVENT_TYPES.has(eventType)) return false;
+ const assets = itemAssetIds(item);
+ return assets.length > 0 && assets.some((assetId) => subscribedAssets.has(assetId));
}
function subscribe(socket: WebSocket, assetIds: string[], operation?: "subscribe"): void {
@@ -209,6 +284,7 @@ async function connectionCycle(options: {
let forcedTimer: ReturnType | undefined;
let heartbeatTimer: ReturnType | undefined;
let discoveryTimer: ReturnType | undefined;
+ let discoveryTask: Promise | undefined;
let deadlineTimer: ReturnType | undefined;
await new Promise((resolve) => {
@@ -230,7 +306,10 @@ async function connectionCycle(options: {
discoveryTimer = setInterval(() => {
if (discoveryBusy || options.stopState.stopped || Date.now() >= options.deadline) return;
discoveryBusy = true;
- void discoverAssets(options)
+ discoveryTask = discoverAssets({
+ ...options,
+ deadlineTsMs: Math.min(options.deadline, Date.now() + 60_000)
+ })
.then(async (records) => {
const additions: string[] = [];
for (const record of records) {
@@ -295,7 +374,16 @@ async function connectionCycle(options: {
}
}
const eventTypes = items.map((item) => String(item.event_type ?? "unknown"));
- if (items.length === 0) eventTypes.push(rawPayload.toLocaleLowerCase() === "pong" ? "heartbeat_pong" : "unparsed");
+ if (items.length === 0) {
+ const heartbeat = rawPayload.toLocaleLowerCase() === "pong";
+ eventTypes.push(heartbeat ? "heartbeat_pong" : "unparsed");
+ if (heartbeat) options.stats.transportHeartbeats += 1;
+ }
+ const inScopeBookItems = items.filter((item) =>
+ isInScopeRealBookItem(item, options.stats.subscribedAssets)
+ );
+ options.stats.inScopeBookEvents += inScopeBookItems.length;
+ options.stats.inScopeBookSnapshots += inScopeBookItems.filter((item) => item.event_type === "book").length;
const assetIds = [...new Set(items.flatMap(itemAssetIds))];
for (const eventType of eventTypes) increment(options.stats.eventTypes, eventType);
options.stats.parsedItems += items.length;
@@ -333,8 +421,10 @@ async function connectionCycle(options: {
finish();
});
deadlineTimer = setTimeout(() => {
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
+ if (socket.readyState === WebSocket.OPEN) {
socket.close(1000, "capture deadline");
+ } else if (socket.readyState === WebSocket.CONNECTING) {
+ socket.terminate();
} else finish();
}, Math.max(1, options.deadline - Date.now()));
});
@@ -343,6 +433,7 @@ async function connectionCycle(options: {
if (heartbeatTimer) clearInterval(heartbeatTimer);
if (discoveryTimer) clearInterval(discoveryTimer);
if (deadlineTimer) clearTimeout(deadlineTimer);
+ if (discoveryTask) await discoveryTask;
await writeQueue;
}
@@ -377,9 +468,20 @@ async function runCapture(options: {
function spawnTxlineCapture(options: {
network: string;
fixtureId: string;
- durationMinutes: number;
runLabel: string;
+ durationMinutes?: number;
+ captureWindow?: AbsoluteCaptureWindow;
}): ChildProcess {
+ const timingArgs = options.captureWindow
+ ? [
+ "--capture-start-utc",
+ options.captureWindow.startUtc,
+ "--capture-end-utc",
+ options.captureWindow.endUtc,
+ "--max-startup-skew-seconds",
+ String(options.captureWindow.maxStartupSkewSeconds)
+ ]
+ : ["--duration-minutes", String(options.durationMinutes)];
return spawn(
"pnpm",
[
@@ -390,8 +492,7 @@ function spawnTxlineCapture(options: {
options.network,
"--fixture-id",
options.fixtureId,
- "--duration-minutes",
- String(options.durationMinutes),
+ ...timingArgs,
"--run-label",
options.runLabel
],
@@ -399,6 +500,62 @@ function spawnTxlineCapture(options: {
);
}
+export type PairedChildExit = {
+ code: number | null;
+ signal: NodeJS.Signals | null;
+ atMs: number;
+ spawnError?: string;
+};
+
+export const PAIRED_CHILD_DEADLINE_GRACE_MS = 15_000;
+
+export function pairedChildExitFailure(options: {
+ exit: PairedChildExit;
+ deadline: number;
+ nearDeadlineGraceMs?: number;
+}): string | undefined {
+ if (options.exit.spawnError) return `TXLine capture failed to spawn: ${options.exit.spawnError}`;
+ if (options.exit.code !== 0) {
+ const detail = options.exit.signal ? `signal ${options.exit.signal}` : `code ${String(options.exit.code)}`;
+ return `TXLine capture exited with ${detail}`;
+ }
+ const remainingMs = options.deadline - options.exit.atMs;
+ const graceMs = options.nearDeadlineGraceMs ?? PAIRED_CHILD_DEADLINE_GRACE_MS;
+ if (remainingMs > graceMs) {
+ return `TXLine capture exited ${remainingMs}ms before the shared deadline`;
+ }
+ return undefined;
+}
+
+function waitForChildExit(child: ChildProcess): Promise {
+ if (child.exitCode !== null || child.signalCode !== null) {
+ return Promise.resolve({ code: child.exitCode, signal: child.signalCode, atMs: Date.now() });
+ }
+ return new Promise((resolve) => {
+ let settled = false;
+ const finish = (exit: PairedChildExit) => {
+ if (settled) return;
+ settled = true;
+ child.off("error", onError);
+ child.off("exit", onExit);
+ resolve(exit);
+ };
+ const onError = (error: Error) => finish({
+ code: child.exitCode,
+ signal: child.signalCode,
+ atMs: Date.now(),
+ spawnError: error.message
+ });
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => finish({
+ code,
+ signal,
+ atMs: Date.now()
+ });
+ child.once("error", onError);
+ child.once("exit", onExit);
+ });
+}
+
function fixtureStartMs(fixture: TxFixture): number {
if (typeof fixture.StartTime === "number") return fixture.StartTime;
const numeric = Number(fixture.StartTime);
@@ -445,14 +602,21 @@ async function writeLiveReport(options: {
startedAt: string;
endedAt: string;
stats: CaptureStats;
+ status: "completed" | "failed";
+ error?: string;
+ eventSlugs: Set;
+ pairedChildExit?: PairedChildExit;
nextFixture?: TxFixture;
}): Promise {
const nextTeams = options.nextFixture
? `${options.nextFixture.Participant1 ?? ""}-${options.nextFixture.Participant2 ?? ""}`.toLocaleLowerCase().replace(/[^a-z0-9]+/g, "-")
: "next-world-cup-match";
const nextDate = options.nextFixture ? new Date(fixtureStartMs(options.nextFixture)).toISOString().slice(0, 10) : "date";
+ const exactSlugArgument = options.eventSlugs.size > 0
+ ? ` --event-slugs ${[...options.eventSlugs].join(",")}`
+ : "";
const fullCommand = options.nextFixture
- ? `cd phase0 && pnpm capture:paired -- --network mainnet --txline-fixture-id ${options.nextFixture.FixtureId} --duration-minutes 300 --run-label paired-${nextTeams}-${nextDate}`
+ ? `cd phase0 && pnpm capture:paired -- --network mainnet --txline-fixture-id ${options.nextFixture.FixtureId} --duration-minutes 300 --run-label paired-${nextTeams}-${nextDate}${exactSlugArgument}`
: "No exact TXLine↔Polymarket candidate is currently available; rerun discovery and human-confirm a fixture before a paired capture.";
const expectedEventTypes = [
"book",
@@ -478,8 +642,11 @@ async function writeLiveReport(options: {
"## Smoke capture",
"",
`Run label: ${options.runLabel}`,
+ `Terminal status: ${options.status}`,
+ ...(options.error ? [`Failure: ${options.error}`] : []),
`Started: ${options.startedAt}`,
`Ended: ${options.endedAt}`,
+ `Paired TXLine exit: ${options.pairedChildExit ? JSON.stringify(options.pairedChildExit) : "not paired / not observed"}`,
`Raw output: ${options.outDir}`,
`Raw socket messages: ${options.stats.messages}`,
`Parsed event items: ${options.stats.parsedItems}`,
@@ -512,7 +679,9 @@ async function writeLiveReport(options: {
options.stats.forcedReconnects > 0
? `The smoke run intentionally closed one socket and observed ${options.stats.reconnects} successful reconnect/resubscription(s). Exact connect, close, backoff, and resubscription records are in \`reconnects.ndjson\`.`
: "No reconnect was forced. Any natural disconnect and resubscription is recorded in `reconnects.ndjson`.",
- "Rolling Gamma discovery remains enabled during capture and dynamically subscribes newly listed in-scope Match Result/full-time totals asset IDs.",
+ options.eventSlugs.size > 0
+ ? `Rolling Gamma refresh was bounded to these exact event slugs; full World Cup pagination was disabled: ${[...options.eventSlugs].join(", ")}.`
+ : "Rolling Gamma discovery remained enabled during capture and dynamically subscribed newly listed in-scope Match Result/full-time totals asset IDs.",
"",
"## Exact next full-match paired command",
"",
@@ -525,7 +694,30 @@ async function writeLiveReport(options: {
await writeAtomicText(options.reportPath, lines.join("\n"));
}
-async function main(): Promise {
+function captureStatsSnapshot(stats: CaptureStats): Record {
+ return {
+ messages: stats.messages,
+ parsedItems: stats.parsedItems,
+ parseErrors: stats.parseErrors,
+ transportHeartbeats: stats.transportHeartbeats,
+ inScopeBookEvents: stats.inScopeBookEvents,
+ inScopeBookSnapshots: stats.inScopeBookSnapshots,
+ discoveredAssets: stats.discoveredAssets.size,
+ subscribedAssets: stats.subscribedAssets.size,
+ connects: stats.connects,
+ opens: stats.opens,
+ disconnects: stats.disconnects,
+ reconnects: stats.reconnects,
+ forcedReconnects: stats.forcedReconnects,
+ eventTypes: Object.fromEntries(stats.eventTypes)
+ };
+}
+
+function errorMessage(error: unknown): string {
+ return error instanceof Error ? error.stack ?? error.message : String(error);
+}
+
+export async function main(): Promise {
const args = parseArgs();
const runLabel = (stringArg(args, "run-label", `polymarket-ws-smoke-${timestampSlug()}`) ?? timestampSlug())
.replace(/[^a-zA-Z0-9._-]+/g, "-");
@@ -534,6 +726,11 @@ async function main(): Promise {
const maxAssets = Math.max(1, Math.floor(numberArg(args, "max-assets", 500)));
const discoveryIntervalMs = Math.max(15_000, numberArg(args, "discovery-interval-seconds", 60) * 1000);
const forceReconnectAfterMs = Math.max(0, numberArg(args, "force-reconnect-after-seconds", 0) * 1000);
+ const captureWindow = parseAbsoluteCaptureWindow({
+ startUtc: stringArg(args, "capture-start-utc"),
+ endUtc: stringArg(args, "capture-end-utc"),
+ maxStartupSkewSeconds: numberArg(args, "max-startup-skew-seconds", 120)
+ });
const eventSlugs = new Set((stringArg(args, "event-slugs", "") ?? "").split(",").map((value) => value.trim()).filter(Boolean));
const explicitAssetIds = (stringArg(args, "asset-ids", "") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
const paired = boolArg(args.paired);
@@ -542,13 +739,10 @@ async function main(): Promise {
const outDir = join(SAMPLES_DIR, "polymarket-live", runLabel);
const runLogPath = join(outDir, "reconnects.ndjson");
+ const terminalManifestPath = join(outDir, "capture-manifest.json");
await ensureDir(outDir);
- const initialRecords = await discoverAssets({ outDir, runLogPath, eventSlugs, maxAssets });
- const discoveredAssets = new Map(initialRecords.map((record) => [record.assetId, record]));
- const subscribedAssets = new Set([...explicitAssetIds, ...initialRecords.map((record) => record.assetId)].slice(0, maxAssets));
- if (subscribedAssets.size === 0) throw new Error("No active in-scope asset IDs were discovered; pass --asset-ids for an explicit smoke test");
- await writeAtomicJson(join(outDir, "subscriptions.json"), [...discoveredAssets.values()]);
-
+ const discoveredAssets = new Map();
+ const subscribedAssets = new Set();
const stats: CaptureStats = {
messages: 0,
parsedItems: 0,
@@ -559,42 +753,128 @@ async function main(): Promise {
reconnects: 0,
forcedReconnects: 0,
parseErrors: 0,
+ transportHeartbeats: 0,
+ inScopeBookEvents: 0,
+ inScopeBookSnapshots: 0,
discoveredAssets,
subscribedAssets
};
const startedAt = new Date().toISOString();
- const deadline = Date.now() + durationSeconds * 1000;
+ let captureStartedAt: string | null = null;
+ let deadline = captureWindow?.endTsMs ?? 0;
const stopState: { stopped: boolean; socket?: WebSocket } = { stopped: false };
let txlineChild: ChildProcess | undefined;
+ let txlineExitPromise: Promise | undefined;
+ let pairedChildExit: PairedChildExit | undefined;
+ let pairedChildError: Error | undefined;
+ let interruptedSignal: "SIGINT" | "SIGTERM" | undefined;
const stop = () => {
stopState.stopped = true;
- if (stopState.socket?.readyState === WebSocket.OPEN || stopState.socket?.readyState === WebSocket.CONNECTING) {
+ if (stopState.socket?.readyState === WebSocket.OPEN) {
stopState.socket.close(1000, "process signal");
+ } else if (stopState.socket?.readyState === WebSocket.CONNECTING) {
+ stopState.socket.terminate();
}
- if (txlineChild && txlineChild.exitCode === null) txlineChild.kill("SIGTERM");
+ if (txlineChild && txlineChild.exitCode === null && txlineChild.signalCode === null) txlineChild.kill("SIGTERM");
+ };
+ const onSigint = () => {
+ interruptedSignal = "SIGINT";
+ stop();
+ };
+ const onSigterm = () => {
+ interruptedSignal = "SIGTERM";
+ stop();
};
- process.once("SIGINT", stop);
- process.once("SIGTERM", stop);
+ process.once("SIGINT", onSigint);
+ process.once("SIGTERM", onSigterm);
- await logManifest({
- type: "polymarket-ws-run-start",
- endpoint: POLYMARKET_WS_URL,
+ const writeRunManifest = async (
+ status: "running" | "completed" | "failed",
+ endedAt?: string,
+ error?: string
+ ) => writeAtomicJson(terminalManifestPath, {
+ schemaVersion: 2,
runId: runLabel,
- rows: subscribedAssets.size,
- path: outDir,
+ status,
+ startedAt,
+ captureStartedAt,
+ endedAt: endedAt ?? null,
+ deadlineAt: deadline > 0 ? new Date(deadline).toISOString() : null,
+ captureWindow: captureWindow === null ? null : {
+ startUtc: captureWindow.startUtc,
+ endUtc: captureWindow.endUtc,
+ maxStartupSkewSeconds: captureWindow.maxStartupSkewSeconds
+ },
+ endpoint: POLYMARKET_WS_URL,
paired,
- txlineFixtureId: txlineFixtureId || undefined
+ txlineFixtureId: txlineFixtureId || null,
+ exactEventSlugs: [...eventSlugs],
+ fullWorldCupDiscovery: eventSlugs.size === 0,
+ stats: captureStatsSnapshot(stats),
+ pairedChildExit: pairedChildExit ?? null,
+ error: error ?? null
});
- if (paired) {
- txlineChild = spawnTxlineCapture({
- network: stringArg(args, "network", "mainnet") ?? "mainnet",
- fixtureId: txlineFixtureId,
- durationMinutes: durationSeconds / 60,
- runLabel
- });
- }
- let captureError: unknown;
+
+ await writeRunManifest("running");
try {
+ await logManifest({
+ type: "polymarket-ws-run-start",
+ endpoint: POLYMARKET_WS_URL,
+ runId: runLabel,
+ rows: 0,
+ path: outDir,
+ paired,
+ txlineFixtureId: txlineFixtureId || undefined,
+ exactEventSlugs: [...eventSlugs]
+ });
+
+ const initialRecords = await discoverAssets({
+ outDir,
+ runLogPath,
+ eventSlugs,
+ maxAssets,
+ ...(captureWindow ? {
+ deadlineTsMs: captureWindow.startTsMs + captureWindow.maxStartupSkewSeconds * 1_000
+ } : {})
+ });
+ for (const record of initialRecords) discoveredAssets.set(record.assetId, record);
+ for (const assetId of [...explicitAssetIds, ...initialRecords.map((record) => record.assetId)].slice(0, maxAssets)) {
+ subscribedAssets.add(assetId);
+ }
+ if (subscribedAssets.size === 0) {
+ throw new Error("No active in-scope asset IDs were discovered; pass --asset-ids for an explicit smoke test");
+ }
+ await writeAtomicJson(join(outDir, "subscriptions.json"), [...discoveredAssets.values()]);
+ if (interruptedSignal) throw new Error(`Capture interrupted by ${interruptedSignal}`);
+ if (captureWindow) {
+ const waitMs = captureWindow.startTsMs - Date.now();
+ if (waitMs > 0) await sleep(waitMs);
+ const startupFailure = captureStartupFailure(captureWindow, Date.now());
+ if (startupFailure) throw new Error(startupFailure);
+ deadline = captureWindow.endTsMs;
+ } else {
+ deadline = Date.now() + durationSeconds * 1_000;
+ }
+ captureStartedAt = new Date().toISOString();
+ await writeRunManifest("running");
+
+ if (paired) {
+ txlineChild = spawnTxlineCapture({
+ network: stringArg(args, "network", "mainnet") ?? "mainnet",
+ fixtureId: txlineFixtureId,
+ runLabel,
+ ...(captureWindow ? { captureWindow } : { durationMinutes: durationSeconds / 60 })
+ });
+ txlineExitPromise = waitForChildExit(txlineChild);
+ void txlineExitPromise.then((exit) => {
+ pairedChildExit = exit;
+ const failure = pairedChildExitFailure({ exit, deadline });
+ if (!failure) return;
+ pairedChildError = new Error(failure);
+ stop();
+ });
+ }
+
await runCapture({
deadline,
outDir,
@@ -606,42 +886,88 @@ async function main(): Promise {
forceReconnectAfterMs,
stopState
});
+ if (txlineExitPromise) pairedChildExit = await txlineExitPromise;
+ if (pairedChildError) throw pairedChildError;
+ if (interruptedSignal) throw new Error(`Capture interrupted by ${interruptedSignal}`);
+
+ const endedAt = new Date().toISOString();
+ const nextFixture = await nextPairedFixture([...discoveredAssets.values()]);
+ const reportOptions = {
+ outDir,
+ runLabel,
+ startedAt,
+ endedAt,
+ stats,
+ status: "completed" as const,
+ eventSlugs,
+ pairedChildExit,
+ nextFixture
+ };
+ await writeLiveReport({ ...reportOptions, reportPath: join(outDir, "REPORT.md") });
+ await writeLiveReport({ ...reportOptions, reportPath: join(SAMPLES_DIR, "POLYMARKET-LIVE.md") });
+ await writeRunManifest("completed", endedAt);
+ await logManifest({
+ type: "polymarket-ws-run-end",
+ status: "completed",
+ endpoint: POLYMARKET_WS_URL,
+ runId: runLabel,
+ rows: stats.messages,
+ path: outDir,
+ eventTypes: Object.fromEntries(stats.eventTypes),
+ reconnects: stats.reconnects,
+ pairedChildExit: pairedChildExit ?? null
+ });
+ console.log(`Polymarket WebSocket capture complete: ${outDir}`);
} catch (error) {
- captureError = error;
stop();
+ if (txlineExitPromise) pairedChildExit = await txlineExitPromise;
+ const endedAt = new Date().toISOString();
+ const failure = errorMessage(error);
+ const nextFixture = await nextPairedFixture([...discoveredAssets.values()]).catch(() => undefined);
+ const reportOptions = {
+ outDir,
+ runLabel,
+ startedAt,
+ endedAt,
+ stats,
+ status: "failed" as const,
+ error: failure,
+ eventSlugs,
+ pairedChildExit,
+ nextFixture
+ };
+ let reportError = "";
+ try {
+ await writeLiveReport({ ...reportOptions, reportPath: join(outDir, "REPORT.md") });
+ await writeLiveReport({ ...reportOptions, reportPath: join(SAMPLES_DIR, "POLYMARKET-LIVE.md") });
+ } catch (writeError) {
+ reportError = errorMessage(writeError);
+ }
+ const terminalError = reportError ? `${failure}\nReport write failed: ${reportError}` : failure;
+ await writeRunManifest("failed", endedAt, terminalError);
+ await logManifest({
+ type: "polymarket-ws-run-end",
+ status: "failed",
+ endpoint: POLYMARKET_WS_URL,
+ runId: runLabel,
+ rows: stats.messages,
+ path: outDir,
+ eventTypes: Object.fromEntries(stats.eventTypes),
+ reconnects: stats.reconnects,
+ pairedChildExit: pairedChildExit ?? null,
+ error: terminalError
+ }).catch(() => undefined);
+ throw error;
+ } finally {
+ process.off("SIGINT", onSigint);
+ process.off("SIGTERM", onSigterm);
}
- if (txlineChild) {
- const exitCode = await new Promise((resolve) => {
- if (txlineChild!.exitCode !== null) resolve(txlineChild!.exitCode);
- else txlineChild!.once("exit", resolve);
- });
- if (exitCode !== 0 && captureError === undefined) captureError = new Error(`TXLine capture exited with code ${exitCode}`);
- }
- const endedAt = new Date().toISOString();
- const nextFixture = await nextPairedFixture([...discoveredAssets.values()]);
- await writeLiveReport({
- reportPath: join(SAMPLES_DIR, "POLYMARKET-LIVE.md"),
- outDir,
- runLabel,
- startedAt,
- endedAt,
- stats,
- nextFixture
- });
- await logManifest({
- type: "polymarket-ws-run-end",
- endpoint: POLYMARKET_WS_URL,
- runId: runLabel,
- rows: stats.messages,
- path: outDir,
- eventTypes: Object.fromEntries(stats.eventTypes),
- reconnects: stats.reconnects
- });
- if (captureError) throw captureError;
- console.log(`Polymarket WebSocket capture complete: ${outDir}`);
}
-main().catch((error: unknown) => {
- console.error(error instanceof Error ? error.stack ?? error.message : error);
- process.exit(1);
-});
+const entryPoint = process.argv[1];
+if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
+ main().catch((error: unknown) => {
+ console.error(errorMessage(error));
+ process.exit(1);
+ });
+}
diff --git a/phase0/src/capture-sse.ts b/phase0/src/capture-sse.ts
index d6120ff..844d03d 100644
--- a/phase0/src/capture-sse.ts
+++ b/phase0/src/capture-sse.ts
@@ -1,5 +1,9 @@
+import { once } from "node:events";
import { createWriteStream } from "node:fs";
import { join } from "node:path";
+import { pathToFileURL } from "node:url";
+import { captureStartupFailure, parseAbsoluteCaptureWindow } from "./capture-window.js";
+import { writeAtomicJson } from "./polymarket-lib.js";
import {
appendJsonl,
authHeaders,
@@ -42,105 +46,257 @@ function parseSseBlock(block: string): ParsedSse | null {
return message.data || message.event || message.id ? message : null;
}
-async function captureStream(options: {
+export type CaptureStreamOptions = {
network: string;
streamName: "odds" | "scores";
url: string;
headers: Record;
outDir: string;
deadline: number;
-}): Promise {
+ fetchImpl?: typeof fetch;
+ reconnectDelayMs?: number;
+ expectedFixtureId?: string;
+};
+
+export type CaptureStreamSummary = {
+ stream: "odds" | "scores";
+ frames: number;
+ jsonDataFrames: number;
+ exactFixtureDataFrames: number;
+ usableExactFixtureOddsFrames: number;
+ completedExactFixtureScoreFrames: number;
+ firstReceivedAt: string | null;
+ lastReceivedAt: string | null;
+};
+
+/**
+ * TXLine `Pct` is a de-vigged percentage on a 0-100 scale. Allow only a
+ * narrow half-percentage-point rounding envelope around 100 so malformed,
+ * fractional 0-1 values, and partially populated rows cannot certify a run.
+ */
+export const TXLINE_DEVIG_SUM_MIN = 99.5;
+export const TXLINE_DEVIG_SUM_MAX = 100.5;
+
+export function isUsableExactFixtureOddsRecord(record: Record): boolean {
+ if (
+ record.SuperOddsType !== "1X2_PARTICIPANT_RESULT" &&
+ record.SuperOddsType !== "OVERUNDER_PARTICIPANT_GOALS"
+ ) return false;
+ if (
+ !Array.isArray(record.PriceNames) ||
+ !Array.isArray(record.Prices) ||
+ !Array.isArray(record.Pct) ||
+ record.PriceNames.length === 0 ||
+ record.Prices.length !== record.PriceNames.length ||
+ record.Pct.length !== record.PriceNames.length
+ ) return false;
+ if (!record.PriceNames.every((value) => typeof value === "string" && value.trim().length > 0)) {
+ return false;
+ }
+ if (!record.Prices.every((value) => typeof value === "number" && Number.isFinite(value) && value > 0)) {
+ return false;
+ }
+ if (!record.Pct.every((value) => {
+ if (typeof value !== "string" || value.trim().length === 0) return false;
+ const parsed = Number(value);
+ return Number.isFinite(parsed) && parsed >= 0 && parsed <= 100;
+ })) return false;
+ const sum = record.Pct.reduce((total, value) => total + Number(value), 0);
+ return sum >= TXLINE_DEVIG_SUM_MIN && sum <= TXLINE_DEVIG_SUM_MAX;
+}
+
+export function isCompletedExactFixtureScoreRecord(record: Record): boolean {
+ return record.Action === "game_finalised" && record.StatusId === 100;
+}
+
+export async function captureStream(options: CaptureStreamOptions): Promise {
let lastEventId = "";
let reconnect = 0;
+ const fetchImpl = options.fetchImpl ?? fetch;
+ const reconnectDelayMs = options.reconnectDelayMs ?? 2_000;
const raw = createWriteStream(join(options.outDir, `${options.streamName}.raw.sse`), { flags: "a" });
const framesPath = join(options.outDir, `${options.streamName}.frames.ndjson`);
const reconnectPath = join(options.outDir, "reconnects.ndjson");
+ const summary: CaptureStreamSummary = {
+ stream: options.streamName,
+ frames: 0,
+ jsonDataFrames: 0,
+ exactFixtureDataFrames: 0,
+ usableExactFixtureOddsFrames: 0,
+ completedExactFixtureScoreFrames: 0,
+ firstReceivedAt: null,
+ lastReceivedAt: null
+ };
- while (Date.now() < options.deadline) {
- const headers: Record = {
- ...options.headers,
- Accept: "text/event-stream",
- "Cache-Control": "no-cache",
- "Accept-Encoding": "gzip"
- };
- if (lastEventId) headers["Last-Event-ID"] = lastEventId;
-
- await appendJsonl(reconnectPath, {
- at: new Date().toISOString(),
- stream: options.streamName,
- reconnect,
- lastEventId: lastEventId || null,
- action: "connect"
- });
-
- try {
- const response = await fetch(options.url, { headers });
- if (!response.ok || !response.body) {
- const text = await response.text();
- throw new Error(`${options.streamName} stream failed ${response.status}: ${text.slice(0, 300)}`);
- }
+ try {
+ while (Date.now() < options.deadline) {
+ const headers: Record = {
+ ...options.headers,
+ Accept: "text/event-stream",
+ "Cache-Control": "no-cache",
+ "Accept-Encoding": "gzip"
+ };
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
- while (Date.now() < options.deadline) {
- const { value, done } = await reader.read();
- if (done) break;
- const chunk = decoder.decode(value, { stream: true });
- raw.write(chunk);
- buffer += chunk;
- let separator = buffer.match(/\r?\n\r?\n/);
- while (separator?.index !== undefined) {
- const receivedAt = new Date();
- const receivedAtUnixNs = `${BigInt(receivedAt.getTime()) * 1_000_000n}`;
- const receivedAtMonotonicNs = nowNs();
- const rawFrame = buffer.slice(0, separator.index);
- buffer = buffer.slice(separator.index + separator[0].length);
- const parsed = parseSseBlock(rawFrame);
- if (parsed?.id) lastEventId = parsed.id;
- await appendJsonl(framesPath, {
- receivedAt: receivedAt.toISOString(),
- receivedAtUnixNs,
- receivedAtMonotonicNs,
- stream: options.streamName,
- lastEventId: parsed?.id ?? null,
- event: parsed?.event ?? null,
- rawFrame
- });
- separator = buffer.match(/\r?\n\r?\n/);
- }
- }
- reader.releaseLock();
- } catch (error) {
await appendJsonl(reconnectPath, {
at: new Date().toISOString(),
stream: options.streamName,
reconnect,
lastEventId: lastEventId || null,
- action: "disconnect",
- error: error instanceof Error ? error.message : String(error)
+ action: "connect"
});
- await new Promise((resolve) => setTimeout(resolve, 2_000));
+
+ const controller = new AbortController();
+ const deadlineTimer = setTimeout(
+ () => controller.abort(new Error("capture deadline reached")),
+ Math.max(1, options.deadline - Date.now())
+ );
+ let reader: ReadableStreamDefaultReader | undefined;
+ try {
+ const response = await fetchImpl(options.url, { headers, signal: controller.signal });
+ if (!response.ok || !response.body) {
+ const text = await response.text();
+ throw new Error(`${options.streamName} stream failed ${response.status}: ${text.slice(0, 300)}`);
+ }
+
+ reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+ while (Date.now() < options.deadline) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ const chunk = decoder.decode(value, { stream: true });
+ raw.write(chunk);
+ buffer += chunk;
+ let separator = buffer.match(/\r?\n\r?\n/);
+ while (separator?.index !== undefined) {
+ const receivedAt = new Date();
+ const receivedAtUnixNs = `${BigInt(receivedAt.getTime()) * 1_000_000n}`;
+ const receivedAtMonotonicNs = nowNs();
+ const rawFrame = buffer.slice(0, separator.index);
+ buffer = buffer.slice(separator.index + separator[0].length);
+ const parsed = parseSseBlock(rawFrame);
+ if (parsed?.id) lastEventId = parsed.id;
+ summary.frames += 1;
+ summary.firstReceivedAt ??= receivedAt.toISOString();
+ summary.lastReceivedAt = receivedAt.toISOString();
+ if (parsed?.data) {
+ try {
+ const payload = JSON.parse(parsed.data) as unknown;
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
+ summary.jsonDataFrames += 1;
+ const record = payload as Record;
+ const exactFixture = options.expectedFixtureId !== undefined &&
+ String(record.FixtureId ?? "") === options.expectedFixtureId;
+ if (exactFixture) summary.exactFixtureDataFrames += 1;
+ if (
+ exactFixture &&
+ options.streamName === "scores" &&
+ isCompletedExactFixtureScoreRecord(record)
+ ) {
+ summary.completedExactFixtureScoreFrames += 1;
+ }
+ if (
+ exactFixture &&
+ options.streamName === "odds" &&
+ isUsableExactFixtureOddsRecord(record)
+ ) {
+ summary.usableExactFixtureOddsFrames += 1;
+ }
+ }
+ } catch {
+ // Raw evidence remains preserved. Malformed data is not counted as usable evidence.
+ }
+ }
+ await appendJsonl(framesPath, {
+ receivedAt: receivedAt.toISOString(),
+ receivedAtUnixNs,
+ receivedAtMonotonicNs,
+ stream: options.streamName,
+ lastEventId: parsed?.id ?? null,
+ event: parsed?.event ?? null,
+ rawFrame
+ });
+ separator = buffer.match(/\r?\n\r?\n/);
+ }
+ }
+ } catch (error) {
+ const expectedDeadline = controller.signal.aborted && Date.now() >= options.deadline;
+ if (!expectedDeadline) {
+ await appendJsonl(reconnectPath, {
+ at: new Date().toISOString(),
+ stream: options.streamName,
+ reconnect,
+ lastEventId: lastEventId || null,
+ action: "disconnect",
+ error: error instanceof Error ? error.message : String(error)
+ });
+ const remainingMs = Math.max(0, options.deadline - Date.now());
+ await new Promise((resolve) => setTimeout(resolve, Math.min(reconnectDelayMs, remainingMs)));
+ }
+ } finally {
+ clearTimeout(deadlineTimer);
+ if (reader) {
+ await reader.cancel("capture ended").catch(() => undefined);
+ reader.releaseLock();
+ }
+ }
+ reconnect += 1;
}
- reconnect += 1;
+ } finally {
+ raw.end();
+ await once(raw, "finish");
}
-
- raw.end();
+ return summary;
}
-async function main(): Promise {
+export async function main(): Promise {
const args = parseArgs();
const network = getNetwork(args);
const token = await loadToken(network);
const config = NETWORKS[network];
const durationMinutes = numberArg(args, "duration-minutes", 240);
+ const captureWindow = parseAbsoluteCaptureWindow({
+ startUtc: stringArg(args, "capture-start-utc"),
+ endUtc: stringArg(args, "capture-end-utc"),
+ maxStartupSkewSeconds: numberArg(args, "max-startup-skew-seconds", 120)
+ });
const fixtureId = stringArg(args, "fixture-id");
const runId = stringArg(args, "run-label", timestampSlug())!;
const outDir = join(SAMPLES_DIR, "odds-sse", network, runId);
const query = fixtureId ? `?fixtureId=${encodeURIComponent(fixtureId)}` : "";
- const deadline = Date.now() + durationMinutes * 60_000;
+ if (captureWindow) {
+ const waitMs = captureWindow.startTsMs - Date.now();
+ if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
+ const startupFailure = captureStartupFailure(captureWindow, Date.now());
+ if (startupFailure) throw new Error(startupFailure);
+ }
+ const deadline = captureWindow?.endTsMs ?? Date.now() + durationMinutes * 60_000;
+ if (captureWindow && !fixtureId) throw new Error("Absolute TXLine capture requires an exact fixture ID");
await ensureDir(outDir);
+ const terminalManifestPath = join(outDir, "txline-capture-manifest.json");
+ const startedAt = new Date().toISOString();
+ const manifestBase = {
+ schemaVersion: 1,
+ network,
+ runId,
+ fixtureId: fixtureId ?? null,
+ startedAt,
+ deadlineAt: new Date(deadline).toISOString(),
+ captureWindow: captureWindow === null ? null : {
+ startUtc: captureWindow.startUtc,
+ endUtc: captureWindow.endUtc,
+ maxStartupSkewSeconds: captureWindow.maxStartupSkewSeconds
+ }
+ };
+ await writeAtomicJson(terminalManifestPath, {
+ ...manifestBase,
+ status: "running",
+ endedAt: null,
+ streams: null,
+ error: null
+ });
await logManifest({
type: "txline-sse-run-start",
@@ -148,27 +304,53 @@ async function main(): Promise {
endpoint: "/api/odds/stream + /api/scores/stream",
query: fixtureId ? { fixtureId } : {},
runId,
- path: outDir
+ path: outDir,
+ captureWindow: captureWindow === null ? null : {
+ startUtc: captureWindow.startUtc,
+ endUtc: captureWindow.endUtc,
+ maxStartupSkewSeconds: captureWindow.maxStartupSkewSeconds
+ }
});
- await Promise.all([
- captureStream({
- network,
- streamName: "odds",
- url: `${config.apiOrigin}/api/odds/stream${query}`,
- headers: authHeaders(token),
- outDir,
- deadline
- }),
- captureStream({
- network,
- streamName: "scores",
- url: `${config.apiOrigin}/api/scores/stream${query}`,
- headers: authHeaders(token),
- outDir,
- deadline
- })
- ]);
+ let streams: CaptureStreamSummary[];
+ try {
+ streams = await Promise.all([
+ captureStream({
+ network,
+ streamName: "odds",
+ url: `${config.apiOrigin}/api/odds/stream${query}`,
+ headers: authHeaders(token),
+ outDir,
+ deadline,
+ expectedFixtureId: fixtureId
+ }),
+ captureStream({
+ network,
+ streamName: "scores",
+ url: `${config.apiOrigin}/api/scores/stream${query}`,
+ headers: authHeaders(token),
+ outDir,
+ deadline,
+ expectedFixtureId: fixtureId
+ })
+ ]);
+ await writeAtomicJson(terminalManifestPath, {
+ ...manifestBase,
+ status: "completed",
+ endedAt: new Date().toISOString(),
+ streams,
+ error: null
+ });
+ } catch (error) {
+ await writeAtomicJson(terminalManifestPath, {
+ ...manifestBase,
+ status: "failed",
+ endedAt: new Date().toISOString(),
+ streams: null,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
await logManifest({
type: "txline-sse-run-end",
@@ -181,7 +363,10 @@ async function main(): Promise {
console.log(`SSE capture complete: ${outDir}`);
}
-main().catch((error: unknown) => {
- console.error(error instanceof Error ? error.message : error);
- process.exit(1);
-});
+const entryPoint = process.argv[1];
+if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
+ main().catch((error: unknown) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+ });
+}
diff --git a/phase0/src/capture-window.ts b/phase0/src/capture-window.ts
new file mode 100644
index 0000000..9ad515c
--- /dev/null
+++ b/phase0/src/capture-window.ts
@@ -0,0 +1,42 @@
+export type AbsoluteCaptureWindow = {
+ startUtc: string;
+ endUtc: string;
+ startTsMs: number;
+ endTsMs: number;
+ maxStartupSkewSeconds: number;
+};
+
+export function parseAbsoluteCaptureWindow(options: {
+ startUtc?: string;
+ endUtc?: string;
+ maxStartupSkewSeconds: number;
+}): AbsoluteCaptureWindow | null {
+ if (!options.startUtc && !options.endUtc) return null;
+ if (!options.startUtc || !options.endUtc) {
+ throw new Error("Absolute capture window requires both --capture-start-utc and --capture-end-utc");
+ }
+ const startTsMs = Date.parse(options.startUtc);
+ const endTsMs = Date.parse(options.endUtc);
+ if (!Number.isFinite(startTsMs) || !Number.isFinite(endTsMs) || endTsMs <= startTsMs) {
+ throw new Error("Absolute capture window is invalid");
+ }
+ if (!Number.isInteger(options.maxStartupSkewSeconds) || options.maxStartupSkewSeconds <= 0) {
+ throw new Error("Absolute capture window requires a positive integer startup skew");
+ }
+ return {
+ startUtc: new Date(startTsMs).toISOString(),
+ endUtc: new Date(endTsMs).toISOString(),
+ startTsMs,
+ endTsMs,
+ maxStartupSkewSeconds: options.maxStartupSkewSeconds
+ };
+}
+
+export function captureStartupFailure(window: AbsoluteCaptureWindow, nowTsMs: number): string | undefined {
+ if (nowTsMs >= window.endTsMs) return "Absolute capture window ended before streams started";
+ const skewMs = nowTsMs - window.startTsMs;
+ if (skewMs > window.maxStartupSkewSeconds * 1_000) {
+ return `Capture streams started ${skewMs}ms after the reviewed window opened`;
+ }
+ return undefined;
+}
diff --git a/phase0/src/polymarket-lib.ts b/phase0/src/polymarket-lib.ts
index a041fa7..90a45b1 100644
--- a/phase0/src/polymarket-lib.ts
+++ b/phase0/src/polymarket-lib.ts
@@ -98,6 +98,13 @@ export type FetchResult = {
attempts: number;
};
+export type ExactSlugDiscoveryResult = {
+ discoveryId: string;
+ events: GammaEvent[];
+};
+
+export type GammaFetch = (url: string) => Promise;
+
export function sleep(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -113,16 +120,35 @@ function retryAfterMs(value: string | null): number | undefined {
export async function fetchWithRetry(
url: string,
init: RequestInit = {},
- options: { attempts?: number; baseDelayMs?: number; maxDelayMs?: number } = {}
+ options: {
+ attempts?: number;
+ baseDelayMs?: number;
+ maxDelayMs?: number;
+ attemptTimeoutMs?: number;
+ deadlineTsMs?: number;
+ } = {}
): Promise {
const attempts = Math.max(1, options.attempts ?? 6);
const baseDelayMs = Math.max(50, options.baseDelayMs ?? 500);
const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 20_000);
+ const attemptTimeoutMs = Math.max(100, options.attemptTimeoutMs ?? 15_000);
let lastError = "";
for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ const remainingMs = options.deadlineTsMs === undefined ? Number.POSITIVE_INFINITY : options.deadlineTsMs - Date.now();
+ if (remainingMs <= 0) {
+ lastError = "request deadline exhausted";
+ break;
+ }
+ const controller = new AbortController();
+ const timeout = setTimeout(
+ () => controller.abort(new Error("request attempt timed out")),
+ Math.max(1, Math.min(attemptTimeoutMs, remainingMs))
+ );
+ const parentSignal = init.signal;
+ const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
try {
- const response = await fetch(url, init);
+ const response = await fetch(url, { ...init, signal });
const text = await response.text();
if (response.ok || (response.status < 500 && response.status !== 429)) {
return {
@@ -145,14 +171,22 @@ export async function fetchWithRetry(
}
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const jitter = Math.floor(Math.random() * Math.max(1, exponential * 0.35));
- await sleep(retryAfterMs(response.headers.get("retry-after")) ?? exponential + jitter);
+ const requestedDelay = retryAfterMs(response.headers.get("retry-after")) ?? exponential + jitter;
+ const retryBudget = options.deadlineTsMs === undefined ? requestedDelay : Math.max(0, options.deadlineTsMs - Date.now());
+ if (retryBudget <= 0) break;
+ await sleep(Math.min(requestedDelay, retryBudget));
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
if (attempt < attempts) {
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const jitter = Math.floor(Math.random() * Math.max(1, exponential * 0.35));
- await sleep(exponential + jitter);
+ const requestedDelay = exponential + jitter;
+ const retryBudget = options.deadlineTsMs === undefined ? requestedDelay : Math.max(0, options.deadlineTsMs - Date.now());
+ if (retryBudget <= 0) break;
+ await sleep(Math.min(requestedDelay, retryBudget));
}
+ } finally {
+ clearTimeout(timeout);
}
}
return { ok: false, status: 0, text: lastError, contentType: "", attempts };
@@ -174,6 +208,111 @@ export async function writeAtomicJson(path: string, value: unknown): Promise;
+ manifestLogPath?: string;
+ fetcher?: GammaFetch;
+ deadlineTsMs?: number;
+}): Promise {
+ const eventSlugs = [...new Set([...options.eventSlugs].map((slug) => slug.trim()).filter(Boolean))];
+ if (eventSlugs.length === 0) throw new Error("Exact-slug discovery requires at least one event slug");
+
+ const discoveryId = timestampSlug();
+ const rawDir = join(options.outDir, "exact-slug", discoveryId);
+ const fetcher = options.fetcher ?? ((url: string) => fetchWithRetry(url, {}, {
+ deadlineTsMs: options.deadlineTsMs,
+ attemptTimeoutMs: 15_000
+ }));
+ const events: GammaEvent[] = [];
+ await ensureDir(rawDir);
+
+ for (const [index, requestedSlug] of eventSlugs.entries()) {
+ const prefix = `${String(index).padStart(3, "0")}-${safePathSegment(requestedSlug)}`;
+ const url = `${GAMMA_ORIGIN}/events/slug/${encodeURIComponent(requestedSlug)}`;
+ let response: FetchResult;
+ try {
+ response = await fetcher(url);
+ } catch (fetchError) {
+ response = {
+ ok: false,
+ status: 0,
+ text: fetchError instanceof Error ? fetchError.stack ?? fetchError.message : String(fetchError),
+ contentType: "",
+ attempts: 1
+ };
+ }
+ const rawPath = join(rawDir, `${prefix}.response.json`);
+ const requestManifestPath = join(rawDir, `${prefix}.manifest.json`);
+ const capturedAt = new Date().toISOString();
+ await writeAtomicText(rawPath, response.text.endsWith("\n") ? response.text : `${response.text}\n`);
+
+ let event: GammaEvent | undefined;
+ let returnedSlug = "";
+ let error = "";
+ if (!response.ok) {
+ error = `Gamma exact event ${requestedSlug} failed ${response.status}`;
+ } else {
+ try {
+ const parsed = JSON.parse(response.text) as unknown;
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+ error = `Gamma exact event ${requestedSlug} returned a non-object JSON body`;
+ } else {
+ event = parsed as GammaEvent;
+ returnedSlug = typeof event.slug === "string" ? event.slug : "";
+ if (returnedSlug !== requestedSlug) {
+ error = `Gamma exact event slug mismatch: requested ${requestedSlug}, received ${returnedSlug || ""}`;
+ }
+ }
+ } catch (parseError) {
+ error = `Gamma exact event ${requestedSlug} returned invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`;
+ }
+ }
+
+ const manifestEntry = {
+ at: capturedAt,
+ action: "gamma-event-slug",
+ discoveryId,
+ requestedSlug,
+ returnedSlug: returnedSlug || null,
+ status: response.status,
+ attempts: response.attempts,
+ contentType: response.contentType,
+ path: rawPath,
+ url,
+ outcome: error ? "failed" : "captured",
+ error: error || null
+ };
+ await writeAtomicJson(requestManifestPath, manifestEntry);
+ if (options.manifestLogPath) await appendJsonl(options.manifestLogPath, manifestEntry);
+ if (error || !event) throw new Error(error || `Gamma exact event ${requestedSlug} was not captured`);
+
+ events.push(event);
+ const eventId = String(event.id ?? "event");
+ await writeAtomicJson(join(options.outDir, "events", `${safePathSegment(eventId)}-${safePathSegment(requestedSlug)}.json`), event);
+ }
+
+ await writeAtomicJson(join(options.outDir, "exact-slug-events.json"), events);
+ await writeAtomicJson(join(options.outDir, "exact-slug-discovery-metadata.json"), {
+ capturedAt: new Date().toISOString(),
+ discoveryId,
+ endpoint: "Gamma /events/slug/{slug}",
+ requestedSlugs: eventSlugs,
+ events: events.length,
+ relevantMarkets: relevantMarkets(events).length
+ });
+ return { discoveryId, events };
+}
+
export function parseStringArray(value: unknown): string[] {
if (Array.isArray(value)) return value.map(String);
if (typeof value !== "string" || !value.trim()) return [];
diff --git a/phase0/test/capture-sse.test.ts b/phase0/test/capture-sse.test.ts
new file mode 100644
index 0000000..13ae44a
--- /dev/null
+++ b/phase0/test/capture-sse.test.ts
@@ -0,0 +1,111 @@
+import assert from "node:assert/strict";
+import { once } from "node:events";
+import { mkdtemp, readFile, rm } from "node:fs/promises";
+import { createServer } from "node:http";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+import {
+ captureStream,
+ isCompletedExactFixtureScoreRecord,
+ isUsableExactFixtureOddsRecord
+} from "../src/capture-sse.js";
+import { fetchWithRetry } from "../src/polymarket-lib.js";
+
+test("captureStream aborts an open SSE response at the deadline", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "samaritan-sse-deadline-"));
+ const server = createServer((_request, response) => {
+ response.writeHead(200, {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache"
+ });
+ response.write('id: event-1\ndata: {"FixtureId":123,"Ts":1,"SuperOddsType":"1X2_PARTICIPANT_RESULT","PriceNames":["1","X","2"],"Prices":[2500,3333,3333],"Pct":["40","30","30"]}\n\n');
+ });
+ server.listen(0, "127.0.0.1");
+ await once(server, "listening");
+ const address = server.address();
+ if (!address || typeof address === "string") throw new Error("Expected a TCP test server");
+
+ const startedAt = Date.now();
+ try {
+ const summary = await captureStream({
+ network: "mainnet",
+ streamName: "odds",
+ url: `http://127.0.0.1:${address.port}`,
+ headers: {},
+ outDir: directory,
+ deadline: startedAt + 100,
+ reconnectDelayMs: 1,
+ expectedFixtureId: "123"
+ });
+
+ assert.ok(Date.now() - startedAt < 1_000, "capture should not wait for the server to close");
+ const frames = await readFile(join(directory, "odds.frames.ndjson"), "utf8");
+ assert.match(frames, /event-1/);
+ const reconnects = await readFile(join(directory, "reconnects.ndjson"), "utf8");
+ assert.doesNotMatch(reconnects, /"action":"disconnect"/);
+ assert.equal(summary.usableExactFixtureOddsFrames, 1);
+ assert.equal(summary.exactFixtureDataFrames, 1);
+ } finally {
+ server.closeAllConnections();
+ server.close();
+ await once(server, "close");
+ await rm(directory, { recursive: true, force: true });
+ }
+});
+
+test("score completion requires the exact terminal TXLine semantic", () => {
+ assert.equal(isCompletedExactFixtureScoreRecord({ Action: "game_finalised", StatusId: 100 }), true);
+ assert.equal(isCompletedExactFixtureScoreRecord({ Action: "status", StatusId: 5 }), false);
+ assert.equal(isCompletedExactFixtureScoreRecord({ Action: "game_finalised", StatusId: 5 }), false);
+});
+
+test("usable exact-fixture odds require aligned prices and sane 0-100 Pct semantics", () => {
+ const valid: Record = {
+ SuperOddsType: "OVERUNDER_PARTICIPANT_GOALS",
+ PriceNames: ["over", "under"],
+ Prices: [1900, 2100],
+ Pct: ["52.631", "47.369"]
+ };
+ assert.equal(isUsableExactFixtureOddsRecord(valid), true);
+ const invalid: Array> = [
+ { ...valid, Prices: [] },
+ { ...valid, Prices: [1900] },
+ { ...valid, Prices: [1900, 0] },
+ { ...valid, Prices: [1900, Number.NaN] },
+ { ...valid, Pct: ["52.631", ""] },
+ { ...valid, Pct: [52.631, 47.369] },
+ { ...valid, Pct: ["0.52631", "0.47369"] },
+ { ...valid, Pct: ["101", "-1"] },
+ { ...valid, PriceNames: ["over", " "] },
+ { ...valid, SuperOddsType: "ASIANHANDICAP_PARTICIPANT_GOALS" }
+ ];
+ for (const row of invalid) assert.equal(isUsableExactFixtureOddsRecord(row), false);
+});
+
+test("Gamma retry aborts inside an absolute request deadline", async () => {
+ const server = createServer((_request, _response) => {
+ // Intentionally never send headers: the deadline must abort this request.
+ });
+ server.listen(0, "127.0.0.1");
+ await once(server, "listening");
+ const address = server.address();
+ if (!address || typeof address === "string") throw new Error("Expected a TCP test server");
+ const startedAt = Date.now();
+ try {
+ const result = await fetchWithRetry(`http://127.0.0.1:${address.port}`, {}, {
+ attempts: 3,
+ attemptTimeoutMs: 30,
+ baseDelayMs: 5,
+ maxDelayMs: 5,
+ deadlineTsMs: startedAt + 80
+ });
+ assert.equal(result.ok, false);
+ assert.equal(result.status, 0);
+ assert.ok(Date.now() - startedAt < 500, "request must not outlive its absolute deadline");
+ } finally {
+ server.closeAllConnections();
+ server.close();
+ await once(server, "close");
+ }
+});
diff --git a/phase0/test/polymarket-exact-discovery.test.ts b/phase0/test/polymarket-exact-discovery.test.ts
new file mode 100644
index 0000000..807a7fd
--- /dev/null
+++ b/phase0/test/polymarket-exact-discovery.test.ts
@@ -0,0 +1,197 @@
+import assert from "node:assert/strict";
+import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+import {
+ isInScopeRealBookItem,
+ pairedChildExitFailure,
+ PAIRED_CHILD_DEADLINE_GRACE_MS,
+ selectSupportedAssetRecords
+} from "../src/capture-polymarket-ws.js";
+import {
+ captureStartupFailure,
+ parseAbsoluteCaptureWindow
+} from "../src/capture-window.js";
+import {
+ discoverEventsByExactSlugs,
+ GAMMA_ORIGIN,
+ type GammaFetch
+} from "../src/polymarket-lib.js";
+
+test("exact-slug discovery fetches only direct official event endpoints and preserves atomic evidence", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "samaritan-gamma-slugs-"));
+ const runLogPath = join(directory, "run.ndjson");
+ const requested = ["fifwc-eng-arg-2026-07-15", "fifwc-eng-arg-2026-07-15-more-markets"];
+ const urls: string[] = [];
+ const fetcher: GammaFetch = async (url) => {
+ urls.push(url);
+ const slug = decodeURIComponent(url.slice(url.lastIndexOf("/") + 1));
+ return {
+ ok: true,
+ status: 200,
+ text: JSON.stringify({ id: `id-${urls.length}`, slug, markets: [] }),
+ contentType: "application/json",
+ attempts: 1
+ };
+ };
+
+ try {
+ const result = await discoverEventsByExactSlugs({
+ outDir: directory,
+ eventSlugs: requested,
+ manifestLogPath: runLogPath,
+ fetcher
+ });
+
+ assert.deepEqual(urls, requested.map((slug) => `${GAMMA_ORIGIN}/events/slug/${slug}`));
+ assert.ok(urls.every((url) => !url.includes("/events/keyset") && !url.endsWith("/sports")));
+ assert.deepEqual(result.events.map((event) => event.slug), requested);
+
+ const evidenceDir = join(directory, "exact-slug", result.discoveryId);
+ const evidenceFiles = await readdir(evidenceDir);
+ assert.equal(evidenceFiles.filter((name) => name.endsWith(".response.json")).length, 2);
+ assert.equal(evidenceFiles.filter((name) => name.endsWith(".manifest.json")).length, 2);
+ assert.equal(evidenceFiles.filter((name) => name.includes(".tmp-")).length, 0);
+
+ const logEntries = (await readFile(runLogPath, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
+ assert.deepEqual(logEntries.map((entry) => entry.requestedSlug), requested);
+ assert.ok(logEntries.every((entry) => entry.outcome === "captured"));
+ const aggregate = JSON.parse(await readFile(join(directory, "exact-slug-events.json"), "utf8"));
+ assert.deepEqual(aggregate.map((event: { slug: string }) => event.slug), requested);
+ } finally {
+ await rm(directory, { recursive: true, force: true });
+ }
+});
+
+test("exact-slug discovery rejects a mismatched response after preserving raw and failure manifests", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "samaritan-gamma-mismatch-"));
+ const runLogPath = join(directory, "run.ndjson");
+ try {
+ await assert.rejects(
+ discoverEventsByExactSlugs({
+ outDir: directory,
+ eventSlugs: ["expected-slug"],
+ manifestLogPath: runLogPath,
+ fetcher: async () => ({
+ ok: true,
+ status: 200,
+ text: JSON.stringify({ id: "wrong", slug: "different-slug", markets: [] }),
+ contentType: "application/json",
+ attempts: 1
+ })
+ }),
+ /slug mismatch: requested expected-slug, received different-slug/
+ );
+
+ const entry = JSON.parse((await readFile(runLogPath, "utf8")).trim());
+ assert.equal(entry.outcome, "failed");
+ assert.equal(entry.requestedSlug, "expected-slug");
+ assert.equal(entry.returnedSlug, "different-slug");
+ assert.match(await readFile(entry.path, "utf8"), /different-slug/);
+ const evidenceFiles = await readdir(join(directory, "exact-slug", entry.discoveryId));
+ assert.equal(evidenceFiles.filter((name) => name.endsWith(".manifest.json")).length, 1);
+ } finally {
+ await rm(directory, { recursive: true, force: true });
+ }
+});
+
+test("paired child supervision fails early or abnormal exits but allows a clean near-deadline exit", () => {
+ const deadline = 1_000_000;
+ assert.match(
+ pairedChildExitFailure({
+ exit: { code: 0, signal: null, atMs: deadline - PAIRED_CHILD_DEADLINE_GRACE_MS - 1 },
+ deadline
+ }) ?? "",
+ /before the shared deadline/
+ );
+ assert.equal(pairedChildExitFailure({
+ exit: { code: 0, signal: null, atMs: deadline - PAIRED_CHILD_DEADLINE_GRACE_MS },
+ deadline
+ }), undefined);
+ assert.match(pairedChildExitFailure({
+ exit: { code: 1, signal: null, atMs: deadline },
+ deadline
+ }) ?? "", /code 1/);
+ assert.match(pairedChildExitFailure({
+ exit: { code: null, signal: "SIGTERM", atMs: deadline },
+ deadline
+ }) ?? "", /signal SIGTERM/);
+});
+
+test("absolute capture windows cannot be shifted by discovery duration", () => {
+ const window = parseAbsoluteCaptureWindow({
+ startUtc: "2026-07-15T16:00:00.000Z",
+ endUtc: "2026-07-15T22:00:00.000Z",
+ maxStartupSkewSeconds: 120
+ });
+ assert.ok(window);
+ assert.equal(window.endTsMs - window.startTsMs, 6 * 60 * 60_000);
+ assert.equal(captureStartupFailure(window, window.startTsMs + 120_000), undefined);
+ assert.match(captureStartupFailure(window, window.startTsMs + 120_001) ?? "", /reviewed window/);
+ assert.match(captureStartupFailure(window, window.endTsMs) ?? "", /ended before streams started/);
+ assert.throws(() => parseAbsoluteCaptureWindow({
+ startUtc: window.startUtc,
+ maxStartupSkewSeconds: 120
+ }), /requires both/);
+});
+
+test("supported asset selection is deterministic and never truncates required strategy markets", (context) => {
+ const kickoff = "2026-07-15T19:00:00.000Z";
+ context.mock.method(Date, "now", () => Date.parse(kickoff) - 60 * 60_000);
+ const market = (id: string, sportsMarketType: "moneyline" | "totals", line?: number) => ({
+ id,
+ conditionId: `condition-${id}`,
+ sportsMarketType,
+ ...(line === undefined ? {} : { line }),
+ gameStartTime: kickoff,
+ active: true,
+ closed: false,
+ acceptingOrders: true,
+ enableOrderBook: true,
+ question: `${sportsMarketType}-${id}`,
+ clobTokenIds: JSON.stringify([`asset-${id}-b`, `asset-${id}-a`]),
+ outcomes: JSON.stringify(sportsMarketType === "moneyline" ? ["Yes", "No"] : ["Over", "Under"])
+ });
+ const matchEvent = {
+ id: "match",
+ slug: "match-slug",
+ startTime: kickoff,
+ markets: [market("3", "moneyline"), market("1", "moneyline"), market("2", "moneyline")]
+ };
+ const totalsEvent = {
+ id: "totals",
+ slug: "totals-slug",
+ startTime: kickoff,
+ markets: [market("5", "totals", 2.5), market("4", "totals", 1.5)]
+ };
+ const slugs = new Set(["match-slug", "totals-slug"]);
+ const selected = selectSupportedAssetRecords([totalsEvent, matchEvent], slugs, 50);
+ const shuffled = selectSupportedAssetRecords([
+ { ...matchEvent, markets: [...matchEvent.markets].reverse() },
+ { ...totalsEvent, markets: [...totalsEvent.markets].reverse() }
+ ], slugs, 50);
+ assert.deepEqual(selected.map((record) => record.assetId), shuffled.map((record) => record.assetId));
+ assert.equal(selected.length, 10);
+ assert.throws(() => selectSupportedAssetRecords([matchEvent, totalsEvent], slugs, 9), /exceed --max-assets/);
+ assert.throws(() => selectSupportedAssetRecords([matchEvent, { ...totalsEvent, markets: [] }], slugs, 50), /full-time totals/);
+ const withUnsupported = selectSupportedAssetRecords([
+ { ...matchEvent, markets: [...matchEvent.markets, { ...market("x", "totals", 3.5), sportsMarketType: "spreads" }] },
+ totalsEvent
+ ], slugs, 50);
+ assert.ok(withUnsupported.every((record) => record.marketId !== "x"));
+ assert.throws(() => selectSupportedAssetRecords([
+ {
+ ...matchEvent,
+ markets: [{ ...matchEvent.markets[0]!, clobTokenIds: JSON.stringify(["only-one"]) }, ...matchEvent.markets.slice(1)]
+ },
+ totalsEvent
+ ], slugs, 50), /required outcome pair/);
+});
+
+test("transport heartbeat cannot masquerade as an in-scope real book event", () => {
+ const subscribed = new Set(["asset-1"]);
+ assert.equal(isInScopeRealBookItem({ event_type: "heartbeat_pong", asset_id: "asset-1" }, subscribed), false);
+ assert.equal(isInScopeRealBookItem({ event_type: "book", asset_id: "asset-1" }, subscribed), true);
+ assert.equal(isInScopeRealBookItem({ event_type: "book", asset_id: "other" }, subscribed), false);
+});
diff --git a/plan.md b/plan.md
index 4f7b6fa..3f372dc 100644
--- a/plan.md
+++ b/plan.md
@@ -1,17 +1,17 @@
# SAMARITAN — Build Plan
-*v1.1, July 10, 2026. Ground truth captured; strategies corrected to CONSENSUS_MOVE, XMARKET, FADER, MODELER. Deadline: submission **July 18** (one-day buffer before the July 19 close, which is also World Cup final day).*
+*v1.3, July 18, 2026. Ground truth captured; strategy designs corrected to CONSENSUS_MOVE, XMARKET, FADER, MODELER. The corrected v2 protocol is registered for forward paper observation only; the complete lifecycle proof still uses deterministic model stubs, and the current receipt is unanchored. Hard deadline: July 19 at 23:59 UTC.*
## The one-line mission
-Ship a live, replayable, risk-gated sports trading system on TXLine World Cup data — paper + gated Polymarket execution — with a verifiable on-chain track record by July 18.
+Ship a live/replay-capable, risk-gated paper sports-trading system on TXLine World Cup data with independently verifiable local decision evidence by July 18. Real-money execution and Solana submission remain separately human-gated roadmap items, not bounty-release claims.
## Non-negotiables (from research + legal terms)
1. **The Gate:** no real money moves until the group-stage replay study (Phase 3) is complete and reviewed.
2. **Judge access:** hosted demo, zero cost, zero wallets for judges. Credentials live server-side.
3. **Submission framing:** Deborah is the participant; Samaritan is her product that uses Claude. Never framed as "AI-built."
-4. **Real money is pre-match only in v1.** In-play signals trade on paper until loop latency is measured.
+4. **The bounty build is paper-only.** Any later real-money path would be pre-match only and remains blocked by the Phase 3 gate plus Deborah's explicit authority; in-play signals stay paper-only until loop latency is measured.
5. **No bookmaker scraping/automation. Ever.** TXLine + Polymarket APIs only.
6. **Replay and live share one code path.** If a strategy behaves differently in replay vs live, that's a bug, not a feature.
@@ -28,7 +28,8 @@ The docs only tell us so much; now we touch the real thing.
- [x] Capture and archive real fixtures, historical odds/scores, and a full mainnet odds+scores SSE match
- [x] Verify price format, `Pct` scale, market taxonomy, World Cup `CompetitionId`, score action envelope, retention, and latency behavior
- [x] Polymarket V2 public-market smoke test: metadata, rules, tick/fee fields, order book
-- [x] Confirm human-participant framing: Deborah is the participant; AI development assistance and Claude as a product component are allowed
+- [x] Adopt the conservative human-participant framing: Deborah is the participant, product decision-maker, narrator, reviewer, and submitter; Claude is only a bounded product component and development aid
+- [ ] Retain written sponsor clarification before relying on any broader interpretation of the hackathon AI clause or public derived-data display rights
**Exit criteria:** a `samples/` directory of real payloads + a short WHAT-WE-GOT-WRONG.md correcting any doc-vs-reality gaps.
@@ -60,10 +61,11 @@ The docs only tell us so much; now we touch the real thing.
- [x] Normalization: `market_key = fixtureId + oddsType + period + parameters`; probability space everywhere; use TXLine `Pct` as fair-prob source, compute our own de-vig as cross-check
- [x] Feature engine foundation: TXLine StablePrice velocity and acceleration, EWMA/CUSUM state, consensus-vs-Polymarket spread, Polymarket-only velocity, score-event context, and feed freshness
-- [ ] Detector bank v1: `CONSENSUS_MOVE`, `XMARKET_DIVERGENCE`, `FADER_CANDIDATE`, `MODEL_MARKET_GAP`, `STALE_QUOTE` (paper/live measurement only)
+- [x] Implemented detector slice: `CONSENSUS_MOVE`, `XMARKET_DIVERGENCE`, and `FADER_CANDIDATE`, with evidence-specific eligibility and fail-closed paper admission
+- [ ] Remaining detector work: `MODEL_MARKET_GAP` is unimplemented; `STALE_QUOTE` remains disabled and outside release claims because the synchronized study did not support promotion
- [x] Replay engine: timestamp-merge and re-emit historical sources onto the bus at configurable speed
- [x] Dynamic main-total research harness: build per-fixture balance/volume/liquidity/coverage evidence and rank exact full-time lines under injected weights; production rule remains unfrozen until Phase 3
-- [ ] Detector metrics harness: streaming threshold grids and forward convergence/reversal labels are implemented; score-event labels and STALE latency labels await the synchronized capture
+- [x] Detector metrics harness: streaming threshold grids, forward convergence/reversal labels, and outage-aware score-event/book latency labels are implemented; Spain-Belgium produced no supporting STALE_QUOTE case, so the detector remains disabled
**Exit criteria:** detectors firing sensibly on replayed group-stage matches, with dashboards-in-terminal output.
@@ -71,51 +73,65 @@ The docs only tell us so much; now we touch the real thing.
The mandatory study before any real-money trading. Deliverable is a written report (`docs/06-gate-study.md`):
-- [ ] Historical lane: align 5-minute TXLine StablePrice with one-minute Polymarket sampled prices; measure divergence, convergence, signal CLV, continuation/reversal, and FADER behavior with train/test separation
-- [ ] Freeze a dynamic main-full-time-total selection rule from captured evidence; never hard-code O/U 2.5
-- [ ] Apply conservative historical fee/spread/slippage proxies and label all historical results as signal research, not executable-fill proof
-- [ ] Live lane: use synchronized TXLine SSE + Polymarket books to measure feed availability, event→repricing lag, spread/depth, and STALE_QUOTE feasibility; in-play stays paper-only
-- [ ] Tune `CONSENSUS_MOVE`, XMARKET, and FADER thresholds on replay; report precision/recall and sample sizes
+- [x] Historical lane: timestamp-align TXLine StablePrice with Polymarket sampled prices; measure divergence, convergence, signal CLV, continuation/reversal, and FADER behavior with chronological train/test separation
+- [x] Preserve and suspend the invalidated v1 paper protocol; Deborah registered the corrected causal/economic v2 configuration for forward paper observation only on July 18
+- [x] Apply conservative historical fee/spread/slippage proxies and label all historical results as signal research, not executable-fill proof
+- [x] Live lane: synchronized Spain-Belgium TXLine SSE + Polymarket books measured feed availability, event→repricing lag, spread/depth, and STALE_QUOTE feasibility; 0/18 cases showed a clean post-TXLine stale window, so in-play remains paper-only and STALE_QUOTE stays disabled
+- [x] Tune `CONSENSUS_MOVE`, XMARKET, and FADER threshold candidates on training replay; report sealed heldout precision/recall, CLV, market-family attribution, cost sensitivity, and sample sizes without freezing them
- [ ] MODELER calibration baseline: fit the in-play goal model on group stage, report Brier vs market
-- [ ] **Go/No-Go review with Deborah:** which pre-match edges are real; set per-trade and drawdown caps within the locked $50 bankroll / $15 initial aggregate-exposure ceiling
+- [x] **Forward-paper review with Deborah:** register the corrected totals-only candidate under the locked `$50` paper bankroll, `$3` stake, `$15` exposure, and `$20` rejection stop; real-money remains closed
+- [ ] Run the registered two-lane v2 paper validation (`$3` stake, `$15` exposure, `$20` rejection stop; executable CLV, match-clustered CI, per-match P&L; real-money gate stays closed)
-**Exit criteria:** signed-off study; thresholds frozen for v1; real-money numbers agreed and written into config.
+**Current exit status:** the corrected totals-only thresholds are frozen and registered for forward paper observation as v2. MODELER calibration and qualifying v2 observations remain open. No real-money limits have been approved, and registration does not open the real-money gate.
### Phase 4 — Claude reasoning layer (Jul 14–15)
-- [ ] Trade-thesis schema (strict) — the only bridge from AI to money
-- [ ] Triage agent (Haiku 4.5): dedupe/classify/drop, one-line rationale
-- [ ] Analyst agent (Opus 4.8, adaptive thinking, prompt-cached context): tools = `query_series`, `get_match_state`, `web_search`, `get_polymarket_book`, `submit_thesis`
-- [ ] Risk manager: deterministic rule layer (caps, quarter-Kelly, correlation buckets, drawdown breakers, kill switch) + agent review pass; veto logged with reasons
-- [ ] Decision ledger: append-only record of every signal → triage → thesis → veto/approval → fill
-- [ ] Paper execution adapter (slippage-modeled fills)
-- [ ] Spend controls: $200 operating target, $300 hard project ceiling; fail to detectors-only mode rather than exceed it
-
-**Exit criteria:** end-to-end on replay — signal fires, Haiku triages, Opus writes a thesis, risk manager rules, paper fill lands, ledger records all of it.
+- [x] Trade-thesis schema (strict) — the only bridge from AI to deterministic risk; unknown sizing/order fields are rejected
+- [x] Triage agent (Haiku 4.5): bounded strict-tool classify/drop/escalate with one-line rationale
+- [x] Analyst agent bounty slice: Opus 4.8 adaptive thinking over a bounded code-assembled signal bundle; strict `submit_thesis` is the only tool/exit
+- [ ] Analyst pull-tools extension: `query_series`, `get_match_state`, `web_search`, and `get_polymarket_book` remain unimplemented and are not submission claims
+- [x] Deterministic paper-risk slice: registered fixed stake, bankroll/exposure/drawdown limits, market/evidence checks, and settled portfolio state
+- [ ] Full risk-manager roadmap: quarter-Kelly, correlation buckets, a separately authorized global manual kill switch, and the judgment pass remain unimplemented and are not bounty claims
+- [x] Decision ledger: append-only hash-chained record of every signal → triage → thesis → veto/approval → execution intent → paper result
+- [x] Paper execution adapter: deterministic limit, post-latency canonical book, depth walking, partial/no fills, per-market fees, micro-unit accounting
+- [x] Paper case scheduler/runtime: pre-model lane-specific ledgering, measured wall-clock decision latency mapped to event time, frozen detector/selector config, 15-minute kickoff cutoff, first eligible post-decision canonical book, duplicate suppression, terminal expiry
+- [x] Public CLOB execution metadata: cached V2 fee curve, taker-only flag, minimum shares, tick size, condition/token identity; malformed or legacy-only metadata fails closed
+- [x] Paper portfolio evidence: actual entry cost, midpoint/executable CLV, settlement P&L/Brier, exposure, equity peak, and drawdown state
+- [x] Kickoff/settlement lifecycle: latest canonical pre-kickoff close, public winning-asset resolution, and pre-mutation append-only accounting
+- [x] Protocol-configured evidence evaluator: ledger reconstruction, sealed long-run endpoints, per-match rows, 10,000-iteration match-block bootstrap, random-direction control, and guardrails
+- [x] Preserve the v1 two-lane zero-observation ledgers as audit history; registered v2 uses fresh versioned ledgers and never relabels v1 rows
+- [x] Fixture evidence/readiness gates: paired-book vs sampled-history classification, physical scheduler exclusion, observed-time captured replay merge, and three-hour pre-kickoff capture target
+- [x] Semifinal capture preflight: authenticated read-only TXLine refresh, exact France-Spain / England-Argentina cross-source identity, full-time totals rules check, and launch held until Deborah's capture-only confirmation
+- [x] Semifinal capture outcome: both watchdog-supervised attempts failed closed—France-Spain before kickoff and England-Argentina after a Polymarket stall with incomplete terminal evidence; neither is replay-, study-, mapping-, or Claude-admissible
+- [x] Rolling fixture admission: atomic two-lane refresh, verified mapping + paired executable evidence, immutable admitted identity, and pre-Claude research-only rejection
+- [x] Evidence artifact writer: ledger-derived JSON + frozen-protocol per-match Markdown table with strict long-run sealing
+- [x] Spend controls: shared append-only reservations, $200 operating target, $300 pre-request hard project ceiling, and fail-closed API accounting
+
+**Bounty-slice exit criteria:** the shared replay conductor completes signal → deterministic Haiku-shaped stub → deterministic Opus-shaped stub → hard risk → paper fill → settlement and ledger proof. V2 registration is complete; a real Haiku/Opus replay remains blocked until a fresh fixture passes every exact admission and mapping-review gate.
### Phase 5 — Tournament & real execution (Jul 15–16)
- [ ] Four personas live: Momentum(CONSENSUS_MOVE), Arb(XMARKET), Fader, Modeler — isolated paper bankrolls
- [ ] Scoring engine: CLV vs TXLine closing `Pct`, Brier, P&L, drawdown; head-trader reallocation (paper capital only in v1)
- [ ] Polymarket market mapping w/ **settlement-rules verification step** (Claude reads market rules; 90-min vs to-advance semantics checked per market) — human confirm on each mapping
-- [ ] `PolymarketAdapter` (V2): real orders, **pre-match markets only**, hard bankroll cap from Phase 3 numbers, global kill switch (manual, Deborah's)
+- [ ] Future `PolymarketAdapter` (V2): real orders, **pre-match markets only**, separately approved bankroll caps, and a global manual kill switch owned by Deborah; none is implemented or enabled in the bounty build
- [ ] Real-money dry run only if the gate approves, mapping is human-confirmed, and Polymarket's minimum order fits every cap; otherwise record an explicit fail-closed no-trade
**Exit criteria:** tournament running on replay/live data; any eligible real fill is correctly ledgered, while absence of a qualifying trade is an acceptable and visible no-trade outcome.
### Phase 6 — On-chain proof & dashboard (Jul 16–17)
-- [ ] Ledger anchoring: hash ledger segments → Solana memo tx per match/hour; explorer links surfaced
-- [ ] Merkle spot-validation of TXLine data we acted on (validation endpoints)
-- [ ] Dashboard (the judge surface): live board, signal tape, agent reasoning feed (triage→thesis→veto visible), tournament leaderboard, CLV/calibration curves, verification panel (message IDs, proofs, anchor txs)
-- [ ] Hosted deployment: credentials server-side, judges get a URL, read-only
+- [ ] Optional post-bounty ledger anchoring: hash ledger segments → Solana memo transactions; the bounty release intentionally remains unanchored and has no explorer transaction
+- [ ] Merkle spot-validation of acted-on TXLine data remains unimplemented; validation endpoints are documented research, not a current receipt claim
+- [x] Dashboard judge surface implemented against the approved Matchroom v2 dark-hybrid direction in [`docs/UI.md`](docs/UI.md): Command, Matchroom, Decision Rail, Casebook, Study, and Proof; captured, historical, synthetic, paper, and unanchored states remain visibly separated. Light mode is deferred and non-blocking.
+- [x] Hosted deployment: credentials remain server-side; judges get a [public, read-only, no-login URL](https://samaritan-txline-2026.asaborodaniel.chatgpt.site/)
-**Exit criteria:** a stranger with a link can watch Samaritan think during a live match.
+**Exit criteria:** a stranger with a link can inspect captured evidence and the clearly separated synthetic lifecycle without credentials, a wallet, or private feed redistribution. Registration is visible but must not be confused with an observed real-Claude case.
### Phase 7 — Live fire & submission (Jul 17–18)
-- [ ] Run live through semifinals; capture the best sequences
-- [ ] Demo video: replay proof (group stage CLV curves) → live signal → agent debate → risk veto/approval → fill → on-chain anchor
+- [x] Attempt both semifinal paired captures under watchdogs; both failed closed and remain inadmissible rather than being promoted into a replay or v2 observation
+- [ ] Demo video: real captured refusal → corrected historical signal evidence → synthetic stubbed lifecycle → offline receipt verification → explicit unanchored/closed-gate boundary
- [ ] Submission writeup: what it is, what's measured, what's verifiable; framing per legal terms
- [ ] **Submit July 18.** July 19 (final day) is buffer + live victory lap, not crunch.
@@ -127,15 +143,16 @@ The mandatory study before any real-money trading. Deliverable is a written repo
|---|---|
| TXLine free tier has no named bookmakers | Momentum is consensus-regime detection; no v1 claim of bookmaker leader-lag |
| Historical Polymarket prices are non-executable samples | Conservative research fills only; live books establish executable spread/depth behavior |
-| Polymarket integration changes | V2-only adapter; paper is the always-working fallback |
+| Polymarket integration changes | V2 public-data and fee integration only in the bounty build; deterministic paper execution is the demonstrated path and no production order adapter is connected |
| Few live matches left | Rescued replay data carries the demo; remaining matches provide synchronized microstructure evidence |
| Loop too slow for in-play edges | In-play is paper-only in v1 by design; latency measured, not assumed |
| Claude API spend | Triage-first funnel; Haiku screens everything; Opus only on escalation; prompt caching on static context |
-| Feed degradation mid-match | Data Doctor watchdog auto-halts trading |
+| Feed degradation mid-match | The deterministic capture supervisor and stream watchdog fail closed; the planned Data Doctor agent is not implemented and cannot be claimed |
| Mapping/settlement mismatch | Research candidates are never tradeable; rules verification + human confirmation per live market |
## Locked and pending from Deborah
- **Locked:** Match Result + dynamic main full-time O/U only; Claude $200 target/$300 hard ceiling; $50 total bankroll; $15 maximum initial aggregate live exposure; daily lesson review; concurrency 3; nightly batch post-mortems.
-- **Pending at Phase 3:** per-trade cap and drawdown halts; approval of any real-money edge.
+- **Registered July 18:** the v1 totals-only paper protocol remains invalidated and preserved. Deborah registered corrected v2 for forward paper observation only under the exact `$50` bankroll, `$3` stake, `$15` aggregate exposure, and `$20` drawdown rejection stop. Fresh v2 ledgers are mandatory; no v1 observation carries forward.
+- **Pending after paper ACCEPT:** separate approval of any real-money edge and its real-money caps; paper approval does not carry over.
- **Pending per live mapping:** human confirmation of teams, kickoff, market period, and full Polymarket rules text.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a785e9a..d89078d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,16 +4,37 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ jayson>uuid: 11.1.1
+
importers:
.:
dependencies:
+ '@anthropic-ai/sdk':
+ specifier: ^0.111.0
+ version: 0.111.0(zod@4.4.3)
'@duckdb/node-api':
specifier: 1.5.4-r.1
version: 1.5.4-r.1
+ '@fontsource/ibm-plex-mono':
+ specifier: ^5.2.6
+ version: 5.2.7
+ '@fontsource/manrope':
+ specifier: ^5.2.6
+ version: 5.2.8
+ '@solana/web3.js':
+ specifier: ^1.98.4
+ version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
better-sqlite3:
specifier: ^12.11.1
version: 12.11.1
+ react:
+ specifier: ^19.1.1
+ version: 19.2.7
+ react-dom:
+ specifier: ^19.1.1
+ version: 19.2.7(react@19.2.7)
stream-json:
specifier: ^3.5.0
version: 3.5.0
@@ -30,59 +51,132 @@ importers:
'@types/node':
specifier: ^22.20.1
version: 22.20.1
+ '@types/react':
+ specifier: ^19.1.10
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: ^19.1.7
+ version: 19.2.3(@types/react@19.2.17)
'@types/stream-json':
specifier: ^1.7.8
version: 1.7.8
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
+ '@vitejs/plugin-react':
+ specifier: ^5.0.4
+ version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0))
tsx:
specifier: ^4.20.6
version: 4.23.0
typescript:
specifier: ^5.9.3
version: 5.9.3
+ vite:
+ specifier: ^7.1.7
+ version: 7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0)
vitest:
specifier: ^4.1.10
- version: 4.1.10(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0))
-
- phase0:
- dependencies:
- '@solana/spl-token':
- specifier: ^0.4.13
- version: 0.4.15(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)
- '@solana/web3.js':
- specifier: ^1.98.4
- version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- dotenv:
- specifier: ^17.2.3
- version: 17.4.2
- tweetnacl:
- specifier: ^1.0.3
- version: 1.0.3
- ws:
- specifier: ^8.21.0
- version: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
- devDependencies:
- '@types/node':
- specifier: ^24.10.0
- version: 24.13.3
- '@types/ws':
- specifier: ^8.18.1
- version: 8.18.1
- tsx:
- specifier: ^4.20.6
- version: 4.23.0
- typescript:
- specifier: ^5.9.3
- version: 5.9.3
+ version: 4.1.10(@types/node@22.20.1)(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0))
packages:
+ '@anthropic-ai/sdk@0.111.0':
+ resolution: {integrity: sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==}
+ hasBin: true
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+ peerDependenciesMeta:
+ zod:
+ optional: true
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
'@duckdb/node-api@1.5.4-r.1':
resolution: {integrity: sha512-3PYk/4//svuYmb9RYVM71cCoNa6ngoYWwrMhJaqbm010txzIMWbJCbxrFeqDl+krdIug+Hc/MNCeS0gHsTXSIw==}
@@ -133,15 +227,6 @@ packages:
'@duckdb/node-bindings@1.5.4-r.1':
resolution: {integrity: sha512-v834OZZKQ59yAvh8GOEmM+R1foPNgdMGL0VTBgHywgblgQNyq11HvWgT4QGJIUFfo/cQ9WFyf1MFhK0+hV1aiA==}
- '@emnapi/core@1.11.1':
- resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
-
- '@emnapi/runtime@1.11.1':
- resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
-
- '@emnapi/wasi-threads@1.2.2':
- resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
-
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
@@ -298,14 +383,27 @@ packages:
cpu: [x64]
os: [win32]
+ '@fontsource/ibm-plex-mono@5.2.7':
+ resolution: {integrity: sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==}
+
+ '@fontsource/manrope@5.2.8':
+ resolution: {integrity: sha512-gJHJmcuUk7qWcNCfcAri/DJQtXtBYqi9yKratr4jXhSo0I3xUtNNKI+igQIcw5c+m95g0vounk8ZnX/kb8o0TA==}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
- '@napi-rs/wasm-runtime@1.1.6':
- resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
- peerDependencies:
- '@emnapi/core': ^1.7.1
- '@emnapi/runtime': ^1.7.1
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@noble/curves@1.9.7':
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
@@ -315,159 +413,163 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
- '@oxc-project/types@0.139.0':
- resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
+ '@rolldown/pluginutils@1.0.0-rc.3':
+ resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
- '@rolldown/binding-android-arm64@1.1.5':
- resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-android-arm-eabi@4.62.2':
+ resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.62.2':
+ resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==}
cpu: [arm64]
os: [android]
- '@rolldown/binding-darwin-arm64@1.1.5':
- resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-darwin-arm64@4.62.2':
+ resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==}
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.1.5':
- resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-darwin-x64@4.62.2':
+ resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==}
cpu: [x64]
os: [darwin]
- '@rolldown/binding-freebsd-x64@1.1.5':
- resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-freebsd-arm64@4.62.2':
+ resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.62.2':
+ resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==}
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
- resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
+ resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
- '@rolldown/binding-linux-arm64-gnu@1.1.5':
- resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
+ resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
+ resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-arm64-musl@1.1.5':
- resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
+ resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rolldown/binding-linux-ppc64-gnu@1.1.5':
- resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
+ resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
+ resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
+ resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-s390x-gnu@1.1.5':
- resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
+ resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
+ resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
+ resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
+ resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-gnu@1.1.5':
- resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-musl@1.1.5':
- resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-linux-x64-musl@4.62.2':
+ resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rolldown/binding-openharmony-arm64@1.1.5':
- resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-openbsd-x64@4.62.2':
+ resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.62.2':
+ resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==}
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-wasm32-wasi@1.1.5':
- resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [wasm32]
-
- '@rolldown/binding-win32-arm64-msvc@1.1.5':
- resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
+ resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==}
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.1.5':
- resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
+ resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==}
+ cpu: [ia32]
os: [win32]
- '@rolldown/pluginutils@1.0.1':
- resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==}
+ cpu: [x64]
+ os: [win32]
- '@solana/buffer-layout-utils@0.3.0':
- resolution: {integrity: sha512-MuQOCC1j0np1xH9yAv0ZWWfwvr7Bt7Sz4LId11Wi4wDdAmJ+lobE+vHg/mZmGcihF0BIkqVBNxGmlv8QE5DrtA==}
- engines: {node: '>= 10'}
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
+ resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==}
+ cpu: [x64]
+ os: [win32]
'@solana/buffer-layout@4.0.1':
resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
engines: {node: '>=5.10'}
- '@solana/codecs-core@2.0.0-rc.1':
- resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==}
- peerDependencies:
- typescript: '>=5'
-
'@solana/codecs-core@2.3.0':
resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==}
engines: {node: '>=20.18.0'}
peerDependencies:
typescript: '>=5.3.3'
- '@solana/codecs-data-structures@2.0.0-rc.1':
- resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==}
- peerDependencies:
- typescript: '>=5'
-
- '@solana/codecs-numbers@2.0.0-rc.1':
- resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==}
- peerDependencies:
- typescript: '>=5'
-
'@solana/codecs-numbers@2.3.0':
resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==}
engines: {node: '>=20.18.0'}
peerDependencies:
typescript: '>=5.3.3'
- '@solana/codecs-strings@2.0.0-rc.1':
- resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==}
- peerDependencies:
- fastestsmallesttextencoderdecoder: ^1.0.22
- typescript: '>=5'
-
- '@solana/codecs@2.0.0-rc.1':
- resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==}
- peerDependencies:
- typescript: '>=5'
-
- '@solana/errors@2.0.0-rc.1':
- resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==}
- hasBin: true
- peerDependencies:
- typescript: '>=5'
-
'@solana/errors@2.3.0':
resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==}
engines: {node: '>=20.18.0'}
@@ -475,40 +577,29 @@ packages:
peerDependencies:
typescript: '>=5.3.3'
- '@solana/options@2.0.0-rc.1':
- resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==}
- peerDependencies:
- typescript: '>=5'
-
- '@solana/spl-token-group@0.0.7':
- resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==}
- engines: {node: '>=16'}
- peerDependencies:
- '@solana/web3.js': ^1.95.3
-
- '@solana/spl-token-metadata@0.1.6':
- resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==}
- engines: {node: '>=16'}
- peerDependencies:
- '@solana/web3.js': ^1.95.3
-
- '@solana/spl-token@0.4.15':
- resolution: {integrity: sha512-3Lof3mNov8NVQ3PalIWb1Jgr/TZ6lYM+/sexv2TLqdhNFVth2OfWmH3d7QucgMjSbokkjNiNlRr6I8Fd269uaw==}
- engines: {node: '>=16'}
- peerDependencies:
- '@solana/web3.js': ^1.95.5
-
'@solana/web3.js@1.98.4':
resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==}
+ '@stablelib/base64@1.0.1':
+ resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
- '@tybys/wasm-util@0.10.3':
- resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
@@ -534,6 +625,14 @@ packages:
'@types/node@24.13.3':
resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.17':
+ resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+
'@types/stream-chain@2.1.0':
resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==}
@@ -549,6 +648,12 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+ '@vitejs/plugin-react@5.2.0':
+ resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+
'@vitest/expect@4.1.10':
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
@@ -592,17 +697,15 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.10.43:
+ resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
better-sqlite3@12.11.1:
resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
- bigint-buffer@1.1.5:
- resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
- engines: {node: '>= 10.0.0'}
-
- bignumber.js@9.3.1:
- resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
-
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -615,6 +718,11 @@ packages:
borsh@0.7.0:
resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
+ browserslist@4.28.6:
+ resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
bs58@4.0.1:
resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
@@ -628,6 +736,9 @@ packages:
resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==}
engines: {node: '>=6.14.2'}
+ caniuse-lite@1.0.30001805:
+ resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==}
+
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -639,10 +750,6 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
- commander@12.1.0:
- resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
- engines: {node: '>=18'}
-
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
@@ -653,6 +760,18 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -669,9 +788,8 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
- dotenv@17.4.2:
- resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
- engines: {node: '>=12'}
+ electron-to-chromium@1.5.389:
+ resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@ -690,6 +808,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -708,12 +830,12 @@ packages:
resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
engines: {node: '> 0.1.90'}
+ fast-sha256@1.3.0:
+ resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
+
fast-stable-stringify@1.0.0:
resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
- fastestsmallesttextencoderdecoder@1.0.22:
- resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
-
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -734,6 +856,10 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@@ -759,9 +885,26 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-schema-to-ts@3.1.1:
+ resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
+ engines: {node: '>=16'}
+
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -836,6 +979,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -877,6 +1023,10 @@ packages:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
+ node-releases@2.0.51:
+ resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
+ engines: {node: '>=18'}
+
obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
@@ -911,13 +1061,26 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ react-dom@19.2.7:
+ resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
+ peerDependencies:
+ react: ^19.2.7
+
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react@19.2.7:
+ resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
+ engines: {node: '>=0.10.0'}
+
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
- rolldown@1.1.5:
- resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ rollup@4.62.2:
+ resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rpc-websockets@9.3.9:
@@ -926,6 +1089,13 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
@@ -947,6 +1117,9 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+ standardwebhooks@1.0.0:
+ resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
+
std-env@4.2.0:
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
@@ -1003,6 +1176,9 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ ts-algebra@2.0.0:
+ resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -1014,9 +1190,6 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
- tweetnacl@1.0.3:
- resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
-
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -1028,6 +1201,12 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
utf-8-validate@6.0.6:
resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==}
engines: {node: '>=6.14.2'}
@@ -1035,25 +1214,23 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- uuid@14.0.1:
- resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
+ uuid@11.1.1:
+ resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
hasBin: true
- uuid@8.3.2:
- resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
- deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
+ uuid@14.0.1:
+ resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
- vite@8.1.4:
- resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==}
+ vite@7.3.6:
+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
- '@vitejs/devtools': ^0.3.0
- esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
+ lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
@@ -1064,14 +1241,12 @@ packages:
peerDependenciesMeta:
'@types/node':
optional: true
- '@vitejs/devtools':
- optional: true
- esbuild:
- optional: true
jiti:
optional: true
less:
optional: true
+ lightningcss:
+ optional: true
sass:
optional: true
sass-embedded:
@@ -1166,13 +1341,135 @@ packages:
utf-8-validate:
optional: true
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
+ '@anthropic-ai/sdk@0.111.0(zod@4.4.3)':
+ dependencies:
+ json-schema-to-ts: 3.1.1
+ standardwebhooks: 1.0.0
+ optionalDependencies:
+ zod: 4.4.3
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.6
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
'@babel/runtime@7.29.7': {}
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/traverse@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
'@duckdb/node-api@1.5.4-r.1':
dependencies:
'@duckdb/node-bindings': 1.5.4-r.1
@@ -1214,22 +1511,6 @@ snapshots:
'@duckdb/node-bindings-win32-arm64': 1.5.4-r.1
'@duckdb/node-bindings-win32-x64': 1.5.4-r.1
- '@emnapi/core@1.11.1':
- dependencies:
- '@emnapi/wasi-threads': 1.2.2
- tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.11.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/wasi-threads@1.2.2':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@esbuild/aix-ppc64@0.28.1':
optional: true
@@ -1308,14 +1589,28 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
+ '@fontsource/ibm-plex-mono@5.2.7': {}
+
+ '@fontsource/manrope@5.2.8': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
'@jridgewell/sourcemap-codec@1.5.5': {}
- '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ '@jridgewell/trace-mapping@0.3.31':
dependencies:
- '@emnapi/core': 1.11.1
- '@emnapi/runtime': 1.11.1
- '@tybys/wasm-util': 0.10.3
- optional: true
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
'@noble/curves@1.9.7':
dependencies:
@@ -1323,177 +1618,104 @@ snapshots:
'@noble/hashes@1.8.0': {}
- '@oxc-project/types@0.139.0': {}
+ '@rolldown/pluginutils@1.0.0-rc.3': {}
- '@rolldown/binding-android-arm64@1.1.5':
+ '@rollup/rollup-android-arm-eabi@4.62.2':
optional: true
- '@rolldown/binding-darwin-arm64@1.1.5':
+ '@rollup/rollup-android-arm64@4.62.2':
optional: true
- '@rolldown/binding-darwin-x64@1.1.5':
+ '@rollup/rollup-darwin-arm64@4.62.2':
optional: true
- '@rolldown/binding-freebsd-x64@1.1.5':
+ '@rollup/rollup-darwin-x64@4.62.2':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ '@rollup/rollup-freebsd-arm64@4.62.2':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ '@rollup/rollup-freebsd-x64@4.62.2':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.1.5':
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.1.5':
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
optional: true
- '@rolldown/binding-linux-x64-musl@1.1.5':
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
optional: true
- '@rolldown/binding-openharmony-arm64@1.1.5':
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
optional: true
- '@rolldown/binding-wasm32-wasi@1.1.5':
- dependencies:
- '@emnapi/core': 1.11.1
- '@emnapi/runtime': 1.11.1
- '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
optional: true
- '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.1.5':
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
optional: true
- '@rolldown/pluginutils@1.0.1': {}
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
+ optional: true
- '@solana/buffer-layout-utils@0.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)':
- dependencies:
- '@solana/buffer-layout': 4.0.1
- '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- bigint-buffer: 1.1.5
- bignumber.js: 9.3.1
- transitivePeerDependencies:
- - bufferutil
- - encoding
- - typescript
- - utf-8-validate
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.62.2':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
+ optional: true
'@solana/buffer-layout@4.0.1':
dependencies:
buffer: 6.0.3
- '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.3)':
- dependencies:
- '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
- typescript: 5.9.3
-
'@solana/codecs-core@2.3.0(typescript@5.9.3)':
dependencies:
'@solana/errors': 2.3.0(typescript@5.9.3)
typescript: 5.9.3
- '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)':
- dependencies:
- '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
- typescript: 5.9.3
-
- '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)':
- dependencies:
- '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
- typescript: 5.9.3
-
'@solana/codecs-numbers@2.3.0(typescript@5.9.3)':
dependencies:
'@solana/codecs-core': 2.3.0(typescript@5.9.3)
'@solana/errors': 2.3.0(typescript@5.9.3)
typescript: 5.9.3
- '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
- dependencies:
- '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
- fastestsmallesttextencoderdecoder: 1.0.22
- typescript: 5.9.3
-
- '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
- dependencies:
- '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - fastestsmallesttextencoderdecoder
-
- '@solana/errors@2.0.0-rc.1(typescript@5.9.3)':
- dependencies:
- chalk: 5.6.2
- commander: 12.1.0
- typescript: 5.9.3
-
'@solana/errors@2.3.0(typescript@5.9.3)':
dependencies:
chalk: 5.6.2
commander: 14.0.3
typescript: 5.9.3
- '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
- dependencies:
- '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
- '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - fastestsmallesttextencoderdecoder
-
- '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
- dependencies:
- '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- transitivePeerDependencies:
- - fastestsmallesttextencoderdecoder
- - typescript
-
- '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
- dependencies:
- '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- transitivePeerDependencies:
- - fastestsmallesttextencoderdecoder
- - typescript
-
- '@solana/spl-token@0.4.15(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)':
- dependencies:
- '@solana/buffer-layout': 4.0.1
- '@solana/buffer-layout-utils': 0.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
- '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
- buffer: 6.0.3
- transitivePeerDependencies:
- - bufferutil
- - encoding
- - fastestsmallesttextencoderdecoder
- - typescript
- - utf-8-validate
-
'@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)':
dependencies:
'@babel/runtime': 7.29.7
@@ -1517,16 +1739,34 @@ snapshots:
- typescript
- utf-8-validate
+ '@stablelib/base64@1.0.1': {}
+
'@standard-schema/spec@1.1.0': {}
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
- '@tybys/wasm-util@0.10.3':
+ '@types/babel__core@7.20.5':
dependencies:
- tslib: 2.8.1
- optional: true
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.7
'@types/better-sqlite3@7.6.13':
dependencies:
@@ -1555,6 +1795,14 @@ snapshots:
dependencies:
undici-types: 7.18.2
+ '@types/react-dom@19.2.3(@types/react@19.2.17)':
+ dependencies:
+ '@types/react': 19.2.17
+
+ '@types/react@19.2.17':
+ dependencies:
+ csstype: 3.2.3
+
'@types/stream-chain@2.1.0':
dependencies:
'@types/node': 24.13.3
@@ -1574,6 +1822,18 @@ snapshots:
dependencies:
'@types/node': 24.13.3
+ '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0))':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
+ '@rolldown/pluginutils': 1.0.0-rc.3
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@vitest/expect@4.1.10':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -1583,13 +1843,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
- '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0))':
+ '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0)
+ vite: 7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0)
'@vitest/pretty-format@4.1.10':
dependencies:
@@ -1627,17 +1887,13 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.10.43: {}
+
better-sqlite3@12.11.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
- bigint-buffer@1.1.5:
- dependencies:
- bindings: 1.5.0
-
- bignumber.js@9.3.1: {}
-
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@@ -1656,6 +1912,14 @@ snapshots:
bs58: 4.0.1
text-encoding-utf-8: 1.0.2
+ browserslist@4.28.6:
+ dependencies:
+ baseline-browser-mapping: 2.10.43
+ caniuse-lite: 1.0.30001805
+ electron-to-chromium: 1.5.389
+ node-releases: 2.0.51
+ update-browserslist-db: 1.2.3(browserslist@4.28.6)
+
bs58@4.0.1:
dependencies:
base-x: 3.0.11
@@ -1675,20 +1939,26 @@ snapshots:
node-gyp-build: 4.8.4
optional: true
+ caniuse-lite@1.0.30001805: {}
+
chai@6.2.2: {}
chalk@5.6.2: {}
chownr@1.1.4: {}
- commander@12.1.0: {}
-
commander@14.0.3: {}
commander@2.20.3: {}
convert-source-map@2.0.0: {}
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -1699,7 +1969,7 @@ snapshots:
detect-libc@2.1.2: {}
- dotenv@17.4.2: {}
+ electron-to-chromium@1.5.389: {}
end-of-stream@1.4.5:
dependencies:
@@ -1742,6 +2012,8 @@ snapshots:
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
+ escalade@3.2.0: {}
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
@@ -1754,9 +2026,9 @@ snapshots:
eyes@0.1.8: {}
- fast-stable-stringify@1.0.0: {}
+ fast-sha256@1.3.0: {}
- fastestsmallesttextencoderdecoder@1.0.22: {}
+ fast-stable-stringify@1.0.0: {}
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
@@ -1769,6 +2041,8 @@ snapshots:
fsevents@2.3.3:
optional: true
+ gensync@1.0.0-beta.2: {}
+
github-from-package@0.0.0: {}
humanize-ms@1.2.1:
@@ -1797,14 +2071,25 @@ snapshots:
isomorphic-ws: 4.0.1(ws@7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6))
json-stringify-safe: 5.0.1
stream-json: 1.9.1
- uuid: 8.3.2
+ uuid: 11.1.1
ws: 7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-schema-to-ts@3.1.1:
+ dependencies:
+ '@babel/runtime': 7.29.7
+ ts-algebra: 2.0.0
+
json-stringify-safe@5.0.1: {}
+ json5@2.2.3: {}
+
lightningcss-android-arm64@1.32.0:
optional: true
@@ -1853,6 +2138,11 @@ snapshots:
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ optional: true
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
magic-string@0.30.21:
dependencies:
@@ -1881,6 +2171,8 @@ snapshots:
node-gyp-build@4.8.4:
optional: true
+ node-releases@2.0.51: {}
+
obug@2.1.3: {}
once@1.4.0:
@@ -1926,32 +2218,51 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
+ react-dom@19.2.7(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ scheduler: 0.27.0
+
+ react-refresh@0.18.0: {}
+
+ react@19.2.7: {}
+
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
- rolldown@1.1.5:
+ rollup@4.62.2:
dependencies:
- '@oxc-project/types': 0.139.0
- '@rolldown/pluginutils': 1.0.1
+ '@types/estree': 1.0.9
optionalDependencies:
- '@rolldown/binding-android-arm64': 1.1.5
- '@rolldown/binding-darwin-arm64': 1.1.5
- '@rolldown/binding-darwin-x64': 1.1.5
- '@rolldown/binding-freebsd-x64': 1.1.5
- '@rolldown/binding-linux-arm-gnueabihf': 1.1.5
- '@rolldown/binding-linux-arm64-gnu': 1.1.5
- '@rolldown/binding-linux-arm64-musl': 1.1.5
- '@rolldown/binding-linux-ppc64-gnu': 1.1.5
- '@rolldown/binding-linux-s390x-gnu': 1.1.5
- '@rolldown/binding-linux-x64-gnu': 1.1.5
- '@rolldown/binding-linux-x64-musl': 1.1.5
- '@rolldown/binding-openharmony-arm64': 1.1.5
- '@rolldown/binding-wasm32-wasi': 1.1.5
- '@rolldown/binding-win32-arm64-msvc': 1.1.5
- '@rolldown/binding-win32-x64-msvc': 1.1.5
+ '@rollup/rollup-android-arm-eabi': 4.62.2
+ '@rollup/rollup-android-arm64': 4.62.2
+ '@rollup/rollup-darwin-arm64': 4.62.2
+ '@rollup/rollup-darwin-x64': 4.62.2
+ '@rollup/rollup-freebsd-arm64': 4.62.2
+ '@rollup/rollup-freebsd-x64': 4.62.2
+ '@rollup/rollup-linux-arm-gnueabihf': 4.62.2
+ '@rollup/rollup-linux-arm-musleabihf': 4.62.2
+ '@rollup/rollup-linux-arm64-gnu': 4.62.2
+ '@rollup/rollup-linux-arm64-musl': 4.62.2
+ '@rollup/rollup-linux-loong64-gnu': 4.62.2
+ '@rollup/rollup-linux-loong64-musl': 4.62.2
+ '@rollup/rollup-linux-ppc64-gnu': 4.62.2
+ '@rollup/rollup-linux-ppc64-musl': 4.62.2
+ '@rollup/rollup-linux-riscv64-gnu': 4.62.2
+ '@rollup/rollup-linux-riscv64-musl': 4.62.2
+ '@rollup/rollup-linux-s390x-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-musl': 4.62.2
+ '@rollup/rollup-openbsd-x64': 4.62.2
+ '@rollup/rollup-openharmony-arm64': 4.62.2
+ '@rollup/rollup-win32-arm64-msvc': 4.62.2
+ '@rollup/rollup-win32-ia32-msvc': 4.62.2
+ '@rollup/rollup-win32-x64-gnu': 4.62.2
+ '@rollup/rollup-win32-x64-msvc': 4.62.2
+ fsevents: 2.3.3
rpc-websockets@9.3.9:
dependencies:
@@ -1968,6 +2279,10 @@ snapshots:
safe-buffer@5.2.1: {}
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
semver@7.8.5: {}
siginfo@2.0.0: {}
@@ -1984,6 +2299,11 @@ snapshots:
stackback@0.0.2: {}
+ standardwebhooks@1.0.0:
+ dependencies:
+ '@stablelib/base64': 1.0.1
+ fast-sha256: 1.3.0
+
std-env@4.2.0: {}
stream-chain@2.2.5: {}
@@ -2036,6 +2356,8 @@ snapshots:
tr46@0.0.3: {}
+ ts-algebra@2.0.0: {}
+
tslib@2.8.1: {}
tsx@4.23.0:
@@ -2048,14 +2370,18 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
- tweetnacl@1.0.3: {}
-
typescript@5.9.3: {}
undici-types@6.21.0: {}
undici-types@7.18.2: {}
+ update-browserslist-db@1.2.3(browserslist@4.28.6):
+ dependencies:
+ browserslist: 4.28.6
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
utf-8-validate@6.0.6:
dependencies:
node-gyp-build: 4.8.4
@@ -2063,27 +2389,28 @@ snapshots:
util-deprecate@1.0.2: {}
- uuid@14.0.1: {}
+ uuid@11.1.1: {}
- uuid@8.3.2: {}
+ uuid@14.0.1: {}
- vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0):
+ vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0):
dependencies:
- lightningcss: 1.32.0
+ esbuild: 0.28.1
+ fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
postcss: 8.5.16
- rolldown: 1.1.5
+ rollup: 4.62.2
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 22.20.1
- esbuild: 0.28.1
fsevents: 2.3.3
+ lightningcss: 1.32.0
tsx: 4.23.0
- vitest@4.1.10(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0)):
+ vitest@4.1.10(@types/node@22.20.1)(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0)):
dependencies:
'@vitest/expect': 4.1.10
- '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0))
+ '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -2100,7 +2427,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.0)
+ vite: 7.3.6(@types/node@22.20.1)(lightningcss@1.32.0)(tsx@4.23.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.20.1
@@ -2131,4 +2458,6 @@ snapshots:
bufferutil: 4.1.0
utf-8-validate: 6.0.6
+ yallist@3.1.1: {}
+
zod@4.4.3: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ed70373..017b7a8 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,3 +1,11 @@
packages:
- .
- - phase0
+
+allowBuilds:
+ better-sqlite3: true
+ bufferutil: true
+ esbuild: true
+ utf-8-validate: true
+
+overrides:
+ 'jayson>uuid': 11.1.1
diff --git a/proof/fixtures/decision-receipt.synthetic.v1.json b/proof/fixtures/decision-receipt.synthetic.v1.json
new file mode 100644
index 0000000..73e28a3
--- /dev/null
+++ b/proof/fixtures/decision-receipt.synthetic.v1.json
@@ -0,0 +1,435 @@
+{
+ "schemaVersion": 1,
+ "receiptType": "samaritan_decision_receipt",
+ "receiptId": "6a5268ce1e18a14b8b07228b74635c4e2337ee40ffa36aefd7a113cf6d174e66",
+ "generatedAtTsMs": 1700000011000,
+ "provenance": {
+ "evidenceClass": "synthetic_proving_fixture",
+ "synthetic": true,
+ "performanceUse": "excluded_synthetic",
+ "label": "SYNTHETIC PROVING FIXTURE — no real match, feed, model call, order, fill, or performance evidence"
+ },
+ "disclosure": {
+ "policy": "hashes_and_derived_signals_only",
+ "rawTxlineFieldsIncluded": false
+ },
+ "build": {
+ "codeVersion": "decision-receipt-v1-synthetic-fixture",
+ "codeSha256": "b3378a2df0ceefd4d0d65a4dbf7cb86ca259be10be737449956502cc38dd64e9",
+ "configSha256": "2a862746dc1f07064367445667a530eea645c42d7ea7f0ffaa432ba72d2d674c"
+ },
+ "sourceEvidence": [
+ {
+ "evidenceRefSha256": "3d3b41a2227873d8b21248202f7b4f883272d57ecef8d0bf6a8098da4cf52b94",
+ "source": "txline",
+ "role": "signal",
+ "sourceTsMs": 1699999999936,
+ "observedAtTsMs": 1700000000000,
+ "payloadSha256": "078399ecb1eedf610fda560c31c8268d71bd8aa0967847f8d46a15397de973df",
+ "disclosure": "hash_only"
+ },
+ {
+ "evidenceRefSha256": "ce70188de2a3ec2c4856b9e7d453b43588ffba88d4f0cf1a54fb15264bc7f400",
+ "source": "polymarket",
+ "role": "signal",
+ "sourceTsMs": 1699999999975,
+ "observedAtTsMs": 1700000000000,
+ "payloadSha256": "c0cbf1028f7d99e2c800c770a9f19edd2630fa7b45cd70d8c2c70137a2f87198",
+ "disclosure": "hash_only"
+ },
+ {
+ "evidenceRefSha256": "0970a82a4d74742708a5ea39e895d7641ce52f92248629afa9280af952808fb0",
+ "source": "polymarket",
+ "role": "execution",
+ "sourceTsMs": 1700000002436,
+ "observedAtTsMs": 1700000002500,
+ "payloadSha256": "dcabb052a305c6f33bf6d019952f4ee8d3e0e57444750ecec411e37b7235fac8",
+ "disclosure": "hash_only"
+ },
+ {
+ "evidenceRefSha256": "cd3a2bb43870e3e037901d91818d22e90271a05248aae7871e851c3b9e10419e",
+ "source": "polymarket",
+ "role": "close",
+ "sourceTsMs": 1700000004930,
+ "observedAtTsMs": 1700000005000,
+ "payloadSha256": "0c45d0ab522c33c8faa6557bcadbd66f69170e1761d6ec550eb60123553754e4",
+ "disclosure": "hash_only"
+ },
+ {
+ "evidenceRefSha256": "807120b1be8e13a10652816d734233b44ea436d2fa190069dc2f72dbb90d2098",
+ "source": "polymarket",
+ "role": "settlement",
+ "sourceTsMs": 1700000009950,
+ "observedAtTsMs": 1700000010000,
+ "payloadSha256": "ce14207457515b49d6df821e29d77c2f1820e1b40c359cd5f831d9922ad86ab3",
+ "disclosure": "hash_only"
+ }
+ ],
+ "agents": {
+ "runs": [
+ {
+ "stage": "triage",
+ "invocationClass": "synthetic_stub",
+ "model": "claude-haiku-4-5",
+ "promptVersion": "triage-v1",
+ "promptSha256": "5165e7052e4f78f3584a2bf8deb109fea2aa2298e05618c3ae62c8759ae8f4d1",
+ "responseSha256": "e306c8b5f4023e537c970fda834c17fe50623f07205c786326e0bea18fc91c20",
+ "billingEvidenceRefSha256": "44d47f382a3be238d8620750a147cff95aff18ff484e944c974ce5987e0e495f",
+ "status": "success",
+ "usage": {
+ "inputTokens": 0,
+ "outputTokens": 0,
+ "cacheCreationInputTokens": 0,
+ "cacheReadInputTokens": 0
+ },
+ "pricing": {
+ "pricingVersion": "anthropic-public-2026-07-12",
+ "currency": "nano_usd",
+ "inputNanoUsdPerToken": 1000,
+ "outputNanoUsdPerToken": 5000,
+ "cacheWriteNanoUsdPerToken": 1250,
+ "cacheReadNanoUsdPerToken": 100
+ },
+ "actualCostNanoUsd": 0
+ },
+ {
+ "stage": "analyst",
+ "invocationClass": "synthetic_stub",
+ "model": "claude-opus-4-8",
+ "promptVersion": "analyst-v1",
+ "promptSha256": "a19775689dfb6bf5032466a6716c0c0a5e4d8358d931bd7869bd10f4b9682523",
+ "responseSha256": "1e3d645c1e9e6c1b8f748b6f2a209ddd22699a0623029355830ea1f3638688a9",
+ "billingEvidenceRefSha256": "47744ac9aab2b40c8deb13f910efc36f61d3204f2157bc06d39a0b502ec6d27e",
+ "status": "success",
+ "usage": {
+ "inputTokens": 0,
+ "outputTokens": 0,
+ "cacheCreationInputTokens": 0,
+ "cacheReadInputTokens": 0
+ },
+ "pricing": {
+ "pricingVersion": "anthropic-public-2026-07-12",
+ "currency": "nano_usd",
+ "inputNanoUsdPerToken": 5000,
+ "outputNanoUsdPerToken": 25000,
+ "cacheWriteNanoUsdPerToken": 6250,
+ "cacheReadNanoUsdPerToken": 500
+ },
+ "actualCostNanoUsd": 0
+ }
+ ],
+ "totalActualCostNanoUsd": 0
+ },
+ "lifecycle": {
+ "finalStatus": "filled_settled",
+ "orderedEventKinds": [
+ "signal_received",
+ "triage_decision",
+ "thesis_submitted",
+ "analysis_completed",
+ "risk_verdict",
+ "execution_intent",
+ "paper_execution",
+ "case_terminal",
+ "position_opened",
+ "position_closed",
+ "position_settled"
+ ],
+ "signal": {
+ "signalRefSha256": "fc61ba747574e3c953a6dd799c50f899300dcf50003fd617639f278d843d882e",
+ "fixtureRefSha256": "1f57de0cedca3e1851b4c05140408d70769031423aa5aa6a93b22e75b036e762",
+ "marketRefSha256": "80b77e4246270ab67aae53c0f61c60545f8b738c7dccf88d7969575bfeae7acb",
+ "lineRefSha256": "fe82e6baa7be838b672749b06987ad38a401fc72651a530e732066bc0daffc86",
+ "detector": "CONSENSUS_MOVE",
+ "sourceTsMs": 1700000000000,
+ "observedAtTsMs": 1700000000000,
+ "marketFamily": "total_goals",
+ "marketPeriod": "full_time",
+ "outcome": "over",
+ "direction": "buy",
+ "eligibility": "pretrade_review_required",
+ "derivedEvidence": {
+ "txlineMovementBucketBps": 25,
+ "consensusVelocityBucketBps": 100,
+ "consensusZScore": 1.5,
+ "polymarketVelocityBucketBps": 0,
+ "polymarketZScore": 0,
+ "cusumUpScore": 0.002,
+ "cusumDownScore": 0,
+ "relativeValueDirection": "consensus_above_venue",
+ "gapBasis": "live_book",
+ "persistenceMs": 60000,
+ "mappingStatus": "verified",
+ "scoreContextActionCount": 0
+ },
+ "sourceEvidenceRefs": [
+ "3d3b41a2227873d8b21248202f7b4f883272d57ecef8d0bf6a8098da4cf52b94",
+ "ce70188de2a3ec2c4856b9e7d453b43588ffba88d4f0cf1a54fb15264bc7f400"
+ ],
+ "ledgerEntryHash": "3a64f4905aea5c1c02f7757cbaf2f7368d0d31e1a176cd66f1f66cc2ccdf7368"
+ },
+ "triage": {
+ "decision": "escalate",
+ "priority": "normal",
+ "rationale": "Synthetic evidence satisfies the proving-fixture escalation path.",
+ "ledgerEntryHash": "6f3d93978471a7af150ee5c97beafb19ae0924c195ae5ee1c62451943f8d51d8"
+ },
+ "thesis": {
+ "schemaVersion": 1,
+ "signalRefSha256": "fc61ba747574e3c953a6dd799c50f899300dcf50003fd617639f278d843d882e",
+ "fixtureRefSha256": "1f57de0cedca3e1851b4c05140408d70769031423aa5aa6a93b22e75b036e762",
+ "marketRefSha256": "80b77e4246270ab67aae53c0f61c60545f8b738c7dccf88d7969575bfeae7acb",
+ "outcome": "over",
+ "direction": "buy",
+ "recommendation": "paper_trade",
+ "disclosure": "private_payload_sha256_commitment",
+ "thesisPayloadSha256": "ab033dcc7a1f947427915746765c84e69ff0e972fd860e79907e7a7a3a6dc51b",
+ "submittedAtTsMs": 1700000000300,
+ "expiresAtTsMs": 1700000900300,
+ "analystModel": "claude-opus-4-8",
+ "ledgerEntryHash": "7b7aacab586ae9586697612d6ee755121cd66d432df402e49b4896e0fc4976c4"
+ },
+ "analysis": {
+ "signalSourceTsMs": 1700000000000,
+ "signalObservedTsMs": 1700000000000,
+ "decisionLatencyMs": 300,
+ "readyAtTsMs": 1700000000300,
+ "venuePlacementDelayMs": 1000,
+ "orderEligibleAtTsMs": 1700000001300,
+ "recommendation": "paper_trade",
+ "ledgerEntryHash": "893f6021f0a397dea91f50e98911d9526213759c57905aecb2257f6332446d00"
+ },
+ "risk": {
+ "decision": "approve",
+ "reasons": [],
+ "stakeMicroUsd": 3000000,
+ "limitPolicy": "deterministic_private_fair_value_boundary",
+ "realMoneyGate": "closed",
+ "ledgerEntryHash": "8cd81eb389ee4a83289158e814fc0ede93b6e0805a09c1dbd159ddeea586c7c4"
+ },
+ "intent": {
+ "lane": "bounty",
+ "signalRefSha256": "fc61ba747574e3c953a6dd799c50f899300dcf50003fd617639f278d843d882e",
+ "fixtureRefSha256": "1f57de0cedca3e1851b4c05140408d70769031423aa5aa6a93b22e75b036e762",
+ "marketRefSha256": "80b77e4246270ab67aae53c0f61c60545f8b738c7dccf88d7969575bfeae7acb",
+ "outcome": "over",
+ "direction": "buy",
+ "stakeMicroUsd": 3000000,
+ "limitPolicy": "deterministic_private_fair_value_boundary",
+ "availableShares": 0,
+ "ledgerEntryHash": "212be5d2c3bdd268bf14b7f9545e7b41224baa2fc61199bd7aae36a36f053c35"
+ },
+ "execution": {
+ "adapter": "paper",
+ "status": "filled",
+ "reason": null,
+ "assetRefSha256": "165750a05c5b4d223975cbfc4c1e6181b052db4bdacd5f93edf445422575e396",
+ "conditionRefSha256": "2e9568f28fc6d4d1f026500f835047b5e096483bd97d428b78cf187e0bc537a2",
+ "direction": "buy",
+ "requestedStakeMicroUsd": 3000000,
+ "grossMicroUsd": 2930000,
+ "feeMicroUsd": 70000,
+ "netConsiderationMicroUsd": 3000000,
+ "filledShares": 5.5,
+ "averagePrice": 0.5327272727272727,
+ "bestPrice": 0.52,
+ "halfSpreadBps": 100,
+ "executableDepthUsd": 10,
+ "slippageProbabilityBps": 127.27272727272698,
+ "bookSourceTsMs": 1700000002436,
+ "bookObservedTsMs": 1700000002500,
+ "bookEvidenceRefSha256": "0970a82a4d74742708a5ea39e895d7641ce52f92248629afa9280af952808fb0",
+ "feeMetadataRefSha256": "494da64419219a3b8d4694bc01b1f8861f596d2fe62335799c15c5ec7c0fc575",
+ "ledgerEntryHash": "facca01a6fcafd86b04cd1dccd12167842f12be43ab53e2b7c083d4aafbe43df"
+ },
+ "position": {
+ "lane": "bounty",
+ "signalRefSha256": "fc61ba747574e3c953a6dd799c50f899300dcf50003fd617639f278d843d882e",
+ "fixtureRefSha256": "1f57de0cedca3e1851b4c05140408d70769031423aa5aa6a93b22e75b036e762",
+ "marketRefSha256": "80b77e4246270ab67aae53c0f61c60545f8b738c7dccf88d7969575bfeae7acb",
+ "conditionRefSha256": "2e9568f28fc6d4d1f026500f835047b5e096483bd97d428b78cf187e0bc537a2",
+ "assetRefSha256": "165750a05c5b4d223975cbfc4c1e6181b052db4bdacd5f93edf445422575e396",
+ "outcome": "over",
+ "lineRefSha256": "fe82e6baa7be838b672749b06987ad38a401fc72651a530e732066bc0daffc86",
+ "openedAtTsMs": 1700000002500,
+ "filledShares": 5.5,
+ "averageEntryPrice": 0.5327272727272727,
+ "entryGrossMicroUsd": 2930000,
+ "entryFeeMicroUsd": 70000,
+ "entryCostMicroUsd": 3000000,
+ "fillStatus": "filled",
+ "entryHalfSpreadBps": 100,
+ "entrySlippageBps": 127.27272727272698,
+ "selectedDepthUsd": 10,
+ "ledgerEntryHash": "6297be56975fc74ea180ee15b23bad116b54236d5f3b3d4c507881dc8edeb084"
+ },
+ "close": {
+ "cutoffTsMs": 1700000005000,
+ "markedAtTsMs": 1700000005000,
+ "bookSourceTsMs": 1700000004930,
+ "bookObservedTsMs": 1700000005000,
+ "closeBid": 0.57,
+ "closeAsk": 0.59,
+ "closeMidpoint": 0.58,
+ "grossMidpointClvBps": 472.72727272727246,
+ "netMidpointClvBps": 345.45454545454544,
+ "executableLiquidationClvBps": 245.45454545454538,
+ "bookEvidenceRefSha256": "cd3a2bb43870e3e037901d91818d22e90271a05248aae7871e851c3b9e10419e",
+ "ledgerEntryHash": "3945a8cf179a117908d99d424d6a1286449cb5571050e810285422c98f3b4259"
+ },
+ "settlement": {
+ "settledAtTsMs": 1700000010000,
+ "won": true,
+ "payoutMicroUsd": 5500000,
+ "pnlMicroUsd": 2500000,
+ "returnBps": 8333.333333333334,
+ "entryBrier": 0.21834380165289258,
+ "resolutionEvidenceRefSha256": "807120b1be8e13a10652816d734233b44ea436d2fa190069dc2f72dbb90d2098",
+ "ledgerEntryHash": "ebadd4b57d1e88a24df104f03918fb8b20e86f51fe17ab913b802f7f13bd31a7"
+ },
+ "terminal": {
+ "status": "filled",
+ "reason": "filled",
+ "decisionLatencyMs": null,
+ "ledgerEntryHash": "ec2db14205262aaa9f4b4c95128db37a389cbe7c33cafd5ba63d0600cb684a62"
+ }
+ },
+ "ledger": {
+ "verificationAtGeneration": "full_v2_chain_valid",
+ "payloadDisclosure": "sha256_commitments_only",
+ "caseIdSha256": "becca5114c0773541b8cc0f18ffd18c67a140a2e863e5a598f5d68d2fbb9036c",
+ "rowsAtGeneration": 12,
+ "finalHeadSequence": 12,
+ "finalHeadHash": "ebadd4b57d1e88a24df104f03918fb8b20e86f51fe17ab913b802f7f13bd31a7",
+ "caseEntries": [
+ {
+ "sequence": 2,
+ "kind": "signal_received",
+ "atTsMs": 1700000000000,
+ "insertedAtMs": 1700000000010,
+ "entryIdSha256": "6c9d8192370ebb7c2a718b0711688e2af9e008d2b4d76cc51fa045e28440da97",
+ "payloadSha256": "f895ad3acba6a4444dfefc1c704d43aa90eb4d45d24d93d89984a8f12e9b54c3",
+ "previousHash": "6449eb53a31b87ec0d7b9f49a59801b5dc677be5b48740006139003c6c309c5a",
+ "entryHash": "3a64f4905aea5c1c02f7757cbaf2f7368d0d31e1a176cd66f1f66cc2ccdf7368",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 3,
+ "kind": "triage_decision",
+ "atTsMs": 1700000000100,
+ "insertedAtMs": 1700000000110,
+ "entryIdSha256": "87d0950aa0f9c4604d4a4a9b6c2e05e5745ba6db8a33ab5c702989070ca74b42",
+ "payloadSha256": "300be11c438b40f10336d7701903465e0f0d684916445996aa599cb45bcb279d",
+ "previousHash": "3a64f4905aea5c1c02f7757cbaf2f7368d0d31e1a176cd66f1f66cc2ccdf7368",
+ "entryHash": "6f3d93978471a7af150ee5c97beafb19ae0924c195ae5ee1c62451943f8d51d8",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 4,
+ "kind": "thesis_submitted",
+ "atTsMs": 1700000000300,
+ "insertedAtMs": 1700000000310,
+ "entryIdSha256": "58a72d3a0bdedb06f37877fe17db18b92aef7201c7f1243a49676274e5121c27",
+ "payloadSha256": "ab033dcc7a1f947427915746765c84e69ff0e972fd860e79907e7a7a3a6dc51b",
+ "previousHash": "6f3d93978471a7af150ee5c97beafb19ae0924c195ae5ee1c62451943f8d51d8",
+ "entryHash": "7b7aacab586ae9586697612d6ee755121cd66d432df402e49b4896e0fc4976c4",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 5,
+ "kind": "analysis_completed",
+ "atTsMs": 1700000000300,
+ "insertedAtMs": 1700000000310,
+ "entryIdSha256": "3b1dc21e93430e1246a0e8ffeeb94825829c85afa99c62ff54bb290a6bd1e4b2",
+ "payloadSha256": "3189b0911b5e14ed47290521b1eb68fcd2dc03d992ac05b4dfdc3ea14e089854",
+ "previousHash": "7b7aacab586ae9586697612d6ee755121cd66d432df402e49b4896e0fc4976c4",
+ "entryHash": "893f6021f0a397dea91f50e98911d9526213759c57905aecb2257f6332446d00",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 6,
+ "kind": "risk_verdict",
+ "atTsMs": 1700000002500,
+ "insertedAtMs": 1700000002510,
+ "entryIdSha256": "1941c96d23ce7aa3395a1da9922bbbb29637b71253a764d4d690debaf3194cd6",
+ "payloadSha256": "72951bf422061213048536dc8b3c85d4109f0f1b6bfb218f1667324c53933a46",
+ "previousHash": "893f6021f0a397dea91f50e98911d9526213759c57905aecb2257f6332446d00",
+ "entryHash": "8cd81eb389ee4a83289158e814fc0ede93b6e0805a09c1dbd159ddeea586c7c4",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 7,
+ "kind": "execution_intent",
+ "atTsMs": 1700000002500,
+ "insertedAtMs": 1700000002510,
+ "entryIdSha256": "966bf1628d1cb4a27d8d108aa8a51d45a16cd1407b0484a7e9d301c1ea637999",
+ "payloadSha256": "095dab4ca494ffe658df7a40270d7dbb2b75ac477b29690fdcf3c36e9caade70",
+ "previousHash": "8cd81eb389ee4a83289158e814fc0ede93b6e0805a09c1dbd159ddeea586c7c4",
+ "entryHash": "212be5d2c3bdd268bf14b7f9545e7b41224baa2fc61199bd7aae36a36f053c35",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 8,
+ "kind": "paper_execution",
+ "atTsMs": 1700000002500,
+ "insertedAtMs": 1700000002510,
+ "entryIdSha256": "37a8d3105fc423efed982b005439833253f1b93a66804cec9dcf3dc9f7d1db08",
+ "payloadSha256": "324f7b7da1190467c8f5fbe4ec5ed5e381d85c7bd216464fcc778da794a89367",
+ "previousHash": "212be5d2c3bdd268bf14b7f9545e7b41224baa2fc61199bd7aae36a36f053c35",
+ "entryHash": "facca01a6fcafd86b04cd1dccd12167842f12be43ab53e2b7c083d4aafbe43df",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 9,
+ "kind": "case_terminal",
+ "atTsMs": 1700000002500,
+ "insertedAtMs": 1700000002510,
+ "entryIdSha256": "ed60575ca0a5c9123fd8ef9cb59c314f051ed89d4bc5157addc71bf2c652bfa6",
+ "payloadSha256": "a279a2a5d4bef14c5f523f7145bcdbd762a9e4ff6bee856ccf4d3214bacc4ead",
+ "previousHash": "facca01a6fcafd86b04cd1dccd12167842f12be43ab53e2b7c083d4aafbe43df",
+ "entryHash": "ec2db14205262aaa9f4b4c95128db37a389cbe7c33cafd5ba63d0600cb684a62",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 10,
+ "kind": "position_opened",
+ "atTsMs": 1700000002500,
+ "insertedAtMs": 1700000002510,
+ "entryIdSha256": "68b0696dffcec92707ae1ea69bb2637f847c2783f8537fc35f78b62733d7be3d",
+ "payloadSha256": "424243b8b05516adb7e51afaced01b0cde62687dd39fb073a66f2974778dbf10",
+ "previousHash": "ec2db14205262aaa9f4b4c95128db37a389cbe7c33cafd5ba63d0600cb684a62",
+ "entryHash": "6297be56975fc74ea180ee15b23bad116b54236d5f3b3d4c507881dc8edeb084",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 11,
+ "kind": "position_closed",
+ "atTsMs": 1700000005000,
+ "insertedAtMs": 1700000005010,
+ "entryIdSha256": "6c93e6fc6e7ca5da3c581fff3cd76667a94c998d7a74383bea0521143fbf7fd5",
+ "payloadSha256": "265c994a18a9a07af46b05896b67aea15953a7e608cf192bfe4023fa087536c8",
+ "previousHash": "6297be56975fc74ea180ee15b23bad116b54236d5f3b3d4c507881dc8edeb084",
+ "entryHash": "3945a8cf179a117908d99d424d6a1286449cb5571050e810285422c98f3b4259",
+ "hashSchemaVersion": 2
+ },
+ {
+ "sequence": 12,
+ "kind": "position_settled",
+ "atTsMs": 1700000010000,
+ "insertedAtMs": 1700000010010,
+ "entryIdSha256": "6e4fc49c621a5b8ec1f25ec148177769ba555797f8f79c235ded44212aafd87a",
+ "payloadSha256": "7bd702c62fe1074025ac0f969eeeff947e700bb66b997221546c183b16179fcb",
+ "previousHash": "3945a8cf179a117908d99d424d6a1286449cb5571050e810285422c98f3b4259",
+ "entryHash": "ebadd4b57d1e88a24df104f03918fb8b20e86f51fe17ab913b802f7f13bd31a7",
+ "hashSchemaVersion": 2
+ }
+ ]
+ },
+ "solanaAnchor": null,
+ "integrity": {
+ "algorithm": "sha256",
+ "canonicalization": "samaritan-stable-json-v1",
+ "domain": "samaritan.decision-receipt/v1",
+ "receiptHash": "5ecddb4707e2d95a231ac8039ea27c318f9f4159da77d4e33a88c4897bd21207"
+ }
+}
diff --git a/public/artifacts/dashboard/casebook.json b/public/artifacts/dashboard/casebook.json
new file mode 100644
index 0000000..4153b18
--- /dev/null
+++ b/public/artifacts/dashboard/casebook.json
@@ -0,0 +1 @@
+{"data":{"cases":[{"awayCode":"BEL","caseId":"SB-20260710-G01-MR","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":null,"marketFamily":"Match result","marketLabel":"Match result","matchroomId":"paired-spain-belgium-2026-07-10","occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":775,"reason":"Market moved before TXLine","selectedExemplar":true,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G01-TG-500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 0.5","matchroomId":null,"occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":163,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G01-TG-1500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":1500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 1.5","matchroomId":null,"occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":537,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G01-TG-2500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":2500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 2.5","matchroomId":null,"occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":837,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G01-TG-3500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":3500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 3.5","matchroomId":null,"occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":600,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G01-TG-4500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":4500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 4.5","matchroomId":null,"occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":800,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-MR","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":null,"marketFamily":"Match result","marketLabel":"Match result","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":575,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-TG-500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 0.5","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":0,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-TG-1500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":1500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 1.5","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":538,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-TG-2500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":2500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 2.5","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":575,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-TG-3500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":3500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 3.5","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":638,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G02-TG-4500","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":2411,"goalOrdinal":2,"homeCode":"ESP","lineMilli":4500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 4.5","matchroomId":null,"occurredAt":"2026-07-10T19:40:50.195Z","preTriggerMarketMoveBps":825,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-MR","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":null,"marketFamily":"Match result","marketLabel":"Match result","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":50,"reason":"Market moved before TXLine","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-TG-500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 0.5","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":0,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-TG-1500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":1500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 1.5","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":0,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-TG-2500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":2500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 2.5","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":0,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-TG-3500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":3500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 3.5","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":13,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"},{"awayCode":"BEL","caseId":"SB-20260710-G03-TG-4500","classification":"no_material_reprice_in_window","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":5244,"goalOrdinal":3,"homeCode":"ESP","lineMilli":4500,"marketFamily":"Full-time total","marketLabel":"Full-time total · O/U 4.5","matchroomId":null,"occurredAt":"2026-07-10T20:49:37.081Z","preTriggerMarketMoveBps":0,"reason":"No material move within 30s","selectedExemplar":false,"source":"Captured replay","verificationStatus":"Internally reconciled"}],"corpus":{"assurance":"local_file_sha256_not_capture_manifest_membership","captureReplays":1,"cleanStaleWindows":0,"commitment":"0c852ded55fbd4acec5e93d3ee4e1ae78f475c9a02837a68f1af1ef695a42672","coverage":"all_reported_goal_market_cases","fixtureCount":1,"goalEvents":3,"marketEventCases":18,"movedBeforeTxlineCases":12,"noMaterialRepriceCases":6,"selectedExemplar":{"caseId":"SB-20260710-G01-MR","detail":"The detail pane is one deterministic Match Result exemplar; the index still contains every reported goal×market observation.","policy":"earliest_pretrigger_match_result_then_largest_pretrigger_ask_move"},"unit":"goal_market_feasibility_observation"},"executionMode":"paper","filterOptions":{"detectors":["STALE_QUOTE_FEASIBILITY"],"dispositions":["No trade"],"evidenceLanes":["Research only"],"executionOutcomes":["Not executed"],"fixtures":["Spain vs Belgium"],"marketFamilies":["Full-time total","Match result"],"sources":["Captured replay"]},"generatedAt":"2026-07-14T23:13:44.692Z","mode":"offline_artifact","nextEvidence":{"detail":"New observations appear only after another paired capture passes identity, completeness, and replay verification.","label":"Current 18-observation corpus is complete","status":"waiting_for_verified_capture"},"publicDataPolicy":{"credentialsRequired":false,"derivedOnly":true,"txlineFixtureIdentifiersExposed":false,"txlineMovementBucketBps":25,"txlineProbabilityDisplay":"bucketed_movement_only","walletControlsExposed":false},"realMoneyGate":"closed","schemaVersion":3,"selectedCase":{"analysis":{"costMicros":0,"costReason":"Execution was outside this retrospective research lane, so no operational cost calculation ran.","costStatus":"not_applicable","invalidation":"The apparent gap is invalid if the executable market repriced before the signal reached Samaritan.","thesisReason":"The deterministic evidence gate rejected the opportunity before analyst escalation.","thesisStatus":"not_requested"},"decision":{"capitalMovedMicros":0,"disposition":"no_trade","explanation":"The candidate arrived after the market had already moved. This retrospective feasibility observation never entered an execution runtime.","label":"No trade","ordersPlaced":0,"primaryReason":"Market moved before signal","semanticStatus":"disciplined_pass","stages":[{"detail":"STALE_QUOTE feasibility · research only","id":"signal","label":"Goal observed","status":"complete","timingLabel":"+0ms"},{"detail":"First material update","id":"evidence","label":"Evidence checked","status":"complete","timingLabel":"+228ms"},{"detail":"No clean stale window","id":"pass","label":"Opportunity passed","status":"passed","timingLabel":"Final"},{"detail":"Research path ended before an execution runtime","id":"execution","label":"Execution not reached","status":"locked","timingLabel":"N/A"}],"walletAccessed":false},"evidence":[{"assessment":"Moved first","bestAsk":0.26,"observation":"Draw ask begins repricing","observedAt":"2026-07-10T19:29:55.389Z","offsetLabel":"T−5.000s","replayStateId":"pre","source":"Polymarket"},{"assessment":"Pass","bestAsk":0.18,"observation":"Spain goal delivered at 29:21","observedAt":"2026-07-10T19:30:00.389Z","offsetLabel":"First seen","replayStateId":"goal","source":"TXLine"},{"assessment":"No trade","bestAsk":0.1625,"observation":"Spread tightens after repricing","observedAt":"2026-07-10T19:30:30.389Z","offsetLabel":"T+30.000s","replayStateId":"post","source":"Samaritan"}],"evidenceReadout":{"bestAsk":0.18,"bestBid":0.17,"consensusMoveFromBaselineBps":25,"movementConclusion":"Polymarket moved before TXLine","preTriggerMarketMoveBps":775,"spread":0.009999999999999981},"lifecycle":[{"detail":"STALE_QUOTE feasibility · research only","id":"signal","label":"Goal observed","status":"complete","timingLabel":"+0ms"},{"detail":"First material update","id":"evidence","label":"Evidence checked","status":"complete","timingLabel":"+228ms"},{"detail":"No clean stale window","id":"pass","label":"Opportunity passed","status":"passed","timingLabel":"Final"},{"detail":"Research path ended before an execution runtime","id":"execution","label":"Execution not reached","status":"locked","timingLabel":"N/A"}],"match":{"away":{"code":"BEL","name":"Belgium"},"clockLabel":"29:21","clockSeconds":1761,"competition":"World Cup","eventSlug":"fifwc-esp-bel-2026-07-10","fixtureRef":"paired-spain-belgium-2026-07-10","goalOrdinal":1,"home":{"code":"ESP","name":"Spain"},"kickoffUtc":"2026-07-10T19:00:00.000Z","originalMatchDate":"2026-07-10","scoreAtCursor":{"away":0,"home":1},"stage":"Captured fixture"},"proof":{"canonicalEvents":2470342,"captureStatus":"verified","cleanStaleWindows":0,"corpusAssurance":"local_file_sha256_not_capture_manifest_membership","corpusCommitment":"0c852ded55fbd4acec5e93d3ee4e1ae78f475c9a02837a68f1af1ef695a42672","feedDowntimeMs":91171,"feedOutageCount":8,"gateCases":18,"headHash":"55b74d2f769bb40b822c02332bf94cc9af645185952dbbe7d115e6dac20ae05b","identityHash":"d7958c89929f2742e42b77ef2a4fb7f8df744f40dc3240e80bf724695eef4260","identityParity":true,"maximumFeedDowntimeMs":25375,"movedBeforeTxlineCases":12,"noMaterialRepriceCases":6,"replayMode":"capture-order-per-source"},"summary":{"awayCode":"BEL","caseId":"SB-20260710-G01-MR","classification":"polymarket_moved_before_txline","detector":"STALE_QUOTE_FEASIBILITY","disposition":"No trade","evidenceLane":"Research only","executionOutcome":"Not executed","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","goalClockSeconds":1761,"goalOrdinal":1,"homeCode":"ESP","lineMilli":null,"marketFamily":"Match result","marketLabel":"Match result","matchroomId":"paired-spain-belgium-2026-07-10","occurredAt":"2026-07-10T19:30:00.389Z","preTriggerMarketMoveBps":775,"reason":"Market moved before TXLine","selectedExemplar":true,"source":"Captured replay","verificationStatus":"Internally reconciled"}},"snapshotId":"casebook-2026-world-cup","statistics":{"capitalMovedMicros":0,"executedCases":0,"noTradeCases":18,"reconciledCases":18,"totalCases":18},"tradeable":false}}
diff --git a/public/artifacts/dashboard/command.json b/public/artifacts/dashboard/command.json
new file mode 100644
index 0000000..3f3436a
--- /dev/null
+++ b/public/artifacts/dashboard/command.json
@@ -0,0 +1 @@
+{"data":{"additionalCaseState":{"detail":"Registration is active for forward paper only; zero observations qualify until a fresh fixture passes every admission gate.","label":"Registered v2 awaits fresh eligible evidence","status":"waiting_for_eligible_capture"},"executionMode":"paper","featuredCase":{"away":{"code":"BEL","name":"Belgium"},"bestAsk":0.18,"candidateLabel":"Live-lane gate readout","canonicalEvents":2470342,"capitalMovedMicros":0,"caseId":"SB-20260710-G01-MR","chart":[{"available":true,"bestAsk":0.26,"bestBid":0.245,"offsetMs":-5000,"spread":0.015000000000000013},{"available":true,"bestAsk":0.205,"bestBid":0.17,"offsetMs":-1000,"spread":0.034999999999999976},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":0,"spread":0.009999999999999981},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":250,"spread":0.009999999999999981},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":500,"spread":0.009999999999999981},{"available":true,"bestAsk":0.1775,"bestBid":0.1525,"offsetMs":1000,"spread":0.024999999999999994},{"available":true,"bestAsk":0.1775,"bestBid":0.16,"offsetMs":2000,"spread":0.017499999999999988},{"available":true,"bestAsk":0.1675,"bestBid":0.16,"offsetMs":5000,"spread":0.007500000000000007},{"available":true,"bestAsk":0.1725,"bestBid":0.17,"offsetMs":10000,"spread":0.0024999999999999745},{"available":true,"bestAsk":0.1625,"bestBid":0.16,"offsetMs":30000,"spread":0.0025000000000000022}],"clockLabel":"29:21","clockSeconds":1761,"conclusion":"The candidate arrived after the market had already moved. This retrospective feasibility observation never entered an execution runtime.","consensusMoveFromBaselineBps":25,"disposition":"no_trade","dispositionLabel":"No trade","evidenceStatus":"verified_replay","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","home":{"code":"ESP","name":"Spain"},"identityParity":true,"marketLabel":"Match result · Draw","marketOutcomeLabel":"draw","matchroomId":"paired-spain-belgium-2026-07-10","occurredAt":"2026-07-10T19:30:00.389Z","ordersPlaced":0,"preTriggerMarketMoveBps":775,"reason":"Market moved before signal","scoreAtCursor":{"away":0,"home":1},"scoreLabel":"1–0","walletAccessed":false},"fixtureSchedule":[{"away":{"code":"ESP","name":"Spain"},"captureEndUtc":"2026-07-14T22:00:00.000Z","captureId":"paired-france-spain-2026-07-14","captureOnly":true,"captureStartUtc":"2026-07-14T16:00:00.000Z","eventSlug":"fifwc-fra-esp-2026-07-14","home":{"code":"FRA","name":"France"},"identityStatus":"historical_reviewed_config","kickoffUtc":"2026-07-14T19:00:00.000Z","phase":"failed","signalCutoffUtc":"2026-07-14T18:45:00.000Z","statusDetail":"6 verification failures recorded","statusLabel":"Failed closed","statusSource":"analysis_manifest","statusUpdatedAt":"2026-07-15T00:23:23.588Z","terminalEvidence":null,"tradeable":false},{"away":{"code":"ARG","name":"Argentina"},"captureEndUtc":"2026-07-15T22:00:00.000Z","captureId":"paired-england-argentina-2026-07-15","captureOnly":true,"captureStartUtc":"2026-07-15T16:00:00.000Z","eventSlug":"fifwc-eng-arg-2026-07-15","home":{"code":"ENG","name":"England"},"identityStatus":"historical_reviewed_config","kickoffUtc":"2026-07-15T19:00:00.000Z","phase":"failed","signalCutoffUtc":"2026-07-15T18:45:00.000Z","statusDetail":"1 verification failure recorded","statusLabel":"Failed closed","statusSource":"analysis_manifest","statusUpdatedAt":"2026-07-16T00:17:14.911Z","terminalEvidence":null,"tradeable":false},{"away":{"code":"ENG","name":"England"},"captureEndUtc":"2026-07-19T00:00:00.000Z","captureId":"paired-france-england-2026-07-18","captureOnly":true,"captureStartUtc":"2026-07-18T18:00:00.000Z","eventSlug":"fifwc-fra-eng-2026-07-18","home":{"code":"FRA","name":"France"},"identityStatus":"exact_match_confirmed","kickoffUtc":"2026-07-18T21:00:00.000Z","phase":"scheduled","signalCutoffUtc":"2026-07-18T20:45:00.000Z","statusDetail":"Scheduled state recorded by the local supervisor","statusLabel":"Capture scheduled","statusSource":"supervisor_status","statusUpdatedAt":"2026-07-18T07:16:05.205Z","terminalEvidence":null,"tradeable":false},{"away":{"code":"ARG","name":"Argentina"},"captureEndUtc":"2026-07-19T22:00:00.000Z","captureId":"paired-spain-argentina-2026-07-19","captureOnly":true,"captureStartUtc":"2026-07-19T16:00:00.000Z","eventSlug":"fifwc-esp-arg-2026-07-19","home":{"code":"ESP","name":"Spain"},"identityStatus":"exact_match_confirmed","kickoffUtc":"2026-07-19T19:00:00.000Z","phase":"scheduled","signalCutoffUtc":"2026-07-19T18:45:00.000Z","statusDetail":"Scheduled state recorded by the local supervisor","statusLabel":"Capture scheduled","statusSource":"supervisor_status","statusUpdatedAt":"2026-07-18T07:23:19.375Z","terminalEvidence":null,"tradeable":false}],"generatedAt":"2026-07-18T15:01:00.000Z","mode":"offline_artifact","proof":{"bountyLedgerRows":1,"bountyLedgerValid":true,"canonicalEvents":2470342,"evidenceFixtures":3,"longRunLedgerRows":1,"longRunLedgerValid":true,"pairedBookReplays":1,"replayIdentityHash":"d7958c89929f2742e42b77ef2a4fb7f8df744f40dc3240e80bf724695eef4260","replayIdentityParity":true,"signalResearchOnly":2},"publicDataPolicy":{"credentialsRequired":false,"derivedOnly":true,"txlineFixtureIdentifiersExposed":false,"txlineMovementBucketBps":25,"txlineProbabilityDisplay":"bucketed_movement_only","walletControlsExposed":false},"realMoneyGate":"closed","recentCases":[{"away":{"code":"BEL","name":"Belgium"},"bestAsk":0.18,"candidateLabel":"Live-lane gate readout","canonicalEvents":2470342,"capitalMovedMicros":0,"caseId":"SB-20260710-G01-MR","consensusMoveFromBaselineBps":25,"disposition":"no_trade","dispositionLabel":"No trade","evidenceStatus":"verified_replay","fixtureLabel":"Spain vs Belgium","fixtureRef":"paired-spain-belgium-2026-07-10","home":{"code":"ESP","name":"Spain"},"marketLabel":"Match result · Draw","marketOutcomeLabel":"draw","matchroomId":"paired-spain-belgium-2026-07-10","occurredAt":"2026-07-10T19:30:00.389Z","ordersPlaced":0,"preTriggerMarketMoveBps":775,"reason":"Market moved before signal","walletAccessed":false}],"schemaVersion":3,"snapshotId":"command-2026-world-cup","sourceFreshness":{"fixtureUniverseGeneratedAt":"2026-07-18T07:17:07.354Z","paperReportGeneratedAt":"2026-07-18T07:17:27.184Z","replayGeneratedAt":"2026-07-14T23:13:44.692Z"},"study":{"bountyStatus":"exploratory","configHash":"93e61c1903d0a13bbeb1dbbd3ad9b11af0335b96c82bd2ca7aa9ddedeeabf3ce","historicalV1":{"configHash":"7dcb4c20ac05195bdb4a6c8803432079eeefb8cc747c186f576084dc772fd2a2","protocolStatus":"invalidated_suspended","protocolVersion":"paper-study-v1-2026-07-12","startedAt":"2026-07-12T09:45:17.212Z","zeroObservationAudit":true},"observationStatus":"awaiting_fresh_evidence","protocolStatus":"registered","protocolVersion":"paper-study-v2-2026-07-18","qualifyingCounts":{"filledMatches":0,"fills":0,"matches":0,"settledFills":0,"signals":0},"realMoneyGate":"closed","reason":"Long-run endpoints sealed until 20 filled matches and 40 fills. Historical, retrospective, and synthetic rows cannot count.","registeredAt":"2026-07-18T07:03:55Z","requiredFilledMatches":20,"requiredFills":40,"status":"active_forward_paper","statusLabel":"V2 registered","stoppingRuleMet":false},"system":{"detail":"France vs England: Scheduled state recorded by the local supervisor. Non-tradeable.","feeds":[{"detail":"Exact source identity confirmed at export · Scheduled state recorded by the local supervisor","id":"txline","label":"TXLine","status":"scheduled","statusLabel":"Capture scheduled"},{"detail":"Exact event family · Scheduled state recorded by the local supervisor","id":"polymarket","label":"Polymarket","status":"scheduled","statusLabel":"Capture scheduled"},{"detail":"Registered forward-paper chains verify · 0 qualifying signals","id":"decision_ledger","label":"V2 study ledgers","status":"verified","statusLabel":"Verified"},{"detail":"2,470,342 canonical events","id":"replay_proof","label":"Replay proof","status":"verified","statusLabel":"Verified"}],"label":"Standing by for paired capture","posture":"standing_by"},"tradeable":false}}
diff --git a/public/artifacts/dashboard/manifest.json b/public/artifacts/dashboard/manifest.json
new file mode 100644
index 0000000..a700502
--- /dev/null
+++ b/public/artifacts/dashboard/manifest.json
@@ -0,0 +1 @@
+{"bundleId":"samaritan-dashboard-public-v1","bundleSha256":"2956a963976485b1933b2876482ae574e095a0e80781edff2433e29bc5bcf6d6","canonicalization":"samaritan-stable-json-v1","downloads":[{"bytes":15362,"file":"synthetic-decision-receipt.json","id":"synthetic-decision-receipt","mediaType":"application/json","performanceUse":"excluded_synthetic","sha256":"94ada838aeac865aa0862fb3e861a465ad4e9b85322a43e4d24726b43ad5ace8","synthetic":true}],"files":[{"apiPath":"/api/v1/command","bytes":8109,"file":"command.json","sha256":"4e37cb3e2100a15d1668415ecf90ba2cc1ead070d03d6a28fe5bf2301e885725","snapshotId":"command-2026-world-cup"},{"apiPath":"/api/v1/casebook","bytes":18346,"file":"casebook.json","sha256":"9dca1308316d6e7a85e7694dd8f14910fa74ebaf014abfa8b1dc2b9a9686bf8c","snapshotId":"casebook-2026-world-cup"},{"apiPath":"/api/v1/study","bytes":5331,"file":"study.json","sha256":"46eb77020ec734bf0faa0247b29537abe0271d5ec6e5d610e66ae5809b0a9716","snapshotId":"paper-study-2026-world-cup"},{"apiPath":"/api/v1/matchroom/paired-spain-belgium-2026-07-10","bytes":5636,"file":"matchroom-spain-belgium.json","sha256":"9ea8e3049c24435d03efe9a2af174d1ef5bcbf56a83bfbf44e78f4d6ac038d7b","snapshotId":"paired-spain-belgium-2026-07-10"}],"generatedAt":"2026-07-18T15:01:00.000Z","publicDataPolicy":{"credentialsRequired":false,"derivedOnly":true,"txlineFixtureIdentifiersExposed":false,"txlineMovementBucketBps":25,"txlineProbabilityDisplay":"bucketed_movement_only","walletControlsExposed":false},"schemaVersion":1}
diff --git a/public/artifacts/dashboard/matchroom-spain-belgium.json b/public/artifacts/dashboard/matchroom-spain-belgium.json
new file mode 100644
index 0000000..6b50d48
--- /dev/null
+++ b/public/artifacts/dashboard/matchroom-spain-belgium.json
@@ -0,0 +1 @@
+{"data":{"casebookCaseCount":18,"caseId":"SB-20260710-G01-MR","decision":{"capitalMovedMicros":0,"disposition":"no_trade","explanation":"The candidate arrived after the market had already moved. This retrospective feasibility observation never entered an execution runtime.","label":"No trade","ordersPlaced":0,"primaryReason":"Market moved before signal","semanticStatus":"disciplined_pass","stages":[{"detail":"STALE_QUOTE feasibility · research only","id":"signal","label":"Goal observed","status":"complete","timingLabel":"+0ms"},{"detail":"First material update","id":"evidence","label":"Evidence checked","status":"complete","timingLabel":"+228ms"},{"detail":"No clean stale window","id":"pass","label":"Opportunity passed","status":"passed","timingLabel":"Final"},{"detail":"Research path ended before an execution runtime","id":"execution","label":"Execution not reached","status":"locked","timingLabel":"N/A"}],"walletAccessed":false},"evidence":[{"assessment":"Moved first","bestAsk":0.26,"observation":"Draw ask begins repricing","observedAt":"2026-07-10T19:29:55.389Z","offsetLabel":"T−5.000s","replayStateId":"pre","source":"Polymarket"},{"assessment":"Pass","bestAsk":0.18,"observation":"Spain goal delivered at 29:21","observedAt":"2026-07-10T19:30:00.389Z","offsetLabel":"First seen","replayStateId":"goal","source":"TXLine"},{"assessment":"No trade","bestAsk":0.1625,"observation":"Spread tightens after repricing","observedAt":"2026-07-10T19:30:30.389Z","offsetLabel":"T+30.000s","replayStateId":"post","source":"Samaritan"}],"executionMode":"paper","generatedAt":"2026-07-14T23:13:44.692Z","market":{"family":"match_result","label":"Match result · Draw","mappingStatus":"research_only","outcome":"draw","period":"90 minutes plus stoppage time"},"match":{"away":{"code":"BEL","name":"Belgium"},"clockLabel":"29:21","clockSeconds":1761,"competition":"World Cup","eventSlug":"fifwc-esp-bel-2026-07-10","fixtureRef":"paired-spain-belgium-2026-07-10","goalOrdinal":1,"home":{"code":"ESP","name":"Spain"},"kickoffUtc":"2026-07-10T19:00:00.000Z","originalMatchDate":"2026-07-10","scoreAtCursor":{"away":0,"home":1},"stage":"Captured fixture"},"mode":"captured_replay","proof":{"canonicalEvents":2470342,"captureStatus":"verified","cleanStaleWindows":0,"corpusAssurance":"local_file_sha256_not_capture_manifest_membership","corpusCommitment":"0c852ded55fbd4acec5e93d3ee4e1ae78f475c9a02837a68f1af1ef695a42672","feedDowntimeMs":91171,"feedOutageCount":8,"gateCases":18,"headHash":"55b74d2f769bb40b822c02332bf94cc9af645185952dbbe7d115e6dac20ae05b","identityHash":"d7958c89929f2742e42b77ef2a4fb7f8df744f40dc3240e80bf724695eef4260","identityParity":true,"maximumFeedDowntimeMs":25375,"movedBeforeTxlineCases":12,"noMaterialRepriceCases":6,"replayMode":"capture-order-per-source"},"publicDataPolicy":{"credentialsRequired":false,"derivedOnly":true,"txlineFixtureIdentifiersExposed":false,"txlineMovementBucketBps":25,"txlineProbabilityDisplay":"bucketed_movement_only","walletControlsExposed":false},"realMoneyGate":"closed","replay":{"activeStateId":"goal","availabilityGaps":[],"chart":[{"available":true,"bestAsk":0.26,"bestBid":0.245,"offsetMs":-5000,"spread":0.015000000000000013},{"available":true,"bestAsk":0.205,"bestBid":0.17,"offsetMs":-1000,"spread":0.034999999999999976},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":0,"spread":0.009999999999999981},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":250,"spread":0.009999999999999981},{"available":true,"bestAsk":0.18,"bestBid":0.17,"offsetMs":500,"spread":0.009999999999999981},{"available":true,"bestAsk":0.1775,"bestBid":0.1525,"offsetMs":1000,"spread":0.024999999999999994},{"available":true,"bestAsk":0.1775,"bestBid":0.16,"offsetMs":2000,"spread":0.017499999999999988},{"available":true,"bestAsk":0.1675,"bestBid":0.16,"offsetMs":5000,"spread":0.007500000000000007},{"available":true,"bestAsk":0.1725,"bestBid":0.17,"offsetMs":10000,"spread":0.0024999999999999745},{"available":true,"bestAsk":0.1625,"bestBid":0.16,"offsetMs":30000,"spread":0.0025000000000000022}],"firstMaterialMoveLatencyMs":228,"firstSeenAt":"2026-07-10T19:30:00.389Z","firstSeenLatencyMs":11,"preTriggerMarketMoveBps":775,"states":[{"bestAsk":0.26,"bestBid":0.245,"conclusionBody":"Polymarket was already moving before TXLine delivered the goal.","conclusionTitle":"Repricing had begun.","consensusMoveFromBaselineBps":0,"decisionExplanation":"The market was moving before the trigger entered Samaritan's event bus.","id":"pre","label":"T−5 seconds","observedAt":"2026-07-10T19:29:55.389Z","offsetMs":-5000,"spread":0.015000000000000013},{"bestAsk":0.18,"bestBid":0.17,"conclusionBody":"It repriced before TXLine delivered the goal. No stale quote remained.","conclusionTitle":"Polymarket moved first.","consensusMoveFromBaselineBps":25,"decisionExplanation":"The candidate arrived after the market had already moved. This retrospective feasibility observation never entered an execution runtime.","id":"goal","label":"Goal first seen","observedAt":"2026-07-10T19:30:00.389Z","offsetMs":0,"spread":0.009999999999999981},{"bestAsk":0.1625,"bestBid":0.16,"conclusionBody":"A tighter spread did not restore an opportunity that disappeared before the trigger.","conclusionTitle":"The window stayed invalid.","consensusMoveFromBaselineBps":25,"decisionExplanation":"The spread tightened after repricing, but the candidate remained ineligible for execution.","id":"post","label":"T+30 seconds","observedAt":"2026-07-10T19:30:30.389Z","offsetMs":30000,"spread":0.0025000000000000022}]},"schemaVersion":2,"snapshotId":"paired-spain-belgium-2026-07-10","tradeable":false}}
diff --git a/public/artifacts/dashboard/study.json b/public/artifacts/dashboard/study.json
new file mode 100644
index 0000000..518d195
--- /dev/null
+++ b/public/artifacts/dashboard/study.json
@@ -0,0 +1 @@
+{"data":{"correctedHistoricalCandidate":{"activeStudyAtGeneration":false,"claimBoundary":"Historical sampled-price evidence justified review only; the separate v2 registration does not turn it into alpha, fill proof, profitability, or permission for real money.","configurationHash":"9a4eeff928f697fc55ab5147a4dc07f611c40bb749501fc3bd92b211f24b2e54","costProxyBps":100,"detector":"CONSENSUS_MOVE","evidenceClass":"historical_sampled_price_signal_research","executable":false,"executionEvidence":"not_established_no_historical_bid_ask_or_depth","generatedAt":"2026-07-14T10:34:05.545Z","heldoutFixtures":18,"heldoutNormalizedCases":38,"marketFamily":"Full-time totals","matchClustered95Bps":{"cluster":"fixture","high":243.9,"iterations":10000,"low":14.3},"meanNetAfterCostProxyBps":132.7,"protocolId":"historical-gate-causal-economic-v4-2026-07-14","schemaVersion":4,"sourceRegistrationAtGeneration":"engineering_candidate_unregistered","status":"historical_signal_candidate_for_forward_paper_review","trainingNormalizedCases":135},"decisionRules":{"accept":["Only fresh observations at or after v2 registration may count toward a decision.","Mean net executable CLV is positive and its match-clustered 95% CI lower bound is above zero.","Mean settlement P&L is positive and the strategy beats no-trade and random-direction baselines.","Every registered fill, slippage, drawdown, and depth guardrail holds."],"inconclusive":["The registered v2 study has not yet reached 20 filled matches and 40 fills from fresh evidence.","Capture-only evidence cannot authorize model spend, paper admission, or real money."],"reject":["The executable CLV interval touches or falls below zero.","The strategy fails to beat no-trade or the seeded random-direction control.","Any registered guardrail fails materially."]},"executionMode":"paper","fixtureUniverse":{"evidenceFixtures":0,"executableBookReplays":0,"generatedAt":"2026-07-18T07:17:07.354Z","longRunEligible":0,"pairedBookReplays":0,"signalResearchOnly":0},"generatedAt":"2026-07-18T07:17:27.184Z","historicalV1":{"active":false,"configHash":"7dcb4c20ac05195bdb4a6c8803432079eeefb8cc747c186f576084dc772fd2a2","invalidatedBeforeObservations":true,"lanes":{"bounty":{"canSatisfyGate":false,"chain":{"headHash":"1ff24c63635535548d17200c478ad895df929a38e43e662325f6a26aad949487","rows":1,"valid":true},"counts":{"filledMatches":0,"fills":0,"matches":0,"settledFills":0,"signals":0},"label":"Preserved invalidated v1 bounty ledger","reason":"Complete close and settlement evidence is required before endpoints are computed","sourceStatus":"exploratory","statusLabel":"V1 invalidated"},"longRun":{"canSatisfyGate":false,"chain":{"headHash":"488ed79a089e35ab2d70800352c49d3587e0284ad64bb0eda32d848874194c92","rows":1,"valid":true},"counts":{"filledMatches":0,"fills":0,"matches":0,"settledFills":0,"signals":0},"label":"Preserved invalidated v1 long-run ledger","reason":"Long-run endpoints sealed until 20 filled matches and 40 fills","sourceStatus":"sealed","statusLabel":"V1 invalidated","stoppingRuleMet":false}},"protocolVersion":"paper-study-v1-2026-07-12","results":{"endpoints":null,"guardrails":null,"rows":null,"visibility":"sealed"},"startedAt":"2026-07-12T09:45:17.212Z","status":"invalidated_suspended"},"mode":"offline_artifact","protocol":{"active":true,"activity":"active_forward_paper","candidate":{"cusumThresholdBps":10,"detector":"CONSENSUS_MOVE","marketFamily":"Full-time totals only","maximumDistanceFromEven":0.15,"minimumCoveragePoints":1000,"minimumGapBps":100,"minimumUpdates":5,"moveAbsZ":1,"selector":"Closest to even"},"configHash":"93e61c1903d0a13bbeb1dbbd3ad9b11af0335b96c82bd2ca7aa9ddedeeabf3ce","evaluation":{"bootstrapIterations":10000,"bootstrapSeed":20260714,"minimumFilledMatches":20,"minimumFills":40,"primaryEndpoint":"Executable CLV net of measured costs","randomDirectionControl":"Seeded matched-cost sign flip","targetMatches":30,"unitOfAnalysis":"match"},"evidencePolicy":"fresh_forward_only","guardrailThresholds":{"maximumDrawdownMicroUsd":20000000,"maximumMeanSlippageBps":100,"minimumFillRate":0.6,"selectedDepthRequired":true},"observationStatus":"awaiting_fresh_evidence","qualifyingCounts":{"filledMatches":0,"fills":0,"matches":0,"settledFills":0,"signals":0},"realMoneyGate":"closed","registeredAt":"2026-07-18T07:03:55Z","risk":{"aggregateExposureMicroUsd":15000000,"bankrollMicroUsd":50000000,"drawdownStopMicroUsd":20000000,"perTradeStakeMicroUsd":3000000},"status":"registered","version":"paper-study-v2-2026-07-18"},"publicDataPolicy":{"credentialsRequired":false,"derivedOnly":true,"txlineFixtureIdentifiersExposed":false,"txlineMovementBucketBps":25,"txlineProbabilityDisplay":"bucketed_movement_only","walletControlsExposed":false},"realMoneyGate":"closed","results":{"endpoints":null,"guardrails":null,"rows":null,"visibility":"sealed"},"schemaVersion":3,"snapshotId":"paper-study-2026-world-cup","syntheticProof":{"explanation":"Closed-world production-component demo; separate from historical evidence and excluded from every performance claim.","externalCalls":0,"label":"Synthetic full-lifecycle proving fixture","lifecycleStatus":"filled_settled","offlineVerified":true,"path":"/artifacts/dashboard/synthetic-decision-receipt.json","performanceUse":"excluded_synthetic","solanaAnchorStatus":"not_submitted"},"tradeable":false}}
diff --git a/public/artifacts/dashboard/synthetic-decision-receipt.json b/public/artifacts/dashboard/synthetic-decision-receipt.json
new file mode 100644
index 0000000..5d2c717
--- /dev/null
+++ b/public/artifacts/dashboard/synthetic-decision-receipt.json
@@ -0,0 +1 @@
+{"agents":{"runs":[{"actualCostNanoUsd":0,"billingEvidenceRefSha256":"440766253819749e1399e4fbba33e009bd89a1228636fe4e882ac67d9fc9046b","invocationClass":"synthetic_stub","model":"claude-haiku-4-5","pricing":{"cacheReadNanoUsdPerToken":100,"cacheWriteNanoUsdPerToken":1250,"currency":"nano_usd","inputNanoUsdPerToken":1000,"outputNanoUsdPerToken":5000,"pricingVersion":"anthropic-public-2026-07-12"},"promptSha256":"6f72831fb8edc6e952d43407a43b715e02b5a4914e9fd20e0821bde8f2d153a5","promptVersion":"synthetic-judge-triage-v1","responseSha256":"8838a6686bb5729f0ac24969e371f9f26ea9097835fc60a5b9cab50b3ceddd28","stage":"triage","status":"success","usage":{"cacheCreationInputTokens":0,"cacheReadInputTokens":0,"inputTokens":0,"outputTokens":0}},{"actualCostNanoUsd":0,"billingEvidenceRefSha256":"5020037e4a8a8abbc61ba717a3d1a21e88bcf5cab68bcd62b85181d7701e4fdf","invocationClass":"synthetic_stub","model":"claude-opus-4-8","pricing":{"cacheReadNanoUsdPerToken":500,"cacheWriteNanoUsdPerToken":6250,"currency":"nano_usd","inputNanoUsdPerToken":5000,"outputNanoUsdPerToken":25000,"pricingVersion":"anthropic-public-2026-07-12"},"promptSha256":"8d22add73e03b60a0c2d71c5cdba5a5a81809f310ea95243a8b385d662f72721","promptVersion":"synthetic-judge-analyst-v1","responseSha256":"c42070e8df1c22584ac572fadfc430a719a786e40e354f9563fd9612a981c707","stage":"analyst","status":"success","usage":{"cacheCreationInputTokens":0,"cacheReadInputTokens":0,"inputTokens":0,"outputTokens":0}}],"totalActualCostNanoUsd":0},"build":{"codeSha256":"a150b4741ce2c0f18137bd05ed3d717ab5a5db42aa6ae856eeffb2206075a524","codeVersion":"synthetic-judge-case-v1","configSha256":"93e61c1903d0a13bbeb1dbbd3ad9b11af0335b96c82bd2ca7aa9ddedeeabf3ce"},"disclosure":{"policy":"hashes_and_derived_signals_only","rawTxlineFieldsIncluded":false},"generatedAtTsMs":1784386860000,"integrity":{"algorithm":"sha256","canonicalization":"samaritan-stable-json-v1","domain":"samaritan.decision-receipt/v1","receiptHash":"e597cc4bb535bc15378af49d9a37f3b2e4f10cdba90b7d1383a7631fa3e940f7"},"ledger":{"caseEntries":[{"atTsMs":1784362080010,"entryHash":"9d093d9fdc042c435d9dae9859c8fb648d0b5f9c06b9e5f90e72925aa4eb36f0","entryIdSha256":"86d47fbbd0b68b1b0f1779cb10d8f4920f8119fdbaa21f96b5dc349e9f56cd67","hashSchemaVersion":2,"insertedAtMs":1784386800001,"kind":"signal_received","payloadSha256":"c21b50839d85b2f567dec58ff10cc92c29c368cc9f15dc956d4eb13f03ba794f","previousHash":"909a4ee701f7e811eb5816b8ebfd9aa8ac98a037e97e857d42395ae517ba42eb","sequence":2},{"atTsMs":1784362080310,"entryHash":"a4d9798539cba839451571a722216c5b1ddc437acb8314e7a88be0cd2846c600","entryIdSha256":"4f1566b9982f7f4a999911b6a35c868407543d29e2313aaa157bad128aaf0949","hashSchemaVersion":2,"insertedAtMs":1784386800002,"kind":"triage_decision","payloadSha256":"8838a6686bb5729f0ac24969e371f9f26ea9097835fc60a5b9cab50b3ceddd28","previousHash":"9d093d9fdc042c435d9dae9859c8fb648d0b5f9c06b9e5f90e72925aa4eb36f0","sequence":3},{"atTsMs":1784362080310,"entryHash":"79fd5609cb89bc656617b55e6bef00a1517cc4c8262a6af51676582133f3289a","entryIdSha256":"90da2d4efeec642093f194cf593179e397229408091d6f6a36becfdbd871b740","hashSchemaVersion":2,"insertedAtMs":1784386800003,"kind":"thesis_submitted","payloadSha256":"c42070e8df1c22584ac572fadfc430a719a786e40e354f9563fd9612a981c707","previousHash":"a4d9798539cba839451571a722216c5b1ddc437acb8314e7a88be0cd2846c600","sequence":4},{"atTsMs":1784362080310,"entryHash":"de63a9964686ca5e0c274cf40d5ce9512bc7709d3c50f9449e1960c9ed840c98","entryIdSha256":"ef90a9fe8bd91999dbca7ab20318e51717c79519d7140d53a0ac8dbd0645528a","hashSchemaVersion":2,"insertedAtMs":1784386800004,"kind":"analysis_completed","payloadSha256":"df208d9f55e0da7e662b0e9a438a2383d53660c02920af5ce27504c274af6920","previousHash":"79fd5609cb89bc656617b55e6bef00a1517cc4c8262a6af51676582133f3289a","sequence":5},{"atTsMs":1784362082010,"entryHash":"5ca2f1d11e658f7c997bb32858a48e1f5f8ee666811414040f56b8fe056ab9a1","entryIdSha256":"d348407763b6c15404c4925464068ef98f863275656ce6fc01ce05a9a1497668","hashSchemaVersion":2,"insertedAtMs":1784386800005,"kind":"risk_verdict","payloadSha256":"72951bf422061213048536dc8b3c85d4109f0f1b6bfb218f1667324c53933a46","previousHash":"de63a9964686ca5e0c274cf40d5ce9512bc7709d3c50f9449e1960c9ed840c98","sequence":6},{"atTsMs":1784362082010,"entryHash":"b54425d5882756bbb7672fa8c392ddd797e4a9b9f3324b5e3dcb2756f3e361bb","entryIdSha256":"ea503875cff54512cb024ed4678d638de776b5fa8e7339c23a058b553304d126","hashSchemaVersion":2,"insertedAtMs":1784386800006,"kind":"execution_intent","payloadSha256":"7533271c53877f517ddcd5cc0980120b6839ed81d9d4d547233fcca11c86c1ca","previousHash":"5ca2f1d11e658f7c997bb32858a48e1f5f8ee666811414040f56b8fe056ab9a1","sequence":7},{"atTsMs":1784362082010,"entryHash":"97e03e3a4c609475589ee7e5bbaf5b04449073cc985cba235370edfba7e04811","entryIdSha256":"8620732877b253479594e0684f67da65293e64a2261d665d369639821701da5b","hashSchemaVersion":2,"insertedAtMs":1784386800007,"kind":"paper_execution","payloadSha256":"838c6c739c35d4a41be2ef975ac6880da7f57310268ead4930b28fef45a697fd","previousHash":"b54425d5882756bbb7672fa8c392ddd797e4a9b9f3324b5e3dcb2756f3e361bb","sequence":8},{"atTsMs":1784362082010,"entryHash":"442f4fc95653773dd9e65e1db70db265cd637972a535698fef6b396cb99dc595","entryIdSha256":"c4eaf33008d0f0a594134cb41baea09e34a55c2882f91fadfd32355e002d0b35","hashSchemaVersion":2,"insertedAtMs":1784386800008,"kind":"case_terminal","payloadSha256":"a279a2a5d4bef14c5f523f7145bcdbd762a9e4ff6bee856ccf4d3214bacc4ead","previousHash":"97e03e3a4c609475589ee7e5bbaf5b04449073cc985cba235370edfba7e04811","sequence":9},{"atTsMs":1784362082010,"entryHash":"55131a86e69127611048477274c964cfeb21693052c43816ce2289624ff56500","entryIdSha256":"ebacc95960b8d5b8203e272255f65eed6a1a28e0468a4e743fc9691babc31122","hashSchemaVersion":2,"insertedAtMs":1784386800009,"kind":"position_opened","payloadSha256":"15d8b818aa256a9ea0ce8b150c0b91dab9ac3ea022167162d2c693f66ca34473","previousHash":"442f4fc95653773dd9e65e1db70db265cd637972a535698fef6b396cb99dc595","sequence":10},{"atTsMs":1784383200010,"entryHash":"a4206028bdbcaae829be2990421a5d9c8d11428b6ef861b250665203dd49b340","entryIdSha256":"271a15bd61a2e5e67ad6642723d4d249903c5667edbd87c9672465ea6e5005db","hashSchemaVersion":2,"insertedAtMs":1784386800010,"kind":"position_closed","payloadSha256":"d133612d12bb5ed9300924f3275601ee0455b6d30109e12c71bbc7e15c611c22","previousHash":"55131a86e69127611048477274c964cfeb21693052c43816ce2289624ff56500","sequence":11},{"atTsMs":1784383200010,"entryHash":"ad3d0cd30459042d6f17e077b0c5a4944c5bc73f9f4686778c45b96131ebf9e4","entryIdSha256":"a31fd159f63230fe51909f8b65efba6cdc82648c96683dd0b9f557637fb6893f","hashSchemaVersion":2,"insertedAtMs":1784386800011,"kind":"position_settled","payloadSha256":"bd687823c82d52db3acbd7092732130e2111cd96da88e5b4f716879c73027a91","previousHash":"a4206028bdbcaae829be2990421a5d9c8d11428b6ef861b250665203dd49b340","sequence":12}],"caseIdSha256":"3291b289b99d65c85807286262a474606f056ef535d0eb9305dc306fcaf1ec2d","finalHeadHash":"ad3d0cd30459042d6f17e077b0c5a4944c5bc73f9f4686778c45b96131ebf9e4","finalHeadSequence":12,"payloadDisclosure":"sha256_commitments_only","rowsAtGeneration":12,"verificationAtGeneration":"full_v2_chain_valid"},"lifecycle":{"analysis":{"decisionLatencyMs":300,"ledgerEntryHash":"de63a9964686ca5e0c274cf40d5ce9512bc7709d3c50f9449e1960c9ed840c98","orderEligibleAtTsMs":1784362081310,"readyAtTsMs":1784362080310,"recommendation":"paper_trade","signalObservedTsMs":1784362080010,"signalSourceTsMs":1784362080000,"venuePlacementDelayMs":1000},"close":{"bookEvidenceRefSha256":"dd92ff34bb513b5e3d5b0acbd250c079d7c7f774a300109685be3f98fe34b4ee","bookObservedTsMs":1784375999010,"bookSourceTsMs":1784375999000,"closeAsk":0.59,"closeBid":0.57,"closeMidpoint":0.58,"cutoffTsMs":1784376000000,"executableLiquidationClvBps":311.28268245494195,"grossMidpointClvBps":535.9241294812489,"ledgerEntryHash":"a4206028bdbcaae829be2990421a5d9c8d11428b6ef861b250665203dd49b340","markedAtTsMs":1784383200010,"netMidpointClvBps":411.28268245494206},"execution":{"adapter":"paper","assetRefSha256":"458b48b13d636ffa4da35d22785caaf845edf323c0c0fafc4bd16423a63b998d","averagePrice":0.5264075870518751,"bestPrice":0.52,"bookEvidenceRefSha256":"1fcc8e954a4e0ea7c4650a236751e90ab632a3c8dde756a13c0f4efa896ed975","bookObservedTsMs":1784362082010,"bookSourceTsMs":1784362082000,"conditionRefSha256":"fbd0f84b94d0f25d65eb3f848da00ac3233d4a54ac8a35e12388c5c3dd149a7b","direction":"buy","executableDepthUsd":54.04,"feeMetadataRefSha256":"5e5e156b36ae71d506074218c6d5d26a481c9e6c575dcfd8863a9b20dfce767a","feeMicroUsd":69390,"filledShares":5.567169,"grossMicroUsd":2930600,"halfSpreadBps":100.00000000000009,"ledgerEntryHash":"97e03e3a4c609475589ee7e5bbaf5b04449073cc985cba235370edfba7e04811","netConsiderationMicroUsd":2999990,"reason":null,"requestedStakeMicroUsd":3000000,"slippageProbabilityBps":64.07587051875052,"status":"filled"},"finalStatus":"filled_settled","intent":{"availableShares":0,"direction":"buy","fixtureRefSha256":"5d9ba078a6b11bbe03d2103ac2366e334c3d1ded3ba5db278aa61a546d263ac8","lane":"bounty","ledgerEntryHash":"b54425d5882756bbb7672fa8c392ddd797e4a9b9f3324b5e3dcb2756f3e361bb","limitPolicy":"deterministic_private_fair_value_boundary","marketRefSha256":"e5b4d3bb0e830b25f37e1e4ce0ab0e21b994c4c8ce0c45878012539b9ba94550","outcome":"over","signalRefSha256":"0c71b303e7613bad8f76d35236e3489f61d13811cab09038d8c1d6be8ba11993","stakeMicroUsd":3000000},"orderedEventKinds":["signal_received","triage_decision","thesis_submitted","analysis_completed","risk_verdict","execution_intent","paper_execution","case_terminal","position_opened","position_closed","position_settled"],"position":{"assetRefSha256":"458b48b13d636ffa4da35d22785caaf845edf323c0c0fafc4bd16423a63b998d","averageEntryPrice":0.5264075870518751,"conditionRefSha256":"fbd0f84b94d0f25d65eb3f848da00ac3233d4a54ac8a35e12388c5c3dd149a7b","entryCostMicroUsd":2999990,"entryFeeMicroUsd":69390,"entryGrossMicroUsd":2930600,"entryHalfSpreadBps":100.00000000000009,"entrySlippageBps":64.07587051875052,"filledShares":5.567169,"fillStatus":"filled","fixtureRefSha256":"5d9ba078a6b11bbe03d2103ac2366e334c3d1ded3ba5db278aa61a546d263ac8","lane":"bounty","ledgerEntryHash":"55131a86e69127611048477274c964cfeb21693052c43816ce2289624ff56500","lineRefSha256":"fe82e6baa7be838b672749b06987ad38a401fc72651a530e732066bc0daffc86","marketRefSha256":"e5b4d3bb0e830b25f37e1e4ce0ab0e21b994c4c8ce0c45878012539b9ba94550","openedAtTsMs":1784362082010,"outcome":"over","selectedDepthUsd":54.04,"signalRefSha256":"0c71b303e7613bad8f76d35236e3489f61d13811cab09038d8c1d6be8ba11993"},"risk":{"decision":"approve","ledgerEntryHash":"5ca2f1d11e658f7c997bb32858a48e1f5f8ee666811414040f56b8fe056ab9a1","limitPolicy":"deterministic_private_fair_value_boundary","realMoneyGate":"closed","reasons":[],"stakeMicroUsd":3000000},"settlement":{"entryBrier":0.2242897736020273,"ledgerEntryHash":"ad3d0cd30459042d6f17e077b0c5a4944c5bc73f9f4686778c45b96131ebf9e4","payoutMicroUsd":5567169,"pnlMicroUsd":2567179,"resolutionEvidenceRefSha256":"a776bd78c0f2f98a9da23f689d0fb110b0dfb8e13cff1023778210897bf04523","returnBps":8557.291857639526,"settledAtTsMs":1784383200010,"won":true},"signal":{"derivedEvidence":{"consensusVelocityBucketBps":500,"consensusZScore":489.5655813290226,"cusumDownScore":0,"cusumUpScore":0.049400000000000055,"gapBasis":"live_book","mappingStatus":"verified","persistenceMs":0,"polymarketVelocityBucketBps":0,"polymarketZScore":null,"relativeValueDirection":"consensus_above_venue","scoreContextActionCount":0,"txlineMovementBucketBps":25},"detector":"CONSENSUS_MOVE","direction":"buy","eligibility":"pretrade_review_required","fixtureRefSha256":"5d9ba078a6b11bbe03d2103ac2366e334c3d1ded3ba5db278aa61a546d263ac8","ledgerEntryHash":"9d093d9fdc042c435d9dae9859c8fb648d0b5f9c06b9e5f90e72925aa4eb36f0","lineRefSha256":"fe82e6baa7be838b672749b06987ad38a401fc72651a530e732066bc0daffc86","marketFamily":"total_goals","marketPeriod":"full_time","marketRefSha256":"e5b4d3bb0e830b25f37e1e4ce0ab0e21b994c4c8ce0c45878012539b9ba94550","observedAtTsMs":1784362080010,"outcome":"over","signalRefSha256":"0c71b303e7613bad8f76d35236e3489f61d13811cab09038d8c1d6be8ba11993","sourceEvidenceRefs":["bb739b22a640e85429ac977935fc2fbb35f9e69a98fbd8203d49740323c5671c","f34ba894468e90c195743f4236b749ebbe1d5579fb3a618e30ce78dc0baffab6"],"sourceTsMs":1784362080000},"terminal":{"decisionLatencyMs":null,"ledgerEntryHash":"442f4fc95653773dd9e65e1db70db265cd637972a535698fef6b396cb99dc595","reason":"filled","status":"filled"},"thesis":{"analystModel":"claude-opus-4-8","direction":"buy","disclosure":"private_payload_sha256_commitment","expiresAtTsMs":1784362980310,"fixtureRefSha256":"5d9ba078a6b11bbe03d2103ac2366e334c3d1ded3ba5db278aa61a546d263ac8","ledgerEntryHash":"79fd5609cb89bc656617b55e6bef00a1517cc4c8262a6af51676582133f3289a","marketRefSha256":"e5b4d3bb0e830b25f37e1e4ce0ab0e21b994c4c8ce0c45878012539b9ba94550","outcome":"over","recommendation":"paper_trade","schemaVersion":1,"signalRefSha256":"0c71b303e7613bad8f76d35236e3489f61d13811cab09038d8c1d6be8ba11993","submittedAtTsMs":1784362080310,"thesisPayloadSha256":"c42070e8df1c22584ac572fadfc430a719a786e40e354f9563fd9612a981c707"},"triage":{"decision":"escalate","ledgerEntryHash":"a4d9798539cba839451571a722216c5b1ddc437acb8314e7a88be0cd2846c600","priority":"normal","rationale":"Synthetic proving signal meets the fixed escalation path; performance use is forbidden."}},"provenance":{"evidenceClass":"synthetic_proving_fixture","label":"SYNTHETIC JUDGE CASE — no real match, licensed feed record, external model call, venue order, wallet, network request, or performance evidence","performanceUse":"excluded_synthetic","synthetic":true},"receiptId":"c1cf5172bbb9e36d6cd3d85c955d42b9185fb1465c927af1d7fc89a4b21c6409","receiptType":"samaritan_decision_receipt","schemaVersion":1,"solanaAnchor":null,"sourceEvidence":[{"disclosure":"hash_only","evidenceRefSha256":"bb739b22a640e85429ac977935fc2fbb35f9e69a98fbd8203d49740323c5671c","observedAtTsMs":1784362080010,"payloadSha256":"05eb18451788618ffefdd1820c802310f8c3442150108537c7d577a71e220c52","role":"signal","source":"txline","sourceTsMs":1784362080000},{"disclosure":"hash_only","evidenceRefSha256":"f34ba894468e90c195743f4236b749ebbe1d5579fb3a618e30ce78dc0baffab6","observedAtTsMs":1784362020110,"payloadSha256":"47ffbf9241e1d81708eb4d56a36923f0a30cb1813ef2831a5ee1dd73c0d75e13","role":"signal","source":"polymarket","sourceTsMs":1784362020100},{"disclosure":"hash_only","evidenceRefSha256":"1fcc8e954a4e0ea7c4650a236751e90ab632a3c8dde756a13c0f4efa896ed975","observedAtTsMs":1784362082010,"payloadSha256":"989540e44507a26d6fcd286e276b0b17337372f45d7262de6ea45f66381b544a","role":"execution","source":"polymarket","sourceTsMs":1784362082000},{"disclosure":"hash_only","evidenceRefSha256":"dd92ff34bb513b5e3d5b0acbd250c079d7c7f774a300109685be3f98fe34b4ee","observedAtTsMs":1784375999010,"payloadSha256":"ab135c8873276b6a525cbcff6673ea387387d010112ba1dc43c8bc0927f1bb64","role":"close","source":"polymarket","sourceTsMs":1784375999000},{"disclosure":"hash_only","evidenceRefSha256":"a776bd78c0f2f98a9da23f689d0fb110b0dfb8e13cff1023778210897bf04523","observedAtTsMs":1784383200010,"payloadSha256":"4b8cdb0cc9fab9e0da3f19b58b335e7ef19d1411e1ff074098fb55cbc969fb0c","role":"settlement","source":"polymarket","sourceTsMs":1784383200000}]}
diff --git a/public/artifacts/edge/judge-evidence.json b/public/artifacts/edge/judge-evidence.json
new file mode 100644
index 0000000..e0ef5d0
--- /dev/null
+++ b/public/artifacts/edge/judge-evidence.json
@@ -0,0 +1 @@
+{"access":{"authenticationRequired":false,"disclosure":"derived_metadata_only","readOnly":true,"source":"verified_frozen_public_bundle"},"app":{"buildCommit":null,"name":"Samaritan","service":"samaritan-dashboard"},"documentation":{"repository":"https://github.com/Pavilion-devs/samaritan","technicalOverview":"https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md","txlineApiFeedback":"https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/txline-api-feedback.md"},"evidenceClassCounts":{"capturedNoTradeObservations":18,"capturedReplayFixtures":1,"capturedReplayObservations":18,"historicalHeldoutFixtures":18,"historicalHeldoutObservations":38,"historicalTrainingObservations":135,"registeredForwardPaperSignals":0,"syntheticLifecycleReceipts":1,"verifiedCapturedAgentLifecycleReceipts":0},"gates":{"executionMode":"paper","paperProtocol":{"active":true,"evidencePolicy":"fresh_forward_only","id":"paper-study-v2-2026-07-18","observationStatus":"awaiting_fresh_evidence","qualifyingCounts":{"filledMatches":0,"fills":0,"matches":0,"settledFills":0,"signals":0},"riskLimits":{"aggregateExposureMicroUsd":15000000,"bankrollMicroUsd":50000000,"drawdownStopMicroUsd":20000000,"perTradeStakeMicroUsd":3000000},"status":"registered"},"realMoney":{"gate":"closed","ordersEnabled":false}},"generatedAt":"2026-07-18T15:01:00.000Z","publicLimitations":["The judge surface is a frozen evidence bundle, not a live private-data relay.","The captured replay lane is retrospective feasibility evidence and did not enter the agent or execution runtime.","Historical research lacks executable order-book depth and is not fill or profitability evidence.","The complete lifecycle fixture uses deterministic model stubs, makes zero external calls, and is excluded from performance claims.","Real-money execution is disabled; the registered study is forward paper observation only.","The receipt is verified offline and has not been submitted to Solana."],"schemaVersion":1,"syntheticReceipt":{"externalCalls":0,"lifecycleStatus":"filled_settled","orderedEventCount":11,"performanceUse":"excluded_synthetic","route":"/artifacts/dashboard/synthetic-decision-receipt.json","solanaAnchorStatus":"not_submitted","synthetic":true,"verificationStatus":"offline_verified"},"trackCriteria":[{"criterion":"Core Functionality & Data Ingestion","evidence":[{"label":"Captured TXLine-derived replay","route":"/matchroom"},{"label":"Derived evidence index API","route":"/api/v1/casebook"},{"label":"Live derived TXLine connectivity pulse","route":"/api/v1/txline/pulse"},{"label":"Integration documentation","route":"https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md"}],"id":"core_functionality_and_data_ingestion"},{"criterion":"Autonomous Operation","evidence":[{"label":"Bounded lifecycle proof","route":"/proof"},{"label":"Offline-verifiable synthetic receipt","route":"/artifacts/dashboard/synthetic-decision-receipt.json"}],"id":"autonomous_operation"},{"criterion":"Logic & Code Architecture","evidence":[{"label":"Interactive receipt verifier","route":"/proof"},{"label":"Decision evidence index","route":"/casebook"},{"label":"Ordered lifecycle receipt","route":"/artifacts/dashboard/synthetic-decision-receipt.json"}],"id":"deterministic_logic_and_architecture"},{"criterion":"Innovation / Novelty","evidence":[{"label":"Disciplined no-trade case","route":"/matchroom"},{"label":"Separated research and proving fixture","route":"/study"}],"id":"innovation_and_novelty"},{"criterion":"Production Readiness","evidence":[{"label":"No-login judge surface","route":"/"},{"label":"Read-only service health","route":"/api/v1/health"},{"label":"Fail-closed evidence summary","route":"/api/judge/evidence"}],"id":"production_readiness"}],"txlineIntegration":{"endpointsUsed":[{"method":"POST","path":"/auth/guest/start","purpose":"Start the guest authentication session"},{"method":"POST","path":"/api/token/activate","purpose":"Activate the subscribed data token"},{"method":"GET","path":"/api/fixtures/snapshot","purpose":"Discover exact fixture and kickoff identities"},{"method":"GET","path":"/api/odds/stream","purpose":"Consume live odds SSE"},{"method":"GET","path":"/api/scores/stream","purpose":"Consume live score and action SSE"},{"method":"GET","path":"/api/odds/snapshot/{fixtureId}","purpose":"Backfill current odds state during recovery"},{"method":"GET","path":"/api/scores/snapshot/{fixtureId}","purpose":"Backfill current score state during recovery"},{"method":"GET","path":"/api/odds/updates/{epochDay}/{hourOfDay}/{interval}","purpose":"Build historical odds replay input"},{"method":"GET","path":"/api/scores/updates/{epochDay}/{hourOfDay}/{interval}","purpose":"Build aligned historical score input"},{"method":"GET","path":"/api/scores/historical/{fixtureId}","purpose":"Recover retained fixture score sequences"}],"publicOutput":"derived_evidence_only"}}
diff --git a/public/artifacts/edge/manifest.json b/public/artifacts/edge/manifest.json
new file mode 100644
index 0000000..a229937
--- /dev/null
+++ b/public/artifacts/edge/manifest.json
@@ -0,0 +1 @@
+{"bundleId":"samaritan-dashboard-edge-v1","edgeBundleSha256":"8aa4da57b4a8a763b978d927d46b9c38388b1f9c0f5052bd795d9681d6e37b81","generatedAt":"2026-07-18T15:01:00.000Z","publicDataPolicy":{"buildCommitDisclosure":"omitted_from_frozen_artifact","credentialsRequired":false,"derivedOnly":true,"judgeEvidenceSource":"verified_node_builder","walletControlsExposed":false},"routes":[{"apiPath":"/api/v1/command","assetPath":"/artifacts/dashboard/command.json","bytes":8109,"sha256":"4e37cb3e2100a15d1668415ecf90ba2cc1ead070d03d6a28fe5bf2301e885725","source":"public_dashboard_manifest"},{"apiPath":"/api/v1/casebook","assetPath":"/artifacts/dashboard/casebook.json","bytes":18346,"sha256":"9dca1308316d6e7a85e7694dd8f14910fa74ebaf014abfa8b1dc2b9a9686bf8c","source":"public_dashboard_manifest"},{"apiPath":"/api/v1/study","assetPath":"/artifacts/dashboard/study.json","bytes":5331,"sha256":"46eb77020ec734bf0faa0247b29537abe0271d5ec6e5d610e66ae5809b0a9716","source":"public_dashboard_manifest"},{"apiPath":"/api/v1/matchroom/paired-spain-belgium-2026-07-10","assetPath":"/artifacts/dashboard/matchroom-spain-belgium.json","bytes":5636,"sha256":"9ea8e3049c24435d03efe9a2af174d1ef5bcbf56a83bfbf44e78f4d6ac038d7b","source":"public_dashboard_manifest"},{"apiPath":"/api/judge/evidence","assetPath":"/artifacts/edge/judge-evidence.json","bytes":4943,"sha256":"629b494e5937c9c18f11845002177d8f8efc21b88b13799fbc3f0f7d6ed44918","source":"buildJudgeEvidenceResponse"}],"schemaVersion":1,"sourceBundleSha256":"2956a963976485b1933b2876482ae574e095a0e80781edff2433e29bc5bcf6d6"}
diff --git a/src/agents/claude-evidence-ledger.ts b/src/agents/claude-evidence-ledger.ts
new file mode 100644
index 0000000..508ee11
--- /dev/null
+++ b/src/agents/claude-evidence-ledger.ts
@@ -0,0 +1,251 @@
+import { createHash } from "node:crypto";
+import { mkdirSync } from "node:fs";
+import { dirname } from "node:path";
+import Database from "better-sqlite3";
+import { z } from "zod";
+import { stableJson } from "../domain/json.js";
+import {
+ CLAUDE_MODEL,
+ claudeUsageCostNanoUsd,
+ type ClaudeUsage
+} from "./claude-pricing.js";
+import type { ClaudeInvocationEvidence } from "./claude.js";
+
+const GENESIS_HASH = "0".repeat(64);
+const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
+const usageSchema = z.object({
+ inputTokens: z.number().int().nonnegative().safe(),
+ outputTokens: z.number().int().nonnegative().safe(),
+ cacheCreationInputTokens: z.number().int().nonnegative().safe(),
+ cacheReadInputTokens: z.number().int().nonnegative().safe()
+}).strict();
+const evidenceSchema = z.object({
+ caseId: z.string().min(1),
+ stage: z.enum(["triage", "analyst"]),
+ invocationClass: z.enum(["anthropic_api", "injected_client"]),
+ model: z.enum([CLAUDE_MODEL.triage, CLAUDE_MODEL.analyst]),
+ promptVersion: z.string().min(1).max(100),
+ promptSha256: sha256Schema,
+ responseSha256: sha256Schema,
+ billingEvidenceSha256: sha256Schema,
+ usage: usageSchema,
+ actualCostNanoUsd: z.number().int().nonnegative().safe()
+}).strict().superRefine((evidence, context) => {
+ const expectedModel = evidence.stage === "triage" ? CLAUDE_MODEL.triage : CLAUDE_MODEL.analyst;
+ if (evidence.model !== expectedModel) {
+ context.addIssue({ code: "custom", path: ["model"], message: "Claude evidence stage/model mismatch" });
+ }
+ if (evidence.actualCostNanoUsd !== claudeUsageCostNanoUsd(evidence.model, evidence.usage)) {
+ context.addIssue({
+ code: "custom",
+ path: ["actualCostNanoUsd"],
+ message: "Claude evidence cost does not match model usage"
+ });
+ }
+});
+
+type EvidenceRow = {
+ sequence: number;
+ entry_id: string;
+ case_id: string;
+ stage: "triage" | "analyst";
+ invocation_class: "anthropic_api" | "injected_client";
+ model: typeof CLAUDE_MODEL.triage | typeof CLAUDE_MODEL.analyst;
+ prompt_version: string;
+ prompt_sha256: string;
+ response_sha256: string;
+ billing_evidence_sha256: string;
+ usage_json: string;
+ actual_cost_nano_usd: number;
+ previous_hash: string;
+ entry_hash: string;
+ inserted_at_ms: number;
+};
+
+function sha256(value: string): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+function fromRow(row: EvidenceRow): ClaudeInvocationEvidence {
+ return evidenceSchema.parse({
+ caseId: row.case_id,
+ stage: row.stage,
+ invocationClass: row.invocation_class,
+ model: row.model,
+ promptVersion: row.prompt_version,
+ promptSha256: row.prompt_sha256,
+ responseSha256: row.response_sha256,
+ billingEvidenceSha256: row.billing_evidence_sha256,
+ usage: JSON.parse(row.usage_json) as ClaudeUsage,
+ actualCostNanoUsd: row.actual_cost_nano_usd
+ });
+}
+
+export type ClaudeInvocationEvidenceRecord = {
+ sequence: number;
+ entryId: string;
+ insertedAtMs: number;
+ previousHash: string;
+ entryHash: string;
+ evidence: ClaudeInvocationEvidence;
+};
+
+function recordFromRow(row: EvidenceRow): ClaudeInvocationEvidenceRecord {
+ return {
+ sequence: row.sequence,
+ entryId: row.entry_id,
+ insertedAtMs: row.inserted_at_ms,
+ previousHash: row.previous_hash,
+ entryHash: row.entry_hash,
+ evidence: fromRow(row)
+ };
+}
+
+export class ClaudeInvocationEvidenceLedger {
+ readonly #db: Database.Database;
+
+ constructor(path: string) {
+ if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
+ this.#db = new Database(path);
+ this.#db.pragma("journal_mode = WAL");
+ this.#db.pragma("synchronous = FULL");
+ this.#db.exec(`
+ CREATE TABLE IF NOT EXISTS claude_invocation_evidence (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ entry_id TEXT NOT NULL UNIQUE,
+ case_id TEXT NOT NULL,
+ stage TEXT NOT NULL CHECK(stage IN ('triage', 'analyst')),
+ invocation_class TEXT NOT NULL CHECK(invocation_class IN ('anthropic_api', 'injected_client')),
+ model TEXT NOT NULL,
+ prompt_version TEXT NOT NULL,
+ prompt_sha256 TEXT NOT NULL,
+ response_sha256 TEXT NOT NULL,
+ billing_evidence_sha256 TEXT NOT NULL,
+ usage_json TEXT NOT NULL,
+ actual_cost_nano_usd INTEGER NOT NULL,
+ previous_hash TEXT NOT NULL,
+ entry_hash TEXT NOT NULL UNIQUE,
+ inserted_at_ms INTEGER NOT NULL,
+ UNIQUE(case_id, stage)
+ );
+ CREATE TRIGGER IF NOT EXISTS claude_invocation_evidence_no_update
+ BEFORE UPDATE ON claude_invocation_evidence
+ BEGIN SELECT RAISE(ABORT, 'Claude invocation evidence is append-only'); END;
+ CREATE TRIGGER IF NOT EXISTS claude_invocation_evidence_no_delete
+ BEFORE DELETE ON claude_invocation_evidence
+ BEGIN SELECT RAISE(ABORT, 'Claude invocation evidence is append-only'); END;
+ `);
+ }
+
+ append(value: ClaudeInvocationEvidence, insertedAtMs = Date.now()): ClaudeInvocationEvidenceRecord {
+ const evidence = evidenceSchema.parse(value);
+ if (!Number.isSafeInteger(insertedAtMs) || insertedAtMs < 0) {
+ throw new Error("Claude invocation evidence insertion time is invalid");
+ }
+ return this.#db.transaction(() => {
+ const existing = this.#db.prepare(
+ "SELECT * FROM claude_invocation_evidence WHERE case_id = ? AND stage = ?"
+ ).get(evidence.caseId, evidence.stage) as EvidenceRow | undefined;
+ if (existing) {
+ if (stableJson(fromRow(existing)) !== stableJson(evidence)) {
+ throw new Error(`Claude invocation evidence collision for ${evidence.caseId}:${evidence.stage}`);
+ }
+ return recordFromRow(existing);
+ }
+ const previous = this.#db.prepare(
+ "SELECT entry_hash FROM claude_invocation_evidence ORDER BY sequence DESC LIMIT 1"
+ ).get() as { entry_hash: string } | undefined;
+ const previousHash = previous?.entry_hash ?? GENESIS_HASH;
+ const entryId = `${evidence.caseId}:${evidence.stage}`;
+ const payload = stableJson({ evidence, insertedAtMs });
+ const entryHash = sha256(`${previousHash}\n${entryId}\n${payload}`);
+ const inserted = this.#db.prepare(`
+ INSERT INTO claude_invocation_evidence
+ (entry_id, case_id, stage, invocation_class, model, prompt_version, prompt_sha256, response_sha256,
+ billing_evidence_sha256, usage_json, actual_cost_nano_usd, previous_hash,
+ entry_hash, inserted_at_ms)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ entryId,
+ evidence.caseId,
+ evidence.stage,
+ evidence.invocationClass,
+ evidence.model,
+ evidence.promptVersion,
+ evidence.promptSha256,
+ evidence.responseSha256,
+ evidence.billingEvidenceSha256,
+ stableJson(evidence.usage),
+ evidence.actualCostNanoUsd,
+ previousHash,
+ entryHash,
+ insertedAtMs
+ );
+ const row = this.#db.prepare(
+ "SELECT * FROM claude_invocation_evidence WHERE sequence = ?"
+ ).get(Number(inserted.lastInsertRowid)) as EvidenceRow | undefined;
+ if (!row) throw new Error("Claude invocation evidence append was not durable");
+ return recordFromRow(row);
+ })();
+ }
+
+ entries(caseId?: string): ClaudeInvocationEvidence[] {
+ const rows = caseId === undefined
+ ? this.#db.prepare("SELECT * FROM claude_invocation_evidence ORDER BY sequence").all()
+ : this.#db.prepare(
+ "SELECT * FROM claude_invocation_evidence WHERE case_id = ? ORDER BY sequence"
+ ).all(caseId);
+ return (rows as EvidenceRow[]).map(fromRow);
+ }
+
+ records(caseId?: string): ClaudeInvocationEvidenceRecord[] {
+ const rows = caseId === undefined
+ ? this.#db.prepare("SELECT * FROM claude_invocation_evidence ORDER BY sequence").all()
+ : this.#db.prepare(
+ "SELECT * FROM claude_invocation_evidence WHERE case_id = ? ORDER BY sequence"
+ ).all(caseId);
+ return (rows as EvidenceRow[]).map(recordFromRow);
+ }
+
+ verifyChain(): ClaudeInvocationEvidenceChainVerification {
+ let previousHash = GENESIS_HASH;
+ let rows = 0;
+ for (const row of this.#db.prepare(
+ "SELECT * FROM claude_invocation_evidence ORDER BY sequence"
+ ).iterate() as Iterable) {
+ if (!Number.isSafeInteger(row.sequence) || row.sequence <= 0) {
+ throw new Error("Claude invocation evidence sequence is invalid");
+ }
+ if (!Number.isSafeInteger(row.inserted_at_ms) || row.inserted_at_ms < 0) {
+ throw new Error(`Claude invocation evidence insertion time is invalid at sequence ${row.sequence}`);
+ }
+ if (row.previous_hash !== previousHash) {
+ throw new Error(`Broken Claude invocation evidence link at ${row.case_id}:${row.stage}`);
+ }
+ const evidence = fromRow(row);
+ const entryId = `${evidence.caseId}:${evidence.stage}`;
+ if (row.entry_id !== entryId) {
+ throw new Error(`Broken Claude invocation evidence identity at sequence ${row.sequence}`);
+ }
+ const expectedHash = sha256(
+ `${previousHash}\n${entryId}\n${stableJson({ evidence, insertedAtMs: row.inserted_at_ms })}`
+ );
+ if (row.entry_hash !== expectedHash) {
+ throw new Error(`Broken Claude invocation evidence hash at ${entryId}`);
+ }
+ previousHash = row.entry_hash;
+ rows += 1;
+ }
+ return { valid: true, rows, headHash: previousHash };
+ }
+
+ close(): void {
+ this.#db.close();
+ }
+}
+
+export type ClaudeInvocationEvidenceChainVerification = {
+ valid: true;
+ rows: number;
+ headHash: string;
+};
diff --git a/src/agents/claude-pricing.ts b/src/agents/claude-pricing.ts
new file mode 100644
index 0000000..159c0c9
--- /dev/null
+++ b/src/agents/claude-pricing.ts
@@ -0,0 +1,64 @@
+export const CLAUDE_MODEL = {
+ triage: "claude-haiku-4-5",
+ analyst: "claude-opus-4-8"
+} as const;
+
+export type ClaudeModel = (typeof CLAUDE_MODEL)[keyof typeof CLAUDE_MODEL];
+
+export type ClaudePricing = {
+ inputNanoUsdPerToken: number;
+ outputNanoUsdPerToken: number;
+ cacheWriteNanoUsdPerToken: number;
+ cacheReadNanoUsdPerToken: number;
+};
+
+// Anthropic API pricing verified 2026-07-12. Nano-USD keeps fractional
+// micro-dollar cache rates exact while all spend arithmetic remains integer.
+export const CLAUDE_PRICING: Record = {
+ "claude-haiku-4-5": {
+ inputNanoUsdPerToken: 1_000,
+ outputNanoUsdPerToken: 5_000,
+ cacheWriteNanoUsdPerToken: 1_250,
+ cacheReadNanoUsdPerToken: 100
+ },
+ "claude-opus-4-8": {
+ inputNanoUsdPerToken: 5_000,
+ outputNanoUsdPerToken: 25_000,
+ cacheWriteNanoUsdPerToken: 6_250,
+ cacheReadNanoUsdPerToken: 500
+ }
+};
+
+export const CLAUDE_OPERATING_TARGET_NANO_USD = 200_000_000_000;
+export const CLAUDE_HARD_CEILING_NANO_USD = 300_000_000_000;
+
+export type ClaudeUsage = {
+ inputTokens: number;
+ outputTokens: number;
+ cacheCreationInputTokens: number;
+ cacheReadInputTokens: number;
+};
+
+export function claudeUsageCostNanoUsd(model: ClaudeModel, usage: ClaudeUsage): number {
+ const pricing = CLAUDE_PRICING[model];
+ return (
+ usage.inputTokens * pricing.inputNanoUsdPerToken +
+ usage.outputTokens * pricing.outputNanoUsdPerToken +
+ usage.cacheCreationInputTokens * pricing.cacheWriteNanoUsdPerToken +
+ usage.cacheReadInputTokens * pricing.cacheReadNanoUsdPerToken
+ );
+}
+
+export function claudeWorstCaseCostNanoUsd(input: {
+ model: ClaudeModel;
+ maximumInputTokens: number;
+ maximumOutputTokens: number;
+}): number {
+ const pricing = CLAUDE_PRICING[input.model];
+ return (
+ input.maximumInputTokens * Math.max(
+ pricing.inputNanoUsdPerToken,
+ pricing.cacheWriteNanoUsdPerToken
+ ) + input.maximumOutputTokens * pricing.outputNanoUsdPerToken
+ );
+}
diff --git a/src/agents/claude-spend-ledger.ts b/src/agents/claude-spend-ledger.ts
new file mode 100644
index 0000000..85e4035
--- /dev/null
+++ b/src/agents/claude-spend-ledger.ts
@@ -0,0 +1,272 @@
+import { createHash } from "node:crypto";
+import { mkdirSync } from "node:fs";
+import { dirname } from "node:path";
+import Database from "better-sqlite3";
+import { stableJson } from "../domain/json.js";
+import {
+ CLAUDE_HARD_CEILING_NANO_USD,
+ claudeUsageCostNanoUsd,
+ type ClaudeModel,
+ type ClaudeUsage
+} from "./claude-pricing.js";
+
+const GENESIS_HASH = "0".repeat(64);
+
+type ReservationRow = {
+ request_id: string;
+ case_id: string;
+ stage: ClaudeStage;
+ model: ClaudeModel;
+ maximum_cost_nano_usd: number;
+};
+
+export type ClaudeStage = "triage" | "analyst" | "smoke";
+export type ClaudeSettlementStatus = "success" | "response_invalid" | "request_rejected" | "billing_unknown";
+
+export type ClaudeSpendSummary = {
+ actualCostNanoUsd: number;
+ outstandingReservedNanoUsd: number;
+ hardCeilingNanoUsd: number;
+ remainingNanoUsd: number;
+};
+
+function sha256(value: string): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+function assertNonnegativeInteger(value: number, name: string): void {
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a nonnegative safe integer`);
+}
+
+export class ClaudeSpendLedger {
+ readonly #db: Database.Database;
+ readonly hardCeilingNanoUsd: number;
+
+ constructor(path: string, hardCeilingNanoUsd = CLAUDE_HARD_CEILING_NANO_USD) {
+ assertNonnegativeInteger(hardCeilingNanoUsd, "hardCeilingNanoUsd");
+ if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
+ this.hardCeilingNanoUsd = hardCeilingNanoUsd;
+ this.#db = new Database(path);
+ this.#db.pragma("journal_mode = WAL");
+ this.#db.pragma("synchronous = FULL");
+ this.#db.exec(`
+ CREATE TABLE IF NOT EXISTS claude_spend_events (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ entry_id TEXT NOT NULL UNIQUE,
+ request_id TEXT NOT NULL,
+ case_id TEXT NOT NULL,
+ stage TEXT NOT NULL,
+ model TEXT NOT NULL,
+ kind TEXT NOT NULL CHECK(kind IN ('reservation', 'settlement')),
+ maximum_cost_nano_usd INTEGER NOT NULL,
+ actual_cost_nano_usd INTEGER,
+ status TEXT,
+ usage_json TEXT,
+ previous_hash TEXT NOT NULL,
+ entry_hash TEXT NOT NULL UNIQUE,
+ inserted_at_ms INTEGER NOT NULL
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS claude_spend_one_reservation
+ ON claude_spend_events(request_id) WHERE kind = 'reservation';
+ CREATE UNIQUE INDEX IF NOT EXISTS claude_spend_one_settlement
+ ON claude_spend_events(request_id) WHERE kind = 'settlement';
+ CREATE TRIGGER IF NOT EXISTS claude_spend_no_update
+ BEFORE UPDATE ON claude_spend_events BEGIN SELECT RAISE(ABORT, 'Claude spend ledger is append-only'); END;
+ CREATE TRIGGER IF NOT EXISTS claude_spend_no_delete
+ BEFORE DELETE ON claude_spend_events BEGIN SELECT RAISE(ABORT, 'Claude spend ledger is append-only'); END;
+ `);
+ }
+
+ reserve(input: {
+ requestId: string;
+ caseId: string;
+ stage: ClaudeStage;
+ model: ClaudeModel;
+ maximumCostNanoUsd: number;
+ insertedAtMs?: number;
+ }): void {
+ assertNonnegativeInteger(input.maximumCostNanoUsd, "maximumCostNanoUsd");
+ this.#db.transaction(() => {
+ const existing = this.#reservation(input.requestId);
+ if (existing) {
+ if (
+ existing.case_id !== input.caseId ||
+ existing.stage !== input.stage ||
+ existing.model !== input.model ||
+ existing.maximum_cost_nano_usd !== input.maximumCostNanoUsd
+ ) {
+ throw new Error(`Claude request ID collision: ${input.requestId}`);
+ }
+ return;
+ }
+ const summary = this.#summary();
+ const projected = summary.actualCostNanoUsd + summary.outstandingReservedNanoUsd + input.maximumCostNanoUsd;
+ if (projected > this.hardCeilingNanoUsd) {
+ throw new Error(`Claude hard spend ceiling would be exceeded by request ${input.requestId}`);
+ }
+ this.#append({
+ entryId: `${input.requestId}:reservation`,
+ requestId: input.requestId,
+ caseId: input.caseId,
+ stage: input.stage,
+ model: input.model,
+ kind: "reservation",
+ maximumCostNanoUsd: input.maximumCostNanoUsd,
+ actualCostNanoUsd: null,
+ status: null,
+ usageJson: null,
+ insertedAtMs: input.insertedAtMs ?? Date.now()
+ });
+ })();
+ }
+
+ settle(input: {
+ requestId: string;
+ status: ClaudeSettlementStatus;
+ usage?: ClaudeUsage;
+ insertedAtMs?: number;
+ }): void {
+ this.#db.transaction(() => {
+ const reservation = this.#reservation(input.requestId);
+ if (!reservation) throw new Error(`No Claude spend reservation for ${input.requestId}`);
+ const existing = this.#db.prepare(
+ "SELECT entry_id FROM claude_spend_events WHERE request_id = ? AND kind = 'settlement'"
+ ).get(input.requestId) as { entry_id: string } | undefined;
+ if (existing) throw new Error(`Claude request already settled: ${input.requestId}`);
+
+ const actualCostNanoUsd = input.status === "billing_unknown"
+ ? reservation.maximum_cost_nano_usd
+ : claudeUsageCostNanoUsd(reservation.model, input.usage ?? {
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheCreationInputTokens: 0,
+ cacheReadInputTokens: 0
+ });
+ if (actualCostNanoUsd > reservation.maximum_cost_nano_usd) {
+ throw new Error(`Claude actual cost exceeded reservation for ${input.requestId}`);
+ }
+ this.#append({
+ entryId: `${input.requestId}:settlement`,
+ requestId: input.requestId,
+ caseId: reservation.case_id,
+ stage: reservation.stage,
+ model: reservation.model,
+ kind: "settlement",
+ maximumCostNanoUsd: reservation.maximum_cost_nano_usd,
+ actualCostNanoUsd,
+ status: input.status,
+ usageJson: input.usage === undefined ? null : stableJson(input.usage),
+ insertedAtMs: input.insertedAtMs ?? Date.now()
+ });
+ })();
+ }
+
+ summary(): ClaudeSpendSummary {
+ return this.#summary();
+ }
+
+ verifyChain(): { valid: true; rows: number; headHash: string } {
+ let previousHash = GENESIS_HASH;
+ let rows = 0;
+ for (const row of this.#db.prepare(
+ "SELECT entry_id, request_id, case_id, stage, model, kind, maximum_cost_nano_usd, actual_cost_nano_usd, status, usage_json, previous_hash, entry_hash FROM claude_spend_events ORDER BY sequence"
+ ).iterate() as Iterable>) {
+ if (row.previous_hash !== previousHash) throw new Error(`Broken Claude spend link at ${String(row.entry_id)}`);
+ const payload = stableJson({
+ requestId: row.request_id,
+ caseId: row.case_id,
+ stage: row.stage,
+ model: row.model,
+ kind: row.kind,
+ maximumCostNanoUsd: row.maximum_cost_nano_usd,
+ actualCostNanoUsd: row.actual_cost_nano_usd,
+ status: row.status,
+ usageJson: row.usage_json
+ });
+ const expected = sha256(`${previousHash}\n${String(row.entry_id)}\n${payload}`);
+ if (row.entry_hash !== expected) throw new Error(`Broken Claude spend hash at ${String(row.entry_id)}`);
+ previousHash = String(row.entry_hash);
+ rows += 1;
+ }
+ return { valid: true, rows, headHash: previousHash };
+ }
+
+ close(): void {
+ this.#db.close();
+ }
+
+ #summary(): ClaudeSpendSummary {
+ const row = this.#db.prepare(`
+ SELECT
+ COALESCE(SUM(CASE WHEN kind = 'settlement' THEN actual_cost_nano_usd ELSE 0 END), 0) AS actual,
+ COALESCE(SUM(CASE WHEN kind = 'reservation' AND request_id NOT IN (
+ SELECT request_id FROM claude_spend_events WHERE kind = 'settlement'
+ ) THEN maximum_cost_nano_usd ELSE 0 END), 0) AS outstanding
+ FROM claude_spend_events
+ `).get() as { actual: number; outstanding: number };
+ return {
+ actualCostNanoUsd: row.actual,
+ outstandingReservedNanoUsd: row.outstanding,
+ hardCeilingNanoUsd: this.hardCeilingNanoUsd,
+ remainingNanoUsd: this.hardCeilingNanoUsd - row.actual - row.outstanding
+ };
+ }
+
+ #reservation(requestId: string): ReservationRow | undefined {
+ return this.#db.prepare(`
+ SELECT request_id, case_id, stage, model, maximum_cost_nano_usd
+ FROM claude_spend_events WHERE request_id = ? AND kind = 'reservation'
+ `).get(requestId) as ReservationRow | undefined;
+ }
+
+ #append(input: {
+ entryId: string;
+ requestId: string;
+ caseId: string;
+ stage: ClaudeStage;
+ model: ClaudeModel;
+ kind: "reservation" | "settlement";
+ maximumCostNanoUsd: number;
+ actualCostNanoUsd: number | null;
+ status: ClaudeSettlementStatus | null;
+ usageJson: string | null;
+ insertedAtMs: number;
+ }): void {
+ const previous = this.#db.prepare(
+ "SELECT entry_hash FROM claude_spend_events ORDER BY sequence DESC LIMIT 1"
+ ).get() as { entry_hash: string } | undefined;
+ const previousHash = previous?.entry_hash ?? GENESIS_HASH;
+ const payload = stableJson({
+ requestId: input.requestId,
+ caseId: input.caseId,
+ stage: input.stage,
+ model: input.model,
+ kind: input.kind,
+ maximumCostNanoUsd: input.maximumCostNanoUsd,
+ actualCostNanoUsd: input.actualCostNanoUsd,
+ status: input.status,
+ usageJson: input.usageJson
+ });
+ const entryHash = sha256(`${previousHash}\n${input.entryId}\n${payload}`);
+ this.#db.prepare(`
+ INSERT INTO claude_spend_events
+ (entry_id, request_id, case_id, stage, model, kind, maximum_cost_nano_usd,
+ actual_cost_nano_usd, status, usage_json, previous_hash, entry_hash, inserted_at_ms)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ input.entryId,
+ input.requestId,
+ input.caseId,
+ input.stage,
+ input.model,
+ input.kind,
+ input.maximumCostNanoUsd,
+ input.actualCostNanoUsd,
+ input.status,
+ input.usageJson,
+ previousHash,
+ entryHash,
+ input.insertedAtMs
+ );
+ }
+}
diff --git a/src/agents/claude.ts b/src/agents/claude.ts
new file mode 100644
index 0000000..48a82fd
--- /dev/null
+++ b/src/agents/claude.ts
@@ -0,0 +1,381 @@
+import { createHash, randomUUID } from "node:crypto";
+import Anthropic from "@anthropic-ai/sdk";
+import type { APIError } from "@anthropic-ai/sdk/core/error";
+import type {
+ Message,
+ MessageCreateParamsNonStreaming,
+ Tool
+} from "@anthropic-ai/sdk/resources/messages/messages";
+import { transformJSONSchema } from "@anthropic-ai/sdk/lib/transform-json-schema";
+import { z, type ZodType } from "zod";
+import { stableJson } from "../domain/json.js";
+import {
+ tradeThesisSchema,
+ triageDecisionSchema,
+ type AnalystAgent,
+ type TriageAgent
+} from "./contracts.js";
+import {
+ CLAUDE_MODEL,
+ claudeUsageCostNanoUsd,
+ claudeWorstCaseCostNanoUsd,
+ type ClaudeModel,
+ type ClaudeUsage
+} from "./claude-pricing.js";
+import { ClaudeSpendLedger, type ClaudeStage } from "./claude-spend-ledger.js";
+
+export const CLAUDE_PROMPT_VERSION = {
+ triage: "triage-v1",
+ analyst: "analyst-v1"
+} as const;
+
+const TRIAGE_SYSTEM = `You are Samaritan's bounded signal triage component.
+Classify only the supplied deterministic detector signal. Escalate only when the evidence is coherent enough for analyst review.
+You cannot trade, size a position, construct an order, change detector eligibility, or override risk rules.
+Your entire response must be one submit_triage tool call.`;
+
+const ANALYST_SYSTEM = `You are Samaritan's bounded sports-market analyst.
+Judge only the supplied detector signal and evidence. Steelman the opposing case and state concrete invalidation conditions.
+You cannot size a position, construct or place an order, access a wallet, authenticate to a venue, or override deterministic risk rules.
+The recommendation is paper-only. Your only valid exit is one submit_thesis tool call. Never answer in prose.`;
+
+type RequestOptions = {
+ timeout?: number;
+ maxRetries?: number;
+ signal?: AbortSignal;
+ idempotencyKey?: string;
+};
+
+export type ClaudeMessagesClient = {
+ create(params: MessageCreateParamsNonStreaming, options?: RequestOptions): Promise;
+};
+
+const anthropicApiClients = new WeakSet();
+
+export function createAnthropicMessagesClient(apiKey: string): ClaudeMessagesClient {
+ if (apiKey.length === 0) throw new Error("ANTHROPIC_API_KEY is required");
+ const client = new Anthropic({ apiKey });
+ const messagesClient: ClaudeMessagesClient = Object.freeze({
+ create: (params, options) => client.messages.create(params, options)
+ });
+ anthropicApiClients.add(messagesClient);
+ return messagesClient;
+}
+
+export function isAnthropicApiMessagesClient(client: ClaudeMessagesClient): boolean {
+ return anthropicApiClients.has(client);
+}
+
+export type ClaudeInvocationEvidence = {
+ caseId: string;
+ stage: "triage" | "analyst";
+ invocationClass: "anthropic_api" | "injected_client";
+ model: ClaudeModel;
+ promptVersion: string;
+ promptSha256: string;
+ responseSha256: string;
+ billingEvidenceSha256: string;
+ usage: ClaudeUsage;
+ actualCostNanoUsd: number;
+};
+
+export type ClaudeEvidenceSink = (
+ evidence: ClaudeInvocationEvidence
+) => void | Promise;
+
+type AdapterConfig = {
+ client: ClaudeMessagesClient;
+ spendLedger: ClaudeSpendLedger;
+ requestId?: () => string;
+ evidenceSink?: ClaudeEvidenceSink;
+};
+
+type Invocation = {
+ caseId: string;
+ stage: Extract;
+ model: ClaudeModel;
+ promptVersion: string;
+ toolName: string;
+ schema: ZodType;
+ maximumInputTokens: number;
+ maximumOutputTokens: number;
+ timeoutMs: number;
+ haltSignal?: AbortSignal;
+ params: MessageCreateParamsNonStreaming;
+};
+
+function tool(name: string, description: string, schema: ZodType): Tool {
+ const jsonSchema = transformJSONSchema(z.toJSONSchema(schema));
+ if (jsonSchema.type !== "object") throw new Error(`Tool schema ${name} must be an object`);
+ return {
+ name,
+ description,
+ input_schema: jsonSchema as Tool.InputSchema,
+ strict: true
+ };
+}
+
+function usage(message: Message): ClaudeUsage {
+ return {
+ inputTokens: message.usage.input_tokens,
+ outputTokens: message.usage.output_tokens,
+ cacheCreationInputTokens: message.usage.cache_creation_input_tokens ?? 0,
+ cacheReadInputTokens: message.usage.cache_read_input_tokens ?? 0
+ };
+}
+
+function responseToolInput(message: Message, expectedToolName: string): unknown {
+ if (message.stop_reason !== "tool_use") {
+ throw new Error(`Claude response stopped with ${message.stop_reason ?? "no stop reason"}`);
+ }
+ const toolUses = message.content.filter((block) => block.type === "tool_use");
+ if (toolUses.length !== 1 || toolUses[0]?.name !== expectedToolName) {
+ throw new Error(`Claude response must contain exactly one ${expectedToolName} call`);
+ }
+ const unexpected = message.content.filter((block) =>
+ block.type !== "tool_use" && block.type !== "thinking" && block.type !== "redacted_thinking"
+ );
+ if (unexpected.length > 0) throw new Error("Claude response contained content outside the submission tool");
+ return toolUses[0].input;
+}
+
+function safeApiError(error: APIError): string {
+ const detail = error.message
+ .replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]")
+ .replace(/\s+/g, " ")
+ .slice(0, 500);
+ return `${error.status ?? "unknown"} ${error.type ?? error.name}: ${detail}`;
+}
+
+function sha256(value: string): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+export class BoundedClaudeInvoker {
+ readonly #client: ClaudeMessagesClient;
+ readonly #spendLedger: ClaudeSpendLedger;
+ readonly #requestId: () => string;
+ readonly #evidenceSink: ClaudeEvidenceSink | undefined;
+ readonly #invocationClass: ClaudeInvocationEvidence["invocationClass"];
+
+ constructor(config: AdapterConfig) {
+ this.#client = config.client;
+ this.#spendLedger = config.spendLedger;
+ this.#requestId = config.requestId ?? randomUUID;
+ this.#evidenceSink = config.evidenceSink;
+ this.#invocationClass = anthropicApiClients.has(config.client)
+ ? "anthropic_api"
+ : "injected_client";
+ }
+
+ async invoke(input: Invocation): Promise {
+ input.haltSignal?.throwIfAborted();
+ const requestId = this.#requestId();
+ // UTF-8 bytes are a deliberately loose token upper bound. The fixed allowance
+ // covers Anthropic's hidden tool-use prompt and request framing.
+ const maximumInputTokens = Buffer.byteLength(stableJson(input.params), "utf8") + 4_096;
+ if (maximumInputTokens > input.maximumInputTokens) {
+ throw new Error(`${input.stage} Claude request exceeds its input bound`);
+ }
+ const maximumCostNanoUsd = claudeWorstCaseCostNanoUsd({
+ model: input.model,
+ maximumInputTokens,
+ maximumOutputTokens: input.maximumOutputTokens
+ });
+ this.#spendLedger.reserve({
+ requestId,
+ caseId: input.caseId,
+ stage: input.stage,
+ model: input.model,
+ maximumCostNanoUsd
+ });
+
+ let message: Message;
+ try {
+ message = await this.#client.create(input.params, {
+ timeout: input.timeoutMs,
+ maxRetries: 1,
+ ...(input.haltSignal === undefined ? {} : { signal: input.haltSignal }),
+ idempotencyKey: requestId
+ });
+ } catch (error) {
+ if (error instanceof Anthropic.APIError && error.status !== undefined && error.status < 500) {
+ this.#spendLedger.settle({ requestId, status: "request_rejected" });
+ throw new Error(`${input.stage} Claude request rejected (${safeApiError(error)})`);
+ }
+ this.#spendLedger.settle({ requestId, status: "billing_unknown" });
+ throw new Error(`${input.stage} Claude request failed`);
+ }
+
+ const measuredUsage = usage(message);
+ let parsed: unknown;
+ try {
+ if (message.model !== input.model) {
+ throw new Error(
+ `${input.stage} Claude response model mismatch: expected ${input.model}, received ${message.model}`
+ );
+ }
+ const toolInput = responseToolInput(message, input.toolName);
+ parsed = input.schema.parse(toolInput);
+ } catch (error) {
+ this.#spendLedger.settle({ requestId, status: "response_invalid", usage: measuredUsage });
+ throw error;
+ }
+ this.#spendLedger.settle({ requestId, status: "success", usage: measuredUsage });
+ if (this.#evidenceSink) {
+ const actualCostNanoUsd = claudeUsageCostNanoUsd(input.model, measuredUsage);
+ const requestIdSha256 = sha256(requestId);
+ const promptSha256 = sha256(`samaritan.claude.prompt/v1\n${stableJson(input.params)}`);
+ const responseSha256 = sha256(`samaritan.claude.response/v1\n${stableJson(parsed)}`);
+ await this.#evidenceSink({
+ caseId: input.caseId,
+ stage: input.stage,
+ invocationClass: this.#invocationClass,
+ model: input.model,
+ promptVersion: input.promptVersion,
+ promptSha256,
+ responseSha256,
+ billingEvidenceSha256: sha256(`samaritan.claude.billing-evidence/v1\n${stableJson({
+ requestIdSha256,
+ providerMessageIdSha256: sha256(message.id),
+ caseId: input.caseId,
+ stage: input.stage,
+ invocationClass: this.#invocationClass,
+ model: input.model,
+ responseModel: message.model,
+ promptSha256,
+ responseSha256,
+ status: "success",
+ usage: measuredUsage,
+ actualCostNanoUsd
+ })}`),
+ usage: measuredUsage,
+ actualCostNanoUsd
+ });
+ }
+ return parsed;
+ }
+}
+
+export class ClaudeTriageAgent implements TriageAgent {
+ readonly #invoker: BoundedClaudeInvoker;
+
+ constructor(config: AdapterConfig) {
+ this.#invoker = new BoundedClaudeInvoker(config);
+ }
+
+ triage(input: Parameters[0]): Promise {
+ const submissionTool = tool(
+ "submit_triage",
+ "Submit the bounded drop-or-escalate classification. This does not execute or authorize a trade.",
+ triageDecisionSchema
+ );
+ const params: MessageCreateParamsNonStreaming = {
+ model: CLAUDE_MODEL.triage,
+ max_tokens: 512,
+ cache_control: { type: "ephemeral" },
+ system: TRIAGE_SYSTEM,
+ messages: [{
+ role: "user",
+ content: stableJson({
+ promptVersion: CLAUDE_PROMPT_VERSION.triage,
+ caseId: input.caseId,
+ signal: input.signal
+ })
+ }],
+ tools: [submissionTool],
+ tool_choice: {
+ type: "tool",
+ name: submissionTool.name,
+ disable_parallel_tool_use: true
+ }
+ };
+ return this.#invoker.invoke({
+ caseId: input.caseId,
+ stage: "triage",
+ model: CLAUDE_MODEL.triage,
+ promptVersion: CLAUDE_PROMPT_VERSION.triage,
+ toolName: submissionTool.name,
+ schema: triageDecisionSchema,
+ maximumInputTokens: 16_000,
+ maximumOutputTokens: params.max_tokens,
+ timeoutMs: 60_000,
+ ...(input.haltSignal === undefined ? {} : { haltSignal: input.haltSignal }),
+ params
+ });
+ }
+}
+
+export class ClaudeAnalystAgent implements AnalystAgent {
+ readonly #invoker: BoundedClaudeInvoker;
+
+ constructor(config: AdapterConfig) {
+ this.#invoker = new BoundedClaudeInvoker(config);
+ }
+
+ async investigate(input: Parameters[0]): Promise {
+ const nowTsMs = input.asOfTsMs;
+ if (!Number.isSafeInteger(nowTsMs) || nowTsMs < input.signal.observedAtTsMs) {
+ throw new Error("Claude analyst as-of timestamp is invalid");
+ }
+ const boundedThesisSchema = tradeThesisSchema.superRefine((thesis, context) => {
+ if (thesis.analystModel !== CLAUDE_MODEL.analyst) {
+ context.addIssue({
+ code: "custom",
+ message: "Unexpected analyst model identity",
+ path: ["analystModel"]
+ });
+ }
+ if (thesis.submittedAtTsMs < nowTsMs || thesis.expiresAtTsMs > nowTsMs + 15 * 60_000) {
+ context.addIssue({
+ code: "custom",
+ message: "Thesis timestamps are outside the bounded analysis window",
+ path: ["submittedAtTsMs"]
+ });
+ }
+ });
+ const submissionTool = tool(
+ "submit_thesis",
+ "Submit the final paper-only thesis. Never include stake, order, wallet, authentication, or execution fields.",
+ boundedThesisSchema
+ );
+ const params: MessageCreateParamsNonStreaming = {
+ model: CLAUDE_MODEL.analyst,
+ max_tokens: 8_192,
+ cache_control: { type: "ephemeral" },
+ thinking: { type: "adaptive", display: "omitted" },
+ output_config: { effort: "medium" },
+ system: ANALYST_SYSTEM,
+ messages: [{
+ role: "user",
+ content: stableJson({
+ promptVersion: CLAUDE_PROMPT_VERSION.analyst,
+ caseId: input.caseId,
+ nowTsMs,
+ maximumExpiryTsMs: nowTsMs + 15 * 60_000,
+ requiredAnalystModel: CLAUDE_MODEL.analyst,
+ triage: input.triage,
+ signal: input.signal
+ })
+ }],
+ tools: [submissionTool],
+ tool_choice: {
+ type: "auto",
+ disable_parallel_tool_use: true
+ }
+ };
+ return this.#invoker.invoke({
+ caseId: input.caseId,
+ stage: "analyst",
+ model: CLAUDE_MODEL.analyst,
+ promptVersion: CLAUDE_PROMPT_VERSION.analyst,
+ toolName: submissionTool.name,
+ schema: boundedThesisSchema,
+ maximumInputTokens: 64_000,
+ maximumOutputTokens: params.max_tokens,
+ timeoutMs: 180_000,
+ ...(input.haltSignal === undefined ? {} : { haltSignal: input.haltSignal }),
+ params
+ });
+ }
+}
diff --git a/src/agents/contracts.ts b/src/agents/contracts.ts
new file mode 100644
index 0000000..28ca066
--- /dev/null
+++ b/src/agents/contracts.ts
@@ -0,0 +1,68 @@
+import { z } from "zod";
+import type { DetectorSignal } from "../detectors/types.js";
+
+export const triageDecisionSchema = z.object({
+ decision: z.enum(["drop", "escalate"]),
+ priority: z.enum(["low", "normal", "high"]),
+ rationale: z.string().min(1).max(500)
+}).strict();
+
+export type TriageDecision = z.infer;
+
+export const tradeThesisSchema = z.object({
+ schemaVersion: z.literal(1),
+ signalId: z.string().min(1),
+ fixtureId: z.string().min(1),
+ marketKey: z.string().min(1),
+ outcome: z.enum(["home", "draw", "away", "over", "under"]),
+ direction: z.enum(["buy", "sell"]),
+ recommendation: z.enum(["paper_trade", "no_trade"]),
+ fairProbability: z.number().finite().min(0).max(1),
+ thesisSummary: z.string().min(1).max(2_000),
+ evidenceFor: z.array(z.string().min(1).max(500)).min(1).max(12),
+ steelmanAgainst: z.string().min(1).max(2_000),
+ invalidationConditions: z.array(z.string().min(1).max(500)).min(1).max(12),
+ submittedAtTsMs: z.number().int().nonnegative(),
+ expiresAtTsMs: z.number().int().nonnegative(),
+ analystModel: z.string().min(1).max(100)
+}).strict().superRefine((thesis, context) => {
+ if (thesis.expiresAtTsMs <= thesis.submittedAtTsMs) {
+ context.addIssue({
+ code: "custom",
+ message: "Thesis expiry must be after submission",
+ path: ["expiresAtTsMs"]
+ });
+ }
+});
+
+export type TradeThesis = z.infer;
+
+export type TriageAgent = {
+ triage(input: {
+ caseId: string;
+ signal: DetectorSignal;
+ haltSignal?: AbortSignal;
+ }): Promise;
+};
+
+export type AnalystAgent = {
+ investigate(input: {
+ caseId: string;
+ signal: DetectorSignal;
+ triage: TriageDecision;
+ asOfTsMs: number;
+ haltSignal?: AbortSignal;
+ }): Promise;
+};
+
+export function assertThesisMatchesSignal(thesis: TradeThesis, signal: DetectorSignal): void {
+ if (
+ thesis.signalId !== signal.signalId ||
+ thesis.fixtureId !== signal.fixtureId ||
+ thesis.marketKey !== signal.market.key ||
+ thesis.outcome !== signal.outcome ||
+ thesis.direction !== signal.direction
+ ) {
+ throw new Error("Trade thesis identity or direction does not match the detector signal");
+ }
+}
diff --git a/src/agents/haiku-smoke.ts b/src/agents/haiku-smoke.ts
new file mode 100644
index 0000000..a97372f
--- /dev/null
+++ b/src/agents/haiku-smoke.ts
@@ -0,0 +1,67 @@
+import { loadEnvFile } from "node:process";
+import { resolve } from "node:path";
+import type { DetectorSignal } from "../detectors/types.js";
+import { ClaudeTriageAgent, createAnthropicMessagesClient } from "./claude.js";
+import { CLAUDE_MODEL } from "./claude-pricing.js";
+import { ClaudeSpendLedger } from "./claude-spend-ledger.js";
+
+loadEnvFile(resolve(process.cwd(), ".env"));
+const apiKey = process.env.ANTHROPIC_API_KEY;
+if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not configured");
+
+const nowTsMs = Date.now();
+const smokeSignal: DetectorSignal = {
+ signalId: `claude-smoke-${nowTsMs}`,
+ kind: "CONSENSUS_MOVE",
+ detectedAtTsMs: nowTsMs,
+ observedAtTsMs: nowTsMs,
+ fixtureId: "synthetic-smoke-fixture",
+ market: {
+ family: "total_goals",
+ period: "full_time",
+ lineMilli: 2_500,
+ key: "synthetic-smoke-fixture:total_goals:full_time:2500"
+ },
+ outcome: "over",
+ direction: "buy",
+ eligibility: "research_only",
+ reason: "Synthetic adapter smoke; no executable market and no trade authorization.",
+ evidence: {
+ consensusProbability: 0.51,
+ polymarketProbability: 0.51,
+ consensusVelocity: 0,
+ consensusZScore: 0,
+ polymarketVelocity: 0,
+ polymarketZScore: 0,
+ cusumUp: 0,
+ cusumDown: 0,
+ rawGap: 0,
+ gapBasis: "sampled_history_proxy",
+ persistenceMs: 0,
+ mappingStatus: null,
+ scoreContextActions: []
+ }
+};
+
+const ledger = new ClaudeSpendLedger(resolve("data/agents/claude-spend.sqlite"));
+try {
+ const agent = new ClaudeTriageAgent({
+ client: createAnthropicMessagesClient(apiKey),
+ spendLedger: ledger
+ });
+ const decision = await agent.triage({
+ caseId: `case-${smokeSignal.signalId}`,
+ signal: smokeSignal
+ });
+ const summary = ledger.summary();
+ process.stdout.write(`${JSON.stringify({
+ ok: true,
+ model: CLAUDE_MODEL.triage,
+ decision,
+ spendNanoUsd: summary.actualCostNanoUsd,
+ outstandingReservedNanoUsd: summary.outstandingReservedNanoUsd,
+ spendChain: ledger.verifyChain()
+ }, null, 2)}\n`);
+} finally {
+ ledger.close();
+}
diff --git a/src/agents/opus-smoke.ts b/src/agents/opus-smoke.ts
new file mode 100644
index 0000000..ded03cb
--- /dev/null
+++ b/src/agents/opus-smoke.ts
@@ -0,0 +1,73 @@
+import { resolve } from "node:path";
+import { loadEnvFile } from "node:process";
+import type { DetectorSignal } from "../detectors/types.js";
+import { ClaudeAnalystAgent, createAnthropicMessagesClient } from "./claude.js";
+import { CLAUDE_MODEL } from "./claude-pricing.js";
+import { ClaudeSpendLedger } from "./claude-spend-ledger.js";
+
+loadEnvFile(resolve(process.cwd(), ".env"));
+const apiKey = process.env.ANTHROPIC_API_KEY;
+if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not configured");
+
+const nowTsMs = Date.now();
+const smokeSignal: DetectorSignal = {
+ signalId: `claude-opus-smoke-${nowTsMs}`,
+ kind: "CONSENSUS_MOVE",
+ detectedAtTsMs: nowTsMs,
+ observedAtTsMs: nowTsMs,
+ fixtureId: "synthetic-opus-smoke-fixture",
+ market: {
+ family: "total_goals",
+ period: "full_time",
+ lineMilli: 2_500,
+ key: "synthetic-opus-smoke-fixture:total_goals:full_time:2500"
+ },
+ outcome: "over",
+ direction: "buy",
+ eligibility: "research_only",
+ reason: "Synthetic analyst adapter smoke with no executable fixture.",
+ evidence: {
+ consensusProbability: 0.55,
+ polymarketProbability: 0.51,
+ consensusVelocity: 0.02,
+ consensusZScore: 1.2,
+ polymarketVelocity: 0,
+ polymarketZScore: 0,
+ cusumUp: 0.001,
+ cusumDown: 0,
+ rawGap: 0.04,
+ gapBasis: "sampled_history_proxy",
+ persistenceMs: 5_000,
+ mappingStatus: null,
+ scoreContextActions: []
+ }
+};
+
+const ledger = new ClaudeSpendLedger(resolve("data/agents/claude-spend.sqlite"));
+try {
+ const agent = new ClaudeAnalystAgent({
+ client: createAnthropicMessagesClient(apiKey),
+ spendLedger: ledger
+ });
+ const thesis = await agent.investigate({
+ caseId: `case-${smokeSignal.signalId}`,
+ signal: smokeSignal,
+ asOfTsMs: nowTsMs,
+ triage: {
+ decision: "escalate",
+ priority: "low",
+ rationale: "Synthetic adapter contract verification only."
+ }
+ });
+ const summary = ledger.summary();
+ process.stdout.write(`${JSON.stringify({
+ ok: true,
+ model: CLAUDE_MODEL.analyst,
+ thesis,
+ totalSpendNanoUsd: summary.actualCostNanoUsd,
+ outstandingReservedNanoUsd: summary.outstandingReservedNanoUsd,
+ spendChain: ledger.verifyChain()
+ }, null, 2)}\n`);
+} finally {
+ ledger.close();
+}
diff --git a/src/bus/events.ts b/src/bus/events.ts
index 234faae..3f72075 100644
--- a/src/bus/events.ts
+++ b/src/bus/events.ts
@@ -91,6 +91,18 @@ export type PolymarketBookEvent = CanonicalEventBase & {
tickSize: string | null;
};
+export type PolymarketResolutionEvent = CanonicalEventBase & {
+ kind: "polymarket.resolution";
+ source: "polymarket";
+ fixtureId: string;
+ market: CanonicalMarket;
+ mappingStatus: MappingStatus;
+ conditionId: string;
+ assetIds: string[];
+ winningAssetId: string;
+ winningOutcomeLabel: string;
+};
+
export type FeedEvent = CanonicalEventBase & {
kind: "feed.heartbeat" | "feed.status";
fixtureId: null;
@@ -104,6 +116,7 @@ export type CanonicalEvent =
| ScoreEvent
| PolymarketPriceEvent
| PolymarketBookEvent
+ | PolymarketResolutionEvent
| FeedEvent;
export function marketKey(
diff --git a/src/chain/decision-anchor-submit.ts b/src/chain/decision-anchor-submit.ts
new file mode 100644
index 0000000..9986675
--- /dev/null
+++ b/src/chain/decision-anchor-submit.ts
@@ -0,0 +1,230 @@
+import { spawnSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ Connection,
+ Keypair,
+ Transaction,
+ clusterApiUrl,
+ type ParsedTransactionWithMeta,
+ type Signer
+} from "@solana/web3.js";
+import { decisionReceiptSchema, verifyDecisionReceipt } from "../proof/decision-receipt-schema.js";
+import {
+ SOLANA_DEVNET_GENESIS_HASH,
+ assertDevnetOnly,
+ buildDecisionAnchorInstruction,
+ verifyDecisionAnchorIntent,
+ verifyDecisionAnchorOnChain,
+ type DecisionAnchorIntent,
+ type DecisionAnchorNetworkVerification,
+ type DecisionAnchorReadRpc
+} from "./decision-anchor.js";
+
+export const SOLANA_ANCHOR_SIGNER_PATH_ENV = "SAMARITAN_SOLANA_ANCHOR_KEYPAIR_PATH" as const;
+export const SOLANA_ANCHOR_RPC_URL_ENV = "SAMARITAN_SOLANA_DEVNET_RPC_URL" as const;
+
+export type DecisionAnchorSubmitRpc = DecisionAnchorReadRpc & {
+ getLatestBlockhash(): Promise<{ blockhash: string; lastValidBlockHeight: number }>;
+ sendRawTransaction(rawTransaction: Buffer): Promise;
+ confirmTransaction(input: {
+ signature: string;
+ blockhash: string;
+ lastValidBlockHeight: number;
+ }): Promise<{ value: { err: unknown | null } }>;
+};
+
+export type DecisionAnchorSubmitDependencies = {
+ assertSignerPathIgnored(path: string): void | Promise;
+ connect(rpcUrl: string): DecisionAnchorSubmitRpc;
+ loadSigner(path: string): Signer | Promise;
+ signAndSend(input: {
+ rpc: DecisionAnchorSubmitRpc;
+ signer: Signer;
+ intent: DecisionAnchorIntent;
+ }): Promise;
+};
+
+export type DecisionAnchorSubmitRequest = {
+ receipt: unknown;
+ intent: unknown;
+ network: string;
+ confirmedReceiptHash: string;
+ signerPath?: string;
+ humanApproved: boolean;
+ rpcUrl?: string;
+};
+
+function checkedRpcUrl(value: string | undefined): string {
+ const rpcUrl = value ?? clusterApiUrl("devnet");
+ let parsed: URL;
+ try {
+ parsed = new URL(rpcUrl);
+ } catch {
+ throw new Error("Solana RPC URL is invalid");
+ }
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
+ throw new Error("Solana RPC URL must use http or https");
+ }
+ return rpcUrl;
+}
+
+export function authorizeDecisionAnchorSubmission(input: DecisionAnchorSubmitRequest): {
+ receiptHash: string;
+ signerPath: string;
+ rpcUrl: string;
+ intent: DecisionAnchorIntent;
+} {
+ assertDevnetOnly(input.network);
+ if (!input.humanApproved) {
+ throw new Error("Human approval is required: pass the explicit devnet write approval flag");
+ }
+ if (!input.signerPath?.trim()) {
+ throw new Error(
+ `A signer path is required via --signer or ${SOLANA_ANCHOR_SIGNER_PATH_ENV}`
+ );
+ }
+ verifyDecisionReceipt(input.receipt);
+ const receipt = decisionReceiptSchema.parse(input.receipt);
+ if (input.confirmedReceiptHash !== receipt.integrity.receiptHash) {
+ throw new Error("Exact receipt-hash confirmation does not match the supplied receipt");
+ }
+ const intent = verifyDecisionAnchorIntent(input.intent, receipt);
+ return {
+ receiptHash: receipt.integrity.receiptHash,
+ signerPath: resolve(input.signerPath),
+ rpcUrl: checkedRpcUrl(input.rpcUrl),
+ intent
+ };
+}
+
+/**
+ * The only write-capable entry point. Every fail-closed authorization check,
+ * ignored-path check, and RPC genesis check runs before the signer is read.
+ */
+export async function submitDecisionAnchor(
+ input: DecisionAnchorSubmitRequest,
+ dependencies: DecisionAnchorSubmitDependencies
+): Promise {
+ const authorized = authorizeDecisionAnchorSubmission(input);
+ await dependencies.assertSignerPathIgnored(authorized.signerPath);
+ const rpc = dependencies.connect(authorized.rpcUrl);
+ const genesisHash = await rpc.getGenesisHash();
+ if (genesisHash !== SOLANA_DEVNET_GENESIS_HASH) {
+ throw new Error(
+ `Solana RPC network mismatch: anchor submission refuses genesis ${genesisHash}`
+ );
+ }
+ const signer = await dependencies.loadSigner(authorized.signerPath);
+ const signature = await dependencies.signAndSend({
+ rpc,
+ signer,
+ intent: authorized.intent
+ });
+ try {
+ return await verifyDecisionAnchorOnChain({
+ receipt: input.receipt,
+ signature,
+ network: "devnet",
+ rpc
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(
+ `Anchor transaction ${signature} was submitted; post-submit verification failed: ${message}. ` +
+ "Verify this signature before any retry."
+ );
+ }
+}
+
+function assertSignerPathIsGitIgnored(path: string): void {
+ const result = spawnSync("git", ["check-ignore", "-q", "--", path], {
+ cwd: process.cwd(),
+ stdio: "ignore"
+ });
+ if (result.status !== 0) {
+ throw new Error(
+ "Signer path is not covered by this repository's .gitignore; place it under .wallet/"
+ );
+ }
+}
+
+function loadSolanaKeypair(path: string): Keypair {
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
+ if (
+ !Array.isArray(parsed) ||
+ parsed.length !== 64 ||
+ parsed.some((value) => !Number.isInteger(value) || value < 0 || value > 255)
+ ) {
+ throw new Error("Signer file must contain a Solana 64-byte secret-key JSON array");
+ }
+ return Keypair.fromSecretKey(Uint8Array.from(parsed));
+}
+
+function createDecisionAnchorSubmitRpc(rpcUrl: string): DecisionAnchorSubmitRpc {
+ const connection = new Connection(rpcUrl, { commitment: "confirmed" });
+ return {
+ getGenesisHash: () => connection.getGenesisHash(),
+ getParsedTransaction: (signature: string): Promise =>
+ connection.getParsedTransaction(signature, {
+ commitment: "confirmed",
+ maxSupportedTransactionVersion: 0
+ }),
+ getLatestBlockhash: () => connection.getLatestBlockhash("confirmed"),
+ sendRawTransaction: (rawTransaction: Buffer) => connection.sendRawTransaction(rawTransaction, {
+ skipPreflight: false,
+ preflightCommitment: "confirmed",
+ maxRetries: 3
+ }),
+ confirmTransaction: async (confirmation) => {
+ const result = await connection.confirmTransaction(confirmation, "confirmed");
+ return { value: { err: result.value.err } };
+ }
+ };
+}
+
+async function signAndSendDecisionAnchor(input: {
+ rpc: DecisionAnchorSubmitRpc;
+ signer: Signer;
+ intent: DecisionAnchorIntent;
+}): Promise {
+ const latestBlockhash = await input.rpc.getLatestBlockhash();
+ const transaction = new Transaction({
+ feePayer: input.signer.publicKey,
+ recentBlockhash: latestBlockhash.blockhash
+ }).add(buildDecisionAnchorInstruction(input.intent));
+ transaction.sign(input.signer);
+ const signature = await input.rpc.sendRawTransaction(transaction.serialize());
+ let confirmation: { value: { err: unknown | null } };
+ try {
+ confirmation = await input.rpc.confirmTransaction({
+ signature,
+ blockhash: latestBlockhash.blockhash,
+ lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(
+ `Solana returned anchor signature ${signature}, but confirmation failed: ${message}. ` +
+ "Verify this signature before any retry."
+ );
+ }
+ if (confirmation.value.err !== null) {
+ throw new Error(`Solana anchor transaction ${signature} failed during confirmation`);
+ }
+ return signature;
+}
+
+const defaultDecisionAnchorSubmitDependencies: DecisionAnchorSubmitDependencies = {
+ assertSignerPathIgnored: assertSignerPathIsGitIgnored,
+ connect: createDecisionAnchorSubmitRpc,
+ loadSigner: loadSolanaKeypair,
+ signAndSend: signAndSendDecisionAnchor
+};
+
+/** Production wrapper: callers cannot reach the signer/send backend without the guards. */
+export function submitDecisionAnchorWithDefaultDependencies(
+ input: DecisionAnchorSubmitRequest
+): Promise {
+ return submitDecisionAnchor(input, defaultDecisionAnchorSubmitDependencies);
+}
diff --git a/src/chain/decision-anchor.ts b/src/chain/decision-anchor.ts
new file mode 100644
index 0000000..1d93783
--- /dev/null
+++ b/src/chain/decision-anchor.ts
@@ -0,0 +1,350 @@
+import {
+ PublicKey,
+ TransactionInstruction,
+ type ParsedInstruction,
+ type ParsedTransactionWithMeta,
+ type PartiallyDecodedInstruction
+} from "@solana/web3.js";
+import { z } from "zod";
+import { stableJson } from "../domain/json.js";
+import {
+ DECISION_RECEIPT_SCHEMA_VERSION,
+ decisionReceiptSchema,
+ sha256,
+ verifyDecisionReceipt,
+ type DecisionReceipt
+} from "../proof/decision-receipt-schema.js";
+
+export const DECISION_ANCHOR_SCHEMA_VERSION = 1 as const;
+export const DECISION_ANCHOR_MEMO_DOMAIN = "samaritan.decision-receipt.anchor/v1" as const;
+export const DECISION_ANCHOR_INTENT_HASH_DOMAIN =
+ "samaritan.decision-receipt.anchor-intent/v1" as const;
+export const DECISION_ANCHOR_COMMITMENT_TYPE =
+ "decision_receipt_and_ledger_head_sha256" as const;
+export const SOLANA_MEMO_PROGRAM_ID =
+ "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" as const;
+export const SOLANA_DEVNET_GENESIS_HASH =
+ "EtWTRABZaYq6iMfeYKouRu166VU2xqa1" as const;
+export const SOLANA_MAINNET_BETA_GENESIS_HASH =
+ "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as const;
+export const SOLANA_MEMO_MAX_BYTES = 566 as const;
+
+const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/);
+const signatureSchema = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{64,88}$/);
+
+export const decisionAnchorMemoPayloadSchema = z.object({
+ schemaVersion: z.literal(DECISION_ANCHOR_SCHEMA_VERSION),
+ anchorType: z.literal("samaritan_decision_receipt_anchor"),
+ network: z.literal("devnet"),
+ commitmentType: z.literal(DECISION_ANCHOR_COMMITMENT_TYPE),
+ receipt: z.object({
+ schemaVersion: z.literal(DECISION_RECEIPT_SCHEMA_VERSION),
+ hash: sha256Schema
+ }).strict(),
+ ledgerHead: z.object({
+ hashSchemaVersion: z.literal(2),
+ sequence: z.number().int().positive().safe(),
+ hash: sha256Schema
+ }).strict()
+}).strict();
+
+export type DecisionAnchorMemoPayload = z.infer;
+
+const decisionAnchorIntentBodySchema = z.object({
+ schemaVersion: z.literal(DECISION_ANCHOR_SCHEMA_VERSION),
+ intentType: z.literal("samaritan_unsigned_solana_memo_transaction_intent"),
+ network: z.literal("devnet"),
+ transaction: z.object({
+ format: z.literal("legacy"),
+ feePayer: z.literal("supplied_only_at_submit"),
+ recentBlockhash: z.literal("fetched_only_at_submit"),
+ signatures: z.literal("none"),
+ instructionCount: z.literal(1)
+ }).strict(),
+ instruction: z.object({
+ programId: z.literal(SOLANA_MEMO_PROGRAM_ID),
+ keys: z.array(z.string()).max(0),
+ dataEncoding: z.literal("utf8"),
+ data: z.string().min(1)
+ }).strict(),
+ commitment: decisionAnchorMemoPayloadSchema,
+ preparation: z.object({
+ offline: z.literal(true),
+ signerAccessed: z.literal(false),
+ networkAccessed: z.literal(false)
+ }).strict()
+}).strict();
+
+export const decisionAnchorIntentSchema = decisionAnchorIntentBodySchema.extend({
+ integrity: z.object({
+ algorithm: z.literal("sha256"),
+ canonicalization: z.literal("samaritan-stable-json-v1"),
+ domain: z.literal(DECISION_ANCHOR_INTENT_HASH_DOMAIN),
+ intentHash: sha256Schema
+ }).strict()
+}).strict();
+
+export type DecisionAnchorIntent = z.infer;
+
+export function assertDevnetOnly(network: string): asserts network is "devnet" {
+ if (network !== "devnet") {
+ throw new Error(
+ `Solana decision anchoring is devnet-only; ${network || "an empty network"} is forbidden`
+ );
+ }
+}
+
+export function buildDecisionAnchorMemoPayload(
+ receiptValue: unknown,
+ network = "devnet"
+): DecisionAnchorMemoPayload {
+ assertDevnetOnly(network);
+ verifyDecisionReceipt(receiptValue);
+ const receipt = decisionReceiptSchema.parse(receiptValue);
+ return decisionAnchorMemoPayloadSchema.parse({
+ schemaVersion: DECISION_ANCHOR_SCHEMA_VERSION,
+ anchorType: "samaritan_decision_receipt_anchor",
+ network,
+ commitmentType: DECISION_ANCHOR_COMMITMENT_TYPE,
+ receipt: {
+ schemaVersion: receipt.schemaVersion,
+ hash: receipt.integrity.receiptHash
+ },
+ ledgerHead: {
+ hashSchemaVersion: 2,
+ sequence: receipt.ledger.finalHeadSequence,
+ hash: receipt.ledger.finalHeadHash
+ }
+ });
+}
+
+export function canonicalDecisionAnchorMemo(payloadValue: unknown): string {
+ const payload = decisionAnchorMemoPayloadSchema.parse(payloadValue);
+ const memo = `${DECISION_ANCHOR_MEMO_DOMAIN}\n${stableJson(payload)}`;
+ const bytes = Buffer.byteLength(memo, "utf8");
+ if (bytes > SOLANA_MEMO_MAX_BYTES) {
+ throw new Error(`Decision anchor memo is ${bytes} bytes; maximum is ${SOLANA_MEMO_MAX_BYTES}`);
+ }
+ return memo;
+}
+
+export function parseCanonicalDecisionAnchorMemo(memo: string): DecisionAnchorMemoPayload {
+ const prefix = `${DECISION_ANCHOR_MEMO_DOMAIN}\n`;
+ if (!memo.startsWith(prefix)) throw new Error("Solana memo has the wrong Samaritan domain");
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(memo.slice(prefix.length)) as unknown;
+ } catch {
+ throw new Error("Solana memo payload is not valid JSON");
+ }
+ const payload = decisionAnchorMemoPayloadSchema.parse(parsed);
+ if (canonicalDecisionAnchorMemo(payload) !== memo) {
+ throw new Error("Solana memo payload is valid but not canonically encoded");
+ }
+ return payload;
+}
+
+function hashIntentBody(body: z.infer): string {
+ return sha256(`${DECISION_ANCHOR_INTENT_HASH_DOMAIN}\n${stableJson(body)}`);
+}
+
+/**
+ * Builds a deterministic unsigned transaction intent. This function does not
+ * construct or read a signer, request a blockhash, or make an RPC call.
+ */
+export function buildDecisionAnchorIntent(
+ receiptValue: unknown,
+ network = "devnet"
+): DecisionAnchorIntent {
+ const commitment = buildDecisionAnchorMemoPayload(receiptValue, network);
+ const memo = canonicalDecisionAnchorMemo(commitment);
+ const body = decisionAnchorIntentBodySchema.parse({
+ schemaVersion: DECISION_ANCHOR_SCHEMA_VERSION,
+ intentType: "samaritan_unsigned_solana_memo_transaction_intent",
+ network: commitment.network,
+ transaction: {
+ format: "legacy",
+ feePayer: "supplied_only_at_submit",
+ recentBlockhash: "fetched_only_at_submit",
+ signatures: "none",
+ instructionCount: 1
+ },
+ instruction: {
+ programId: SOLANA_MEMO_PROGRAM_ID,
+ keys: [],
+ dataEncoding: "utf8",
+ data: memo
+ },
+ commitment,
+ preparation: {
+ offline: true,
+ signerAccessed: false,
+ networkAccessed: false
+ }
+ });
+ return decisionAnchorIntentSchema.parse({
+ ...body,
+ integrity: {
+ algorithm: "sha256",
+ canonicalization: "samaritan-stable-json-v1",
+ domain: DECISION_ANCHOR_INTENT_HASH_DOMAIN,
+ intentHash: hashIntentBody(body)
+ }
+ });
+}
+
+export function verifyDecisionAnchorIntent(
+ intentValue: unknown,
+ receiptValue: unknown
+): DecisionAnchorIntent {
+ assertDevnetOnly(
+ typeof intentValue === "object" && intentValue !== null && "network" in intentValue
+ ? String(intentValue.network)
+ : ""
+ );
+ const intent = decisionAnchorIntentSchema.parse(intentValue);
+ const { integrity: _integrity, ...bodyValue } = intent;
+ const body = decisionAnchorIntentBodySchema.parse(bodyValue);
+ if (intent.integrity.intentHash !== hashIntentBody(body)) {
+ throw new Error("Decision anchor intent canonical hash mismatch");
+ }
+ const expected = buildDecisionAnchorIntent(receiptValue, "devnet");
+ if (stableJson(intent) !== stableJson(expected)) {
+ throw new Error("Decision anchor intent does not match the supplied decision receipt");
+ }
+ const memoPayload = parseCanonicalDecisionAnchorMemo(intent.instruction.data);
+ if (stableJson(memoPayload) !== stableJson(intent.commitment)) {
+ throw new Error("Decision anchor instruction and commitment payload differ");
+ }
+ return intent;
+}
+
+export function buildDecisionAnchorInstruction(intentValue: unknown): TransactionInstruction {
+ const intent = decisionAnchorIntentSchema.parse(intentValue);
+ assertDevnetOnly(intent.network);
+ parseCanonicalDecisionAnchorMemo(intent.instruction.data);
+ return new TransactionInstruction({
+ keys: [],
+ programId: new PublicKey(SOLANA_MEMO_PROGRAM_ID),
+ data: Buffer.from(intent.instruction.data, "utf8")
+ });
+}
+
+export type DecisionAnchorReadRpc = {
+ getGenesisHash(): Promise;
+ getParsedTransaction(signature: string): Promise;
+};
+
+function memoText(
+ instruction: ParsedInstruction | PartiallyDecodedInstruction
+): string | null {
+ if (!instruction.programId.equals(new PublicKey(SOLANA_MEMO_PROGRAM_ID))) return null;
+ if (!("parsed" in instruction)) {
+ throw new Error("Solana RPC returned an undecoded Memo instruction");
+ }
+ if (typeof instruction.parsed === "string") return instruction.parsed;
+ if (
+ typeof instruction.parsed === "object" &&
+ instruction.parsed !== null &&
+ "memo" in instruction.parsed &&
+ typeof instruction.parsed.memo === "string"
+ ) return instruction.parsed.memo;
+ throw new Error("Solana RPC returned an unsupported parsed Memo instruction");
+}
+
+export type DecisionAnchorNetworkVerification = {
+ schemaVersion: typeof DECISION_ANCHOR_SCHEMA_VERSION;
+ verificationType: "samaritan_solana_decision_anchor_verification";
+ network: "devnet";
+ genesisHash: typeof SOLANA_DEVNET_GENESIS_HASH;
+ transactionSignature: string;
+ slot: number;
+ blockTimeTsMs: number | null;
+ rpcCommitment: "confirmed";
+ memoProgramId: typeof SOLANA_MEMO_PROGRAM_ID;
+ commitmentType: typeof DECISION_ANCHOR_COMMITMENT_TYPE;
+ receiptSchemaVersion: typeof DECISION_RECEIPT_SCHEMA_VERSION;
+ receiptHash: string;
+ ledgerHeadSequence: number;
+ ledgerHeadHash: string;
+ assurance: [
+ "devnet_genesis_hash_verified",
+ "transaction_succeeded_at_confirmed_commitment",
+ "canonical_memo_matches_receipt_and_ledger_head"
+ ];
+};
+
+/**
+ * Performs only Solana read calls. It does not sign, send, persist, or mutate
+ * either the receipt or an anchor sidecar.
+ */
+export async function verifyDecisionAnchorOnChain(input: {
+ receipt: unknown;
+ signature: string;
+ network: string;
+ rpc: DecisionAnchorReadRpc;
+}): Promise {
+ assertDevnetOnly(input.network);
+ const signature = signatureSchema.parse(input.signature);
+ verifyDecisionReceipt(input.receipt);
+ const receipt: DecisionReceipt = decisionReceiptSchema.parse(input.receipt);
+ const genesisHash = await input.rpc.getGenesisHash();
+ if (genesisHash !== SOLANA_DEVNET_GENESIS_HASH) {
+ throw new Error(
+ `Solana RPC network mismatch: expected devnet genesis ${SOLANA_DEVNET_GENESIS_HASH}, received ${genesisHash}`
+ );
+ }
+ const transaction = await input.rpc.getParsedTransaction(signature);
+ if (!transaction) throw new Error(`Solana transaction ${signature} was not found at confirmed commitment`);
+ if (!transaction.transaction.signatures.includes(signature)) {
+ throw new Error("Solana RPC transaction does not contain the requested signature");
+ }
+ if (!transaction.meta) throw new Error("Solana transaction metadata is unavailable");
+ if (transaction.meta.err !== null) throw new Error("Solana anchor transaction failed on-chain");
+
+ const memos = transaction.transaction.message.instructions
+ .map(memoText)
+ .filter((value): value is string => value !== null);
+ if (memos.length !== 1) {
+ throw new Error(`Expected exactly one Solana Memo instruction, found ${memos.length}`);
+ }
+ const payload = parseCanonicalDecisionAnchorMemo(memos[0]!);
+ if (payload.network !== input.network) throw new Error("Solana memo commits the wrong network");
+ if (payload.receipt.schemaVersion !== receipt.schemaVersion) {
+ throw new Error("Solana memo commits the wrong decision receipt schema version");
+ }
+ if (payload.receipt.hash !== receipt.integrity.receiptHash) {
+ throw new Error("Solana memo receipt hash does not match the supplied receipt");
+ }
+ if (
+ payload.ledgerHead.sequence !== receipt.ledger.finalHeadSequence ||
+ payload.ledgerHead.hash !== receipt.ledger.finalHeadHash
+ ) {
+ throw new Error("Solana memo ledger head does not match the supplied receipt");
+ }
+
+ const blockTimeTsMs = transaction.blockTime === null || transaction.blockTime === undefined
+ ? null
+ : transaction.blockTime * 1_000;
+ return {
+ schemaVersion: DECISION_ANCHOR_SCHEMA_VERSION,
+ verificationType: "samaritan_solana_decision_anchor_verification",
+ network: "devnet",
+ genesisHash: SOLANA_DEVNET_GENESIS_HASH,
+ transactionSignature: signature,
+ slot: transaction.slot,
+ blockTimeTsMs,
+ rpcCommitment: "confirmed",
+ memoProgramId: SOLANA_MEMO_PROGRAM_ID,
+ commitmentType: DECISION_ANCHOR_COMMITMENT_TYPE,
+ receiptSchemaVersion: receipt.schemaVersion,
+ receiptHash: receipt.integrity.receiptHash,
+ ledgerHeadSequence: receipt.ledger.finalHeadSequence,
+ ledgerHeadHash: receipt.ledger.finalHeadHash,
+ assurance: [
+ "devnet_genesis_hash_verified",
+ "transaction_succeeded_at_confirmed_commitment",
+ "canonical_memo_matches_receipt_and_ledger_head"
+ ]
+ };
+}
diff --git a/src/chain/prepare-decision-anchor.ts b/src/chain/prepare-decision-anchor.ts
new file mode 100644
index 0000000..fba0971
--- /dev/null
+++ b/src/chain/prepare-decision-anchor.ts
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { buildDecisionAnchorIntent } from "./decision-anchor.js";
+
+function usage(): never {
+ throw new Error(
+ "Usage: pnpm anchor:prepare -- [--network devnet]"
+ );
+}
+
+function main(): void {
+ const args = process.argv.slice(2).filter((argument) => argument !== "--");
+ const receiptArgument = args[0];
+ if (!receiptArgument || receiptArgument.startsWith("--")) usage();
+ let network = "devnet";
+ for (let index = 1; index < args.length; index += 1) {
+ const argument = args[index];
+ if (argument === "--network") {
+ const value = args[index + 1];
+ if (!value) usage();
+ network = value;
+ index += 1;
+ continue;
+ }
+ usage();
+ }
+ const receiptPath = resolve(receiptArgument);
+ const receipt = JSON.parse(readFileSync(receiptPath, "utf8")) as unknown;
+ const intent = buildDecisionAnchorIntent(receipt, network);
+ process.stdout.write(`${JSON.stringify(intent, null, 2)}\n`);
+}
+
+try {
+ main();
+} catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`Decision anchor preparation failed: ${message}\n`);
+ process.exitCode = 1;
+}
diff --git a/src/chain/submit-decision-anchor.ts b/src/chain/submit-decision-anchor.ts
new file mode 100644
index 0000000..9b6611a
--- /dev/null
+++ b/src/chain/submit-decision-anchor.ts
@@ -0,0 +1,96 @@
+#!/usr/bin/env node
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ SOLANA_ANCHOR_RPC_URL_ENV,
+ SOLANA_ANCHOR_SIGNER_PATH_ENV,
+ submitDecisionAnchorWithDefaultDependencies
+} from "./decision-anchor-submit.js";
+
+type SubmitCliArguments = {
+ receiptPath: string;
+ intentPath: string;
+ network: string;
+ confirmedReceiptHash: string;
+ signerPath?: string;
+ rpcUrl?: string;
+ humanApproved: boolean;
+};
+
+function usage(): never {
+ throw new Error(
+ "Usage: pnpm anchor:submit -- --receipt --intent " +
+ "--network devnet --confirm-receipt-hash <64-hex> --approve-devnet-write " +
+ `[--signer ] [--rpc-url ] (signer env: ${SOLANA_ANCHOR_SIGNER_PATH_ENV})`
+ );
+}
+
+function parseArguments(): SubmitCliArguments {
+ const args = process.argv.slice(2).filter((argument) => argument !== "--");
+ const values = new Map();
+ let humanApproved = false;
+ for (let index = 0; index < args.length; index += 1) {
+ const argument = args[index];
+ if (argument === "--approve-devnet-write") {
+ humanApproved = true;
+ continue;
+ }
+ if (!argument?.startsWith("--")) usage();
+ const value = args[index + 1];
+ if (!value || value.startsWith("--")) usage();
+ values.set(argument, value);
+ index += 1;
+ }
+ const receiptPath = values.get("--receipt");
+ const intentPath = values.get("--intent");
+ const network = values.get("--network");
+ const confirmedReceiptHash = values.get("--confirm-receipt-hash");
+ if (!receiptPath || !intentPath || !network || !confirmedReceiptHash) usage();
+ const known = new Set([
+ "--receipt",
+ "--intent",
+ "--network",
+ "--confirm-receipt-hash",
+ "--signer",
+ "--rpc-url"
+ ]);
+ if ([...values.keys()].some((key) => !known.has(key))) usage();
+ const signerPath = values.get("--signer") ?? process.env[SOLANA_ANCHOR_SIGNER_PATH_ENV];
+ const rpcUrl = values.get("--rpc-url") ?? process.env[SOLANA_ANCHOR_RPC_URL_ENV];
+ return {
+ receiptPath: resolve(receiptPath),
+ intentPath: resolve(intentPath),
+ network,
+ confirmedReceiptHash,
+ ...(signerPath === undefined ? {} : { signerPath }),
+ ...(rpcUrl === undefined ? {} : { rpcUrl }),
+ humanApproved
+ };
+}
+
+async function main(): Promise {
+ const args = parseArguments();
+ const receipt = JSON.parse(readFileSync(args.receiptPath, "utf8")) as unknown;
+ const intent = JSON.parse(readFileSync(args.intentPath, "utf8")) as unknown;
+ const result = await submitDecisionAnchorWithDefaultDependencies({
+ receipt,
+ intent,
+ network: args.network,
+ confirmedReceiptHash: args.confirmedReceiptHash,
+ humanApproved: args.humanApproved,
+ ...(args.signerPath === undefined ? {} : { signerPath: args.signerPath }),
+ ...(args.rpcUrl === undefined ? {} : { rpcUrl: args.rpcUrl })
+ });
+ process.stdout.write(`${JSON.stringify({
+ ...result,
+ note: "Devnet write completed after explicit human approval; the source receipt was not modified."
+ }, null, 2)}\n`);
+}
+
+try {
+ await main();
+} catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`Decision anchor submission failed: ${message}\n`);
+ process.exitCode = 1;
+}
diff --git a/src/chain/verify-decision-anchor.ts b/src/chain/verify-decision-anchor.ts
new file mode 100644
index 0000000..4e56de4
--- /dev/null
+++ b/src/chain/verify-decision-anchor.ts
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { Connection, clusterApiUrl } from "@solana/web3.js";
+import { verifyDecisionAnchorOnChain, type DecisionAnchorReadRpc } from "./decision-anchor.js";
+
+function usage(): never {
+ throw new Error(
+ "Usage: pnpm anchor:verify -- --receipt --signature " +
+ "--network devnet [--rpc-url ]"
+ );
+}
+
+function argumentsMap(): Map {
+ const args = process.argv.slice(2).filter((argument) => argument !== "--");
+ const values = new Map();
+ for (let index = 0; index < args.length; index += 2) {
+ const key = args[index];
+ const value = args[index + 1];
+ if (!key?.startsWith("--") || !value || value.startsWith("--")) usage();
+ values.set(key, value);
+ }
+ const known = new Set(["--receipt", "--signature", "--network", "--rpc-url"]);
+ if ([...values.keys()].some((key) => !known.has(key))) usage();
+ return values;
+}
+
+async function main(): Promise {
+ const values = argumentsMap();
+ const receiptArgument = values.get("--receipt");
+ const signature = values.get("--signature");
+ const network = values.get("--network");
+ if (!receiptArgument || !signature || !network) usage();
+ const receiptPath = resolve(receiptArgument);
+ const receipt = JSON.parse(readFileSync(receiptPath, "utf8")) as unknown;
+ const connection = new Connection(values.get("--rpc-url") ?? clusterApiUrl("devnet"), {
+ commitment: "confirmed"
+ });
+ const rpc: DecisionAnchorReadRpc = {
+ getGenesisHash: () => connection.getGenesisHash(),
+ getParsedTransaction: (transactionSignature) => connection.getParsedTransaction(
+ transactionSignature,
+ { commitment: "confirmed", maxSupportedTransactionVersion: 0 }
+ )
+ };
+ const result = await verifyDecisionAnchorOnChain({ receipt, signature, network, rpc });
+ process.stdout.write(`${JSON.stringify({
+ file: receiptPath,
+ ...result,
+ note: "Read-only Solana verification; no receipt, sidecar, wallet, or network state was modified."
+ }, null, 2)}\n`);
+}
+
+try {
+ await main();
+} catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`Decision anchor verification failed: ${message}\n`);
+ process.exitCode = 1;
+}
diff --git a/src/config/paper-study.ts b/src/config/paper-study.ts
new file mode 100644
index 0000000..747b41f
--- /dev/null
+++ b/src/config/paper-study.ts
@@ -0,0 +1,57 @@
+import type { DetectorBankConfig } from "../detectors/bank.js";
+import type { FeatureEngineConfig } from "../features/engine.js";
+import type { TotalLineSelectorConfig } from "../research/main-total-selector.js";
+
+export const PAPER_STUDY_MINIMUM_SIGNAL_TO_KICKOFF_MS = 15 * 60_000;
+export const PAPER_STUDY_REPLAY_WINDOW_BEFORE_KICKOFF_MS = 180 * 60_000;
+
+// The historical total line must be frozen before the first detector snapshot.
+// Keeping this equal to the replay window makes the causal boundary explicit.
+export const PAPER_STUDY_TOTAL_SELECTOR_AS_OF_BEFORE_KICKOFF_MS =
+ PAPER_STUDY_REPLAY_WINDOW_BEFORE_KICKOFF_MS;
+
+export const PAPER_STUDY_ECONOMIC_CASE_CONFIG = Object.freeze({
+ version: "binary_totals_executable_buy_v1" as const,
+ marketFamily: "total_goals" as const,
+ retain: "actual_buy_only" as const,
+ complementarySell: "collapse_only_when_actual_buy_present" as const,
+ sellOnly: "drop_unproven_complementary_ask" as const,
+ matchResult: "not_in_candidate" as const
+});
+
+export const PAPER_STUDY_HISTORICAL_EVIDENCE = Object.freeze({
+ protocolId: "historical-gate-causal-economic-v4-2026-07-14" as const,
+ configurationHash: "9a4eeff928f697fc55ab5147a4dc07f611c40bb749501fc3bd92b211f24b2e54" as const,
+ status: "historical_signal_candidate_for_forward_paper_review" as const,
+ executionEvidence: "not_established_sampled_prices_only" as const
+});
+
+export const PAPER_STUDY_FEATURE_CONFIG: FeatureEngineConfig = Object.freeze({
+ velocityWindowsMs: Object.freeze([60_000, 300_000, 900_000]),
+ velocityEwmaHalfLifeMs: 900_000,
+ cusumDriftProbability: 0.0005,
+ scoreContextWindowMs: 300_000,
+ freshnessMaxAgeMs: 360_000
+});
+
+export const PAPER_STUDY_DETECTOR_CONFIG: DetectorBankConfig = Object.freeze({
+ velocityWindowMs: 300_000,
+ consensusMoveAbsZ: 1,
+ consensusCusumThreshold: 0.001,
+ consensusMinimumUpdates: 5,
+ consensusMinimumRawGap: 0.01,
+ consensusStableAbsZ: 0,
+ xmarketMinimumRawGap: 2,
+ xmarketPersistenceMs: Number.MAX_SAFE_INTEGER,
+ faderPolymarketAbsZ: Number.MAX_SAFE_INTEGER,
+ faderMinimumRawGap: 2,
+ faderPersistenceMs: Number.MAX_SAFE_INTEGER
+});
+
+export const PAPER_STUDY_TOTAL_SELECTOR_CONFIG: TotalLineSelectorConfig = Object.freeze({
+ minimumCoveragePoints: 1_000,
+ minimumVolume: 0,
+ minimumLiquidity: 0,
+ maximumDistanceFromEven: 0.15,
+ weights: Object.freeze({ balance: 1, volume: 0, liquidity: 0, coverage: 0 })
+});
diff --git a/src/dash/api.ts b/src/dash/api.ts
new file mode 100644
index 0000000..eeec650
--- /dev/null
+++ b/src/dash/api.ts
@@ -0,0 +1,117 @@
+import type { ServerResponse } from "node:http";
+import {
+ CASEBOOK_API_PATH,
+ COMMAND_API_PATH,
+ readFrozenDashboardResponse,
+ SPAIN_BELGIUM_API_PATH,
+ STUDY_API_PATH,
+ type PublicDashboardApiPath
+} from "./public-bundle.js";
+import { buildJudgeEvidenceResponse, JUDGE_EVIDENCE_API_PATH } from "./judge-evidence.js";
+import { buildNodeTxlinePulse, type NodeTxlinePulseOptions } from "./txline-pulse-node.js";
+import { TXLINE_PULSE_API_PATH } from "./txline-pulse.js";
+
+export { CASEBOOK_API_PATH, COMMAND_API_PATH, SPAIN_BELGIUM_API_PATH, STUDY_API_PATH } from "./public-bundle.js";
+export { JUDGE_EVIDENCE_API_PATH } from "./judge-evidence.js";
+export { TXLINE_PULSE_API_PATH } from "./txline-pulse.js";
+
+export type DashboardApiResult = {
+ status: number;
+ headers: Record;
+ body: string;
+};
+
+function jsonResult(status: number, value: unknown): DashboardApiResult {
+ return {
+ status,
+ headers: {
+ "cache-control": "no-store",
+ "content-type": "application/json; charset=utf-8",
+ "x-content-type-options": "nosniff"
+ },
+ body: JSON.stringify(value)
+ };
+}
+
+function frozenJsonResult(body: string, bundleSha256: string): DashboardApiResult {
+ return {
+ status: 200,
+ headers: {
+ "cache-control": "no-store",
+ "content-type": "application/json; charset=utf-8",
+ "x-content-type-options": "nosniff",
+ "x-samaritan-public-bundle": bundleSha256
+ },
+ body
+ };
+}
+
+function methodNotAllowedResult(): DashboardApiResult {
+ const result = jsonResult(405, { error: "method_not_allowed" });
+ return {
+ ...result,
+ headers: {
+ ...result.headers,
+ allow: "GET, HEAD"
+ }
+ };
+}
+
+export type DashboardApiSource = "frozen" | "private";
+
+async function privateProjection(pathname: PublicDashboardApiPath, repoRoot: string): Promise {
+ if (pathname === COMMAND_API_PATH) {
+ const { buildCommandDashboardResponse } = await import("./command-projection.js");
+ return buildCommandDashboardResponse(repoRoot);
+ }
+ if (pathname === CASEBOOK_API_PATH) {
+ const { buildCasebookDashboardResponse } = await import("./casebook-projection.js");
+ return buildCasebookDashboardResponse(repoRoot);
+ }
+ if (pathname === STUDY_API_PATH) {
+ const { buildStudyDashboardResponse } = await import("./study-projection.js");
+ return buildStudyDashboardResponse(repoRoot);
+ }
+ const { buildSpainBelgiumDashboardResponse } = await import("./projection.js");
+ return buildSpainBelgiumDashboardResponse(repoRoot);
+}
+
+export async function handleDashboardApi(
+ pathname: string,
+ repoRoot: string,
+ options: { source?: DashboardApiSource; method?: string; txlinePulse?: NodeTxlinePulseOptions } = {}
+): Promise {
+ const method = (options.method ?? "GET").toUpperCase();
+ if (pathname.startsWith("/api/") && method !== "GET" && method !== "HEAD") {
+ return methodNotAllowedResult();
+ }
+ if (pathname === TXLINE_PULSE_API_PATH) {
+ return jsonResult(200, await buildNodeTxlinePulse(repoRoot, options.txlinePulse));
+ }
+ if (pathname === JUDGE_EVIDENCE_API_PATH) {
+ try {
+ return jsonResult(200, await buildJudgeEvidenceResponse(repoRoot));
+ } catch {
+ return jsonResult(503, { error: "evidence_unavailable" });
+ }
+ }
+ if (pathname === "/api/v1/health") {
+ return jsonResult(200, { status: "ok", service: "samaritan-dashboard", readOnly: true });
+ }
+ const definition = [COMMAND_API_PATH, CASEBOOK_API_PATH, STUDY_API_PATH, SPAIN_BELGIUM_API_PATH]
+ .find((candidate) => candidate === pathname) as PublicDashboardApiPath | undefined;
+ if (definition) {
+ if (options.source === "private") return jsonResult(200, await privateProjection(definition, repoRoot));
+ const frozen = await readFrozenDashboardResponse(repoRoot, definition);
+ return frozenJsonResult(frozen.body, frozen.manifest.bundleSha256);
+ }
+ if (pathname.startsWith("/api/")) {
+ return jsonResult(404, { error: "not_found" });
+ }
+ return null;
+}
+
+export function writeApiResult(response: ServerResponse, result: DashboardApiResult, headOnly = false): void {
+ response.writeHead(result.status, result.headers);
+ response.end(headOnly ? undefined : result.body);
+}
diff --git a/src/dash/casebook-projection.ts b/src/dash/casebook-projection.ts
new file mode 100644
index 0000000..3100f2a
--- /dev/null
+++ b/src/dash/casebook-projection.ts
@@ -0,0 +1,149 @@
+import {
+ CASEBOOK_SNAPSHOT_ID,
+ type CasebookApiResponse,
+ type CasebookCaseSummary,
+ type CasebookSnapshot
+} from "./public-contract.js";
+import {
+ buildSpainBelgiumFeasibilityCorpus,
+ buildSpainBelgiumMatchroomSnapshot
+} from "./projection.js";
+
+function uniqueSorted(values: readonly string[]): string[] {
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
+}
+
+export async function buildCasebookSnapshot(repoRoot: string): Promise {
+ const [matchroom, corpus] = await Promise.all([
+ buildSpainBelgiumMatchroomSnapshot(repoRoot),
+ buildSpainBelgiumFeasibilityCorpus(repoRoot)
+ ]);
+ if (
+ matchroom.decision.disposition !== "no_trade" ||
+ matchroom.decision.capitalMovedMicros !== 0 ||
+ matchroom.decision.ordersPlaced !== 0 ||
+ matchroom.decision.walletAccessed ||
+ !matchroom.proof.identityParity
+ ) {
+ throw new Error("Casebook projection failed closed: verified refusal case changed");
+ }
+ if (
+ corpus.marketEventCases !== corpus.cases.length ||
+ corpus.marketEventCases !== matchroom.proof.gateCases ||
+ corpus.marketEventCases !== matchroom.casebookCaseCount ||
+ corpus.selectedCaseId !== matchroom.caseId ||
+ corpus.corpusCommitment !== matchroom.proof.corpusCommitment
+ ) {
+ throw new Error("Casebook projection failed closed: corpus and exemplar do not reconcile");
+ }
+ const goal = matchroom.replay.states.find((state) => state.id === "goal");
+ if (!goal) throw new Error("Casebook projection failed closed: selected case has no goal state");
+
+ const cases: CasebookCaseSummary[] = corpus.cases.map((item) => ({
+ caseId: item.caseId,
+ matchroomId: item.selectedExemplar ? matchroom.snapshotId : null,
+ selectedExemplar: item.selectedExemplar,
+ goalOrdinal: item.goalOrdinal,
+ goalClockSeconds: item.goalClockSeconds,
+ occurredAt: item.occurredAt,
+ fixtureRef: corpus.fixtureRef,
+ fixtureLabel: `${corpus.home.name} vs ${corpus.away.name}`,
+ homeCode: corpus.home.code,
+ awayCode: corpus.away.code,
+ marketFamily: item.marketFamily === "match_result" ? "Match result" : "Full-time total",
+ marketLabel: item.marketLabel,
+ lineMilli: item.lineMilli,
+ classification: item.classification,
+ detector: "STALE_QUOTE_FEASIBILITY",
+ disposition: "No trade",
+ executionOutcome: "Not executed",
+ evidenceLane: "Research only",
+ source: "Captured replay",
+ verificationStatus: "Internally reconciled",
+ reason: item.classification === "polymarket_moved_before_txline"
+ ? "Market moved before TXLine"
+ : "No material move within 30s",
+ preTriggerMarketMoveBps: item.preTriggerMarketMoveBps
+ }));
+ const summary = cases.find((item) => item.caseId === corpus.selectedCaseId);
+ if (!summary) throw new Error("Casebook projection failed closed: selected exemplar is absent from corpus");
+
+ return {
+ schemaVersion: 3,
+ snapshotId: CASEBOOK_SNAPSHOT_ID,
+ generatedAt: matchroom.generatedAt,
+ mode: "offline_artifact",
+ executionMode: "paper",
+ realMoneyGate: "closed",
+ tradeable: false,
+ statistics: {
+ totalCases: cases.length,
+ noTradeCases: cases.length,
+ executedCases: 0,
+ reconciledCases: cases.length,
+ capitalMovedMicros: matchroom.decision.capitalMovedMicros
+ },
+ corpus: {
+ unit: "goal_market_feasibility_observation",
+ coverage: "all_reported_goal_market_cases",
+ captureReplays: 1,
+ fixtureCount: 1,
+ goalEvents: corpus.goalEvents,
+ marketEventCases: corpus.marketEventCases,
+ movedBeforeTxlineCases: corpus.movedBeforeTxlineCases,
+ noMaterialRepriceCases: corpus.noMaterialRepriceCases,
+ cleanStaleWindows: corpus.cleanStaleWindows,
+ commitment: corpus.corpusCommitment,
+ assurance: corpus.corpusAssurance,
+ selectedExemplar: {
+ caseId: corpus.selectedCaseId,
+ policy: corpus.selectionPolicy,
+ detail: "The detail pane is one deterministic Match Result exemplar; the index still contains every reported goal×market observation."
+ }
+ },
+ filterOptions: {
+ fixtures: uniqueSorted(cases.map((item) => item.fixtureLabel)),
+ marketFamilies: uniqueSorted(cases.map((item) => item.marketFamily)),
+ detectors: uniqueSorted(cases.map((item) => item.detector)),
+ dispositions: uniqueSorted(cases.map((item) => item.disposition)),
+ executionOutcomes: uniqueSorted(cases.map((item) => item.executionOutcome)),
+ evidenceLanes: uniqueSorted(cases.map((item) => item.evidenceLane)),
+ sources: uniqueSorted(cases.map((item) => item.source))
+ },
+ cases,
+ selectedCase: {
+ summary,
+ match: matchroom.match,
+ decision: matchroom.decision,
+ lifecycle: matchroom.decision.stages,
+ evidenceReadout: {
+ consensusMoveFromBaselineBps: goal.consensusMoveFromBaselineBps,
+ bestBid: goal.bestBid,
+ bestAsk: goal.bestAsk,
+ spread: goal.spread,
+ preTriggerMarketMoveBps: matchroom.replay.preTriggerMarketMoveBps,
+ movementConclusion: "Polymarket moved before TXLine"
+ },
+ analysis: {
+ thesisStatus: "not_requested",
+ thesisReason: "The deterministic evidence gate rejected the opportunity before analyst escalation.",
+ invalidation: "The apparent gap is invalid if the executable market repriced before the signal reached Samaritan.",
+ costStatus: "not_applicable",
+ costMicros: 0,
+ costReason: "Execution was outside this retrospective research lane, so no operational cost calculation ran."
+ },
+ evidence: matchroom.evidence,
+ proof: matchroom.proof
+ },
+ nextEvidence: {
+ status: "waiting_for_verified_capture",
+ label: `Current ${cases.length}-observation corpus is complete`,
+ detail: "New observations appear only after another paired capture passes identity, completeness, and replay verification."
+ },
+ publicDataPolicy: matchroom.publicDataPolicy
+ };
+}
+
+export async function buildCasebookDashboardResponse(repoRoot: string): Promise {
+ return { data: await buildCasebookSnapshot(repoRoot) };
+}
diff --git a/src/dash/command-projection.ts b/src/dash/command-projection.ts
new file mode 100644
index 0000000..224ded5
--- /dev/null
+++ b/src/dash/command-projection.ts
@@ -0,0 +1,659 @@
+import { readdir, readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { z } from "zod";
+import { parseCaptureConfig, validateCaptureConfig } from "../harness/capture-config.js";
+import { parseVerifiedPairedAnalysisManifest } from "../harness/paired-capture-manifest.js";
+import { PAPER_STUDY_EVALUATION_CANDIDATE } from "../metrics/paper-study.js";
+import {
+ COMMAND_SNAPSHOT_ID,
+ FORWARD_PAPER_CONFIG_HASH,
+ FORWARD_PAPER_PROTOCOL_ID,
+ FORWARD_PAPER_REGISTERED_AT,
+ type CommandApiResponse,
+ type CommandCase,
+ type CommandFixture,
+ type CommandSnapshot
+} from "./public-contract.js";
+import { buildSpainBelgiumMatchroomSnapshot } from "./projection.js";
+
+const paperCountsSchema = z.object({
+ matches: z.number().int().nonnegative(),
+ signals: z.number().int().nonnegative(),
+ filledMatches: z.number().int().nonnegative(),
+ fills: z.number().int().nonnegative(),
+ settledFills: z.number().int().nonnegative()
+});
+
+const paperReportSchema = z.object({
+ generatedAt: z.string().datetime(),
+ protocolVersion: z.string().min(1),
+ configHash: z.string().regex(/^[a-f0-9]{64}$/),
+ realMoneyGate: z.literal("closed"),
+ fixtureUniverseGeneratedAt: z.string().datetime(),
+ lanes: z.object({
+ bounty: z.object({
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: z.string().regex(/^[a-f0-9]{64}$/) }),
+ report: z.object({ status: z.literal("exploratory"), counts: paperCountsSchema })
+ }),
+ longRun: z.object({
+ initialization: z.object({
+ startedAt: z.string().datetime(),
+ frozenConfig: z.object({
+ evaluation: z.object({
+ minimumFilledMatches: z.number().int().positive(),
+ minimumFills: z.number().int().positive()
+ })
+ })
+ }),
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: z.string().regex(/^[a-f0-9]{64}$/) }),
+ report: z.object({
+ status: z.literal("sealed"),
+ reason: z.string().min(1),
+ counts: paperCountsSchema,
+ stoppingRuleMet: z.literal(false),
+ rows: z.null(),
+ endpoints: z.null(),
+ guardrails: z.null()
+ })
+ })
+ })
+});
+
+const registeredInitializationSchema = z.object({
+ protocolVersion: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ protocolStatus: z.literal("registered"),
+ configHash: z.literal(FORWARD_PAPER_CONFIG_HASH),
+ realMoneyGate: z.literal("closed"),
+ startedAtTsMs: z.number().int().nonnegative(),
+ startedAt: z.string().datetime(),
+ registration: z.object({
+ protocolId: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ status: z.literal("registered"),
+ registeredBy: z.literal("Deborah"),
+ registeredAt: z.literal(FORWARD_PAPER_REGISTERED_AT),
+ scope: z.literal("forward_paper_only"),
+ realMoneyGate: z.literal("closed")
+ }).strict(),
+ frozenConfig: z.object({
+ evaluation: z.object({
+ minimumFilledMatches: z.number().int().positive(),
+ minimumFills: z.number().int().positive()
+ }).passthrough()
+ }).passthrough()
+}).passthrough();
+
+const registeredV2ReportSchema = z.object({
+ generatedAt: z.string().datetime(),
+ protocolVersion: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ configHash: z.literal(FORWARD_PAPER_CONFIG_HASH),
+ realMoneyGate: z.literal("closed"),
+ fixtureUniverseGeneratedAt: z.string().datetime(),
+ lanes: z.object({
+ bounty: z.object({
+ initialization: registeredInitializationSchema,
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: z.string().regex(/^[a-f0-9]{64}$/) }).passthrough(),
+ report: z.object({ status: z.literal("exploratory"), counts: paperCountsSchema }).passthrough()
+ }).passthrough(),
+ longRun: z.object({
+ initialization: registeredInitializationSchema,
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: z.string().regex(/^[a-f0-9]{64}$/) }).passthrough(),
+ report: z.object({
+ status: z.literal("sealed"),
+ reason: z.string().min(1),
+ counts: paperCountsSchema,
+ stoppingRuleMet: z.literal(false),
+ rows: z.null(),
+ endpoints: z.null(),
+ guardrails: z.null()
+ }).passthrough()
+ }).passthrough()
+ }).strict()
+}).strict();
+
+const fixtureUniverseSchema = z.object({
+ generatedAt: z.string().datetime(),
+ summary: z.object({
+ fixtures: z.number().int().nonnegative(),
+ pairedBookReplays: z.number().int().nonnegative(),
+ signalResearchOnly: z.number().int().nonnegative(),
+ longRunEligible: z.number().int().nonnegative()
+ })
+});
+
+const analysisHeaderSchema = z.object({
+ runId: z.string().min(1),
+ checkedAt: z.string().datetime(),
+ status: z.string().min(1),
+ fixtureId: z.string().min(1),
+ eventSlug: z.string().min(1)
+}).passthrough();
+
+const failedAnalysisSchema = analysisHeaderSchema.extend({
+ status: z.literal("failed_closed"),
+ failures: z.array(z.object({
+ code: z.string().min(1),
+ detail: z.string().min(1)
+ }).strict()).min(1)
+});
+
+const captureStreamCoverageSchema = z.object({
+ name: z.enum(["polymarket", "txline_odds", "txline_scores"]),
+ path: z.string().min(1),
+ bytes: z.number().int().positive(),
+ firstReceivedAt: z.string().datetime(),
+ lastReceivedAt: z.string().datetime(),
+ firstReceivedTsMs: z.number().int().nonnegative(),
+ lastReceivedTsMs: z.number().int().nonnegative()
+}).strict().superRefine((stream, context) => {
+ if (
+ Date.parse(stream.firstReceivedAt) !== stream.firstReceivedTsMs ||
+ Date.parse(stream.lastReceivedAt) !== stream.lastReceivedTsMs
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor stream ISO and millisecond timestamps disagree",
+ path: ["firstReceivedTsMs"]
+ });
+ }
+ if (stream.lastReceivedTsMs <= stream.firstReceivedTsMs) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor stream lacks a positive observation window",
+ path: ["lastReceivedTsMs"]
+ });
+ }
+});
+
+const synchronizedCaptureEvidenceSchema = z.object({
+ manifestPath: z.string().min(1),
+ windowStartUtc: z.string().datetime(),
+ windowEndUtc: z.string().datetime(),
+ synchronizedStartUtc: z.string().datetime(),
+ synchronizedEndUtc: z.string().datetime(),
+ streams: z.array(captureStreamCoverageSchema).length(3)
+}).strict().superRefine((evidence, context) => {
+ const names = new Set(evidence.streams.map((stream) => stream.name));
+ if (names.size !== 3) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor terminal evidence requires all three unique streams",
+ path: ["streams"]
+ });
+ }
+ const windowStart = Date.parse(evidence.windowStartUtc);
+ const windowEnd = Date.parse(evidence.windowEndUtc);
+ const synchronizedStart = Date.parse(evidence.synchronizedStartUtc);
+ const synchronizedEnd = Date.parse(evidence.synchronizedEndUtc);
+ const derivedStart = Math.max(...evidence.streams.map((stream) => stream.firstReceivedTsMs));
+ const derivedEnd = Math.min(...evidence.streams.map((stream) => stream.lastReceivedTsMs));
+ if (
+ windowEnd <= windowStart ||
+ synchronizedEnd <= synchronizedStart ||
+ synchronizedStart < windowStart ||
+ synchronizedStart !== derivedStart ||
+ synchronizedEnd !== derivedEnd
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor terminal evidence has an invalid synchronized window",
+ path: ["synchronizedStartUtc"]
+ });
+ }
+});
+
+const supervisorStatusSchema = z.object({
+ schemaVersion: z.literal(1),
+ captureId: z.string().min(1),
+ runLabel: z.string().min(1),
+ state: z.enum(["scheduled", "preflight", "running", "completed", "failed"]),
+ updatedAt: z.string().datetime(),
+ supervisorPid: z.number().int().positive(),
+ scheduledStartUtc: z.string().datetime(),
+ scheduledEndUtc: z.string().datetime().optional(),
+ childPid: z.number().int().positive().optional(),
+ exitCode: z.number().int().nullable().optional(),
+ error: z.string().min(1).optional(),
+ terminalEvidence: synchronizedCaptureEvidenceSchema.optional()
+}).strict().superRefine((status, context) => {
+ if (
+ status.scheduledEndUtc !== undefined &&
+ Date.parse(status.scheduledEndUtc) <= Date.parse(status.scheduledStartUtc)
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor scheduled end must follow its start",
+ path: ["scheduledEndUtc"]
+ });
+ }
+ if (status.terminalEvidence !== undefined && status.state !== "completed") {
+ context.addIssue({
+ code: "custom",
+ message: "Only a completed supervisor may carry terminal evidence",
+ path: ["terminalEvidence"]
+ });
+ }
+ if (status.state === "completed") {
+ if (status.scheduledEndUtc === undefined || status.terminalEvidence === undefined || status.exitCode !== 0) {
+ context.addIssue({
+ code: "custom",
+ message: "Completed supervisor status requires exact-window terminal evidence and exit code zero",
+ path: ["terminalEvidence"]
+ });
+ return;
+ }
+ if (
+ status.terminalEvidence.windowStartUtc !== status.scheduledStartUtc ||
+ status.terminalEvidence.windowEndUtc !== status.scheduledEndUtc
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Supervisor terminal evidence does not match its scheduled window",
+ path: ["terminalEvidence", "windowStartUtc"]
+ });
+ }
+ }
+});
+
+export type LocalCaptureArtifact = { present: boolean; value: unknown };
+
+type CaptureOutcome = Pick;
+
+async function json(path: string): Promise {
+ return JSON.parse(await readFile(path, "utf8"));
+}
+
+async function optionalJson(path: string): Promise {
+ try {
+ return { present: true, value: await json(path) };
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { present: false, value: null };
+ // A present but unreadable/malformed outcome artifact is not silently treated as absent.
+ return { present: true, value: undefined };
+ }
+}
+
+function teamCodes(eventSlug: string): { home: string; away: string } {
+ const match = /^fifwc-([a-z0-9]{3})-([a-z0-9]{3})-\d{4}-\d{2}-\d{2}$/i.exec(eventSlug);
+ if (!match) throw new Error(`Command projection cannot derive team codes from ${eventSlug}`);
+ return { home: match[1]!.toUpperCase(), away: match[2]!.toUpperCase() };
+}
+
+function unknownOutcome(source: CaptureOutcome["statusSource"], detail: string, updatedAt: string | null): CaptureOutcome {
+ return {
+ phase: "unknown",
+ statusLabel: "Outcome unknown",
+ statusDetail: detail,
+ statusSource: source,
+ statusUpdatedAt: updatedAt,
+ terminalEvidence: null
+ };
+}
+
+export function resolveCaptureOutcome(input: {
+ nowTsMs: number;
+ captureId: string;
+ runLabel: string;
+ fixtureId: string;
+ eventSlug: string;
+ captureStartUtc: string;
+ captureEndUtc: string;
+ analysisManifest: LocalCaptureArtifact;
+ supervisorStatus: LocalCaptureArtifact;
+}): CaptureOutcome {
+ const captureStartTsMs = Date.parse(input.captureStartUtc);
+ const captureEndTsMs = Date.parse(input.captureEndUtc);
+ if (input.analysisManifest.present) {
+ const header = analysisHeaderSchema.safeParse(input.analysisManifest.value);
+ if (!header.success) {
+ return unknownOutcome("analysis_manifest", "Local analysis manifest is malformed", null);
+ }
+ const checkedAtTsMs = Date.parse(header.data.checkedAt);
+ if (checkedAtTsMs <= input.nowTsMs) {
+ if (
+ header.data.runId !== input.runLabel ||
+ header.data.fixtureId !== input.fixtureId ||
+ header.data.eventSlug !== input.eventSlug
+ ) {
+ return unknownOutcome("analysis_manifest", "Local analysis identity does not match the reviewed capture", header.data.checkedAt);
+ }
+ if (header.data.status === "failed_closed") {
+ const failed = failedAnalysisSchema.safeParse(input.analysisManifest.value);
+ if (!failed.success) {
+ return unknownOutcome("analysis_manifest", "Failed-closed analysis record is malformed", header.data.checkedAt);
+ }
+ return {
+ phase: "failed",
+ statusLabel: "Failed closed",
+ statusDetail: `${failed.data.failures.length} verification failure${failed.data.failures.length === 1 ? "" : "s"} recorded`,
+ statusSource: "analysis_manifest",
+ statusUpdatedAt: failed.data.checkedAt,
+ terminalEvidence: null
+ };
+ }
+ if (header.data.status === "verified") {
+ try {
+ const verified = parseVerifiedPairedAnalysisManifest(input.analysisManifest.value);
+ if (verified === null) throw new Error("Verified analysis parser returned no record");
+ return {
+ phase: "complete",
+ statusLabel: "Verification complete",
+ statusDetail: `${verified.verification.rows.toLocaleString("en-US")} canonical events verified`,
+ statusSource: "analysis_manifest",
+ statusUpdatedAt: verified.checkedAt,
+ terminalEvidence: null
+ };
+ } catch {
+ return unknownOutcome("analysis_manifest", "Verified analysis record failed strict validation", header.data.checkedAt);
+ }
+ }
+ return unknownOutcome("analysis_manifest", "Analysis manifest has no recognized terminal outcome", header.data.checkedAt);
+ }
+ }
+
+ if (input.supervisorStatus.present) {
+ const parsed = supervisorStatusSchema.safeParse(input.supervisorStatus.value);
+ if (!parsed.success) return unknownOutcome("supervisor_status", "Local supervisor status is malformed", null);
+ const status = parsed.data;
+ const updatedAtTsMs = Date.parse(status.updatedAt);
+ if (updatedAtTsMs <= input.nowTsMs) {
+ if (
+ status.captureId !== input.captureId ||
+ status.runLabel !== input.runLabel ||
+ status.scheduledStartUtc !== input.captureStartUtc ||
+ (status.scheduledEndUtc !== undefined && status.scheduledEndUtc !== input.captureEndUtc)
+ ) {
+ return unknownOutcome("supervisor_status", "Local supervisor identity does not match the reviewed capture", status.updatedAt);
+ }
+ if (status.state === "failed") {
+ return {
+ phase: "failed",
+ statusLabel: "Supervisor failed",
+ statusDetail: "Capture supervisor recorded a fail-closed terminal state",
+ statusSource: "supervisor_status",
+ statusUpdatedAt: status.updatedAt,
+ terminalEvidence: null
+ };
+ }
+ if (status.state === "completed") {
+ const terminalEvidence = status.terminalEvidence!;
+ return {
+ phase: "complete",
+ statusLabel: "Capture complete",
+ statusDetail: "Synchronized terminal coverage recorded; analysis verification is still separate",
+ statusSource: "supervisor_status",
+ statusUpdatedAt: status.updatedAt,
+ terminalEvidence: {
+ windowStartUtc: terminalEvidence.windowStartUtc,
+ windowEndUtc: terminalEvidence.windowEndUtc,
+ synchronizedStartUtc: terminalEvidence.synchronizedStartUtc,
+ synchronizedEndUtc: terminalEvidence.synchronizedEndUtc,
+ streamCount: 3
+ }
+ };
+ }
+ if (status.state === "preflight" || status.state === "running") {
+ if (input.nowTsMs > captureEndTsMs) {
+ return unknownOutcome("supervisor_status", "Supervisor state is stale beyond the configured capture window", status.updatedAt);
+ }
+ return {
+ phase: "running",
+ statusLabel: status.state === "preflight" ? "Preflight recorded" : "Capture running",
+ statusDetail: "State comes from the local capture supervisor",
+ statusSource: "supervisor_status",
+ statusUpdatedAt: status.updatedAt,
+ terminalEvidence: null
+ };
+ }
+ if (input.nowTsMs < captureStartTsMs) {
+ return {
+ phase: "scheduled",
+ statusLabel: "Capture scheduled",
+ statusDetail: "Scheduled state recorded by the local supervisor",
+ statusSource: "supervisor_status",
+ statusUpdatedAt: status.updatedAt,
+ terminalEvidence: null
+ };
+ }
+ return unknownOutcome("supervisor_status", "Scheduled supervisor state is stale after launch time", status.updatedAt);
+ }
+ }
+
+ if (input.nowTsMs < captureStartTsMs) {
+ return {
+ phase: "scheduled",
+ statusLabel: "Capture scheduled",
+ statusDetail: "Human-confirmed capture-only config",
+ statusSource: "reviewed_config",
+ statusUpdatedAt: null,
+ terminalEvidence: null
+ };
+ }
+ return unknownOutcome("none", "No terminal analysis or current supervisor outcome is available", null);
+}
+
+async function buildFixtureSchedule(repoRoot: string, nowTsMs: number): Promise {
+ const configDir = resolve(repoRoot, "config/captures");
+ const [names, txlineFixtures, polymarketEvents] = await Promise.all([
+ readdir(configDir),
+ json(resolve(repoRoot, "samples/fixtures/mainnet-world-cup-fixtures.json")),
+ json(resolve(repoRoot, "data/live/gamma-discovery/open-world-cup-events.json"))
+ ]);
+ if (!Array.isArray(txlineFixtures) || !Array.isArray(polymarketEvents)) {
+ throw new Error("Command projection failed closed: capture identity evidence is malformed");
+ }
+
+ const fixtures: CommandFixture[] = [];
+ for (const name of names.filter((value) => value.endsWith(".json")).sort()) {
+ const configValue = await json(resolve(configDir, name));
+ if (configValue === null || typeof configValue !== "object" || !("capture" in configValue)) continue;
+ const parsedConfig = parseCaptureConfig(configValue);
+ const historicalReviewedConfig = nowTsMs > Date.parse(parsedConfig.capture.scheduledEndUtc);
+ const config = historicalReviewedConfig
+ ? parsedConfig
+ : validateCaptureConfig({ repoRoot, config: configValue, txlineFixtures, polymarketEvents, nowTsMs }).config;
+ // Historical confirmed captures remain valid display evidence after their launch
+ // window closes, but they are labelled as reviewed config rather than
+ // re-verified against today's rolling source snapshots.
+ if (config.status !== "human_confirmed_for_capture_only") {
+ throw new Error(`Command projection failed closed: ${name} is not confirmed for capture`);
+ }
+ const kickoffTsMs = Date.parse(config.txline.kickoffUtc);
+ const [analysisManifest, supervisorStatus] = await Promise.all([
+ optionalJson(resolve(repoRoot, "data/live", config.capture.runLabel, "analysis-manifest.json")),
+ optionalJson(resolve(repoRoot, "samples/_logs", `${config.capture.runLabel}.supervisor.json`))
+ ]);
+ const outcome = resolveCaptureOutcome({
+ nowTsMs,
+ captureId: config.captureId,
+ runLabel: config.capture.runLabel,
+ fixtureId: config.txline.fixtureId,
+ eventSlug: config.polymarket.eventSlug,
+ captureStartUtc: config.capture.scheduledStartUtc,
+ captureEndUtc: config.capture.scheduledEndUtc,
+ analysisManifest,
+ supervisorStatus
+ });
+ const codes = teamCodes(config.polymarket.eventSlug);
+ fixtures.push({
+ captureId: config.captureId,
+ home: { name: config.txline.home, code: codes.home },
+ away: { name: config.txline.away, code: codes.away },
+ kickoffUtc: config.txline.kickoffUtc,
+ captureStartUtc: config.capture.scheduledStartUtc,
+ captureEndUtc: config.capture.scheduledEndUtc,
+ signalCutoffUtc: new Date(kickoffTsMs - 15 * 60_000).toISOString(),
+ eventSlug: config.polymarket.eventSlug,
+ ...outcome,
+ identityStatus: historicalReviewedConfig ? "historical_reviewed_config" : "exact_match_confirmed",
+ captureOnly: true,
+ tradeable: false
+ });
+ }
+ if (fixtures.length === 0) throw new Error("Command projection failed closed: no confirmed capture fixtures");
+ return fixtures.sort((left, right) => Date.parse(left.kickoffUtc) - Date.parse(right.kickoffUtc));
+}
+
+export async function buildCommandSnapshot(repoRoot: string, nowTsMs = Date.now()): Promise {
+ if (!Number.isSafeInteger(nowTsMs)) throw new Error("Command projection requires a valid current timestamp");
+ const [matchroom, reportValue, universeValue, registeredReportValue, registeredUniverseValue, fixtureSchedule] = await Promise.all([
+ buildSpainBelgiumMatchroomSnapshot(repoRoot),
+ json(resolve(repoRoot, "data/paper/reports/current.json")),
+ json(resolve(repoRoot, "data/research/paper-fixture-universe.json")),
+ json(resolve(repoRoot, "data/paper/v2/reports/current.json")),
+ json(resolve(repoRoot, "data/paper/v2/fixture-universe.json")),
+ buildFixtureSchedule(repoRoot, nowTsMs)
+ ]);
+ const report = paperReportSchema.parse(reportValue);
+ const universe = fixtureUniverseSchema.parse(universeValue);
+ const registeredReport = registeredV2ReportSchema.parse(registeredReportValue);
+ const registeredUniverse = fixtureUniverseSchema.parse(registeredUniverseValue);
+ if (universe.summary.longRunEligible !== 0) {
+ throw new Error("Command projection failed closed: fixture admission changed without a refreshed public contract");
+ }
+ if (
+ Object.values(report.lanes.bounty.report.counts).some((value) => value !== 0) ||
+ Object.values(report.lanes.longRun.report.counts).some((value) => value !== 0)
+ ) {
+ throw new Error("Command projection failed closed: invalidated v1 audit contains observations");
+ }
+ const registeredAtTsMs = Date.parse(FORWARD_PAPER_REGISTERED_AT);
+ if (
+ registeredReport.fixtureUniverseGeneratedAt !== registeredUniverse.generatedAt ||
+ registeredReport.lanes.bounty.initialization.startedAtTsMs < registeredAtTsMs ||
+ registeredReport.lanes.longRun.initialization.startedAtTsMs < registeredAtTsMs ||
+ registeredReport.lanes.bounty.initialization.startedAt !== new Date(registeredReport.lanes.bounty.initialization.startedAtTsMs).toISOString() ||
+ registeredReport.lanes.longRun.initialization.startedAt !== new Date(registeredReport.lanes.longRun.initialization.startedAtTsMs).toISOString()
+ ) {
+ throw new Error("Command projection failed closed: registered v2 report does not match its forward-only boundary");
+ }
+
+ const goal = matchroom.replay.states.find((state) => state.id === "goal");
+ if (!goal) throw new Error("Command projection failed closed: featured replay has no goal state");
+ const recentCase: CommandCase = {
+ caseId: matchroom.caseId,
+ matchroomId: matchroom.snapshotId,
+ fixtureRef: matchroom.match.fixtureRef,
+ fixtureLabel: `${matchroom.match.home.name} vs ${matchroom.match.away.name}`,
+ home: matchroom.match.home,
+ away: matchroom.match.away,
+ occurredAt: matchroom.replay.firstSeenAt,
+ marketLabel: matchroom.market.label,
+ marketOutcomeLabel: matchroom.market.outcome,
+ candidateLabel: "Live-lane gate readout",
+ disposition: matchroom.decision.disposition,
+ dispositionLabel: matchroom.decision.label,
+ reason: matchroom.decision.primaryReason,
+ evidenceStatus: "verified_replay",
+ preTriggerMarketMoveBps: matchroom.replay.preTriggerMarketMoveBps,
+ consensusMoveFromBaselineBps: goal.consensusMoveFromBaselineBps,
+ bestAsk: goal.bestAsk,
+ canonicalEvents: matchroom.proof.canonicalEvents,
+ capitalMovedMicros: matchroom.decision.capitalMovedMicros,
+ ordersPlaced: matchroom.decision.ordersPlaced,
+ walletAccessed: matchroom.decision.walletAccessed
+ };
+ const activeFixture = fixtureSchedule.find((fixture) => fixture.phase === "running") ??
+ fixtureSchedule.find((fixture) => fixture.phase === "scheduled") ??
+ fixtureSchedule.find((fixture) => fixture.phase === "unknown") ??
+ fixtureSchedule.find((fixture) => fixture.phase === "failed") ?? fixtureSchedule[0]!;
+ const posture = activeFixture.phase === "scheduled" ? "standing_by" :
+ activeFixture.phase === "running" ? "capture_window" : "awaiting_verification";
+ const postureLabel = activeFixture.phase === "scheduled" ? "Standing by for paired capture" :
+ activeFixture.phase === "running" ? "Capture supervisor recorded running" :
+ activeFixture.phase === "failed" ? "Capture failed closed" :
+ activeFixture.phase === "complete" ? "Capture outcome recorded" : "Capture outcome unknown";
+ const postureDetail = `${activeFixture.home.name} vs ${activeFixture.away.name}: ${activeFixture.statusDetail}. Non-tradeable.`;
+ const longRun = report.lanes.longRun;
+ const registeredLongRun = registeredReport.lanes.longRun;
+ const qualifyingCounts = registeredLongRun.report.counts;
+ const observationStatus = qualifyingCounts.signals === 0
+ ? "awaiting_fresh_evidence" as const
+ : "collecting_forward_evidence" as const;
+
+ return {
+ schemaVersion: 3,
+ snapshotId: COMMAND_SNAPSHOT_ID,
+ generatedAt: new Date(nowTsMs).toISOString(),
+ mode: "offline_artifact",
+ executionMode: "paper",
+ realMoneyGate: "closed",
+ tradeable: false,
+ system: {
+ posture,
+ label: postureLabel,
+ detail: postureDetail,
+ feeds: [
+ { id: "txline", label: "TXLine", status: activeFixture.phase, statusLabel: activeFixture.statusLabel, detail: `Exact source identity confirmed at export · ${activeFixture.statusDetail}` },
+ { id: "polymarket", label: "Polymarket", status: activeFixture.phase, statusLabel: activeFixture.statusLabel, detail: `Exact event family · ${activeFixture.statusDetail}` },
+ { id: "decision_ledger", label: "V2 study ledgers", status: "verified", statusLabel: "Verified", detail: `Registered forward-paper chains verify · ${qualifyingCounts.signals} qualifying signal${qualifyingCounts.signals === 1 ? "" : "s"}` },
+ { id: "replay_proof", label: "Replay proof", status: "verified", statusLabel: "Verified", detail: `${matchroom.proof.canonicalEvents.toLocaleString("en-US")} canonical events` }
+ ]
+ },
+ featuredCase: {
+ ...recentCase,
+ scoreLabel: `${matchroom.match.scoreAtCursor.home}–${matchroom.match.scoreAtCursor.away}`,
+ scoreAtCursor: matchroom.match.scoreAtCursor,
+ clockSeconds: matchroom.match.clockSeconds,
+ clockLabel: matchroom.match.clockLabel,
+ conclusion: goal.decisionExplanation,
+ identityParity: true,
+ chart: matchroom.replay.chart
+ },
+ fixtureSchedule,
+ recentCases: [recentCase],
+ additionalCaseState: {
+ status: "waiting_for_eligible_capture",
+ label: "Registered v2 awaits fresh eligible evidence",
+ detail: qualifyingCounts.signals === 0
+ ? "Registration is active for forward paper only; zero observations qualify until a fresh fixture passes every admission gate."
+ : `${qualifyingCounts.signals} forward signal${qualifyingCounts.signals === 1 ? " is" : "s are"} reconstructed from the registered v2 ledger; historical, retrospective, and synthetic rows remain excluded.`
+ },
+ study: {
+ protocolVersion: FORWARD_PAPER_PROTOCOL_ID,
+ protocolStatus: "registered",
+ configHash: FORWARD_PAPER_CONFIG_HASH,
+ registeredAt: FORWARD_PAPER_REGISTERED_AT,
+ status: "active_forward_paper",
+ statusLabel: "V2 registered",
+ observationStatus,
+ qualifyingCounts,
+ requiredFilledMatches: PAPER_STUDY_EVALUATION_CANDIDATE.minimumFilledMatches,
+ requiredFills: PAPER_STUDY_EVALUATION_CANDIDATE.minimumFills,
+ bountyStatus: "exploratory",
+ stoppingRuleMet: registeredLongRun.report.stoppingRuleMet,
+ realMoneyGate: "closed",
+ reason: `${registeredLongRun.report.reason}. Historical, retrospective, and synthetic rows cannot count.`,
+ historicalV1: {
+ protocolVersion: report.protocolVersion,
+ protocolStatus: "invalidated_suspended",
+ configHash: report.configHash,
+ startedAt: longRun.initialization.startedAt,
+ zeroObservationAudit: true
+ }
+ },
+ proof: {
+ replayIdentityParity: true,
+ replayIdentityHash: matchroom.proof.identityHash,
+ canonicalEvents: matchroom.proof.canonicalEvents,
+ bountyLedgerValid: true,
+ bountyLedgerRows: registeredReport.lanes.bounty.chain.rows,
+ longRunLedgerValid: true,
+ longRunLedgerRows: registeredLongRun.chain.rows,
+ evidenceFixtures: universe.summary.fixtures,
+ pairedBookReplays: universe.summary.pairedBookReplays,
+ signalResearchOnly: universe.summary.signalResearchOnly
+ },
+ sourceFreshness: {
+ paperReportGeneratedAt: registeredReport.generatedAt,
+ fixtureUniverseGeneratedAt: registeredUniverse.generatedAt,
+ replayGeneratedAt: matchroom.generatedAt
+ },
+ publicDataPolicy: matchroom.publicDataPolicy
+ };
+}
+
+export async function buildCommandDashboardResponse(repoRoot: string, nowTsMs = Date.now()): Promise {
+ return { data: await buildCommandSnapshot(repoRoot, nowTsMs) };
+}
diff --git a/src/dash/edge-bundle.ts b/src/dash/edge-bundle.ts
new file mode 100644
index 0000000..7af75d0
--- /dev/null
+++ b/src/dash/edge-bundle.ts
@@ -0,0 +1,336 @@
+import { mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, join, resolve } from "node:path";
+import { z } from "zod";
+import { stableJson } from "../domain/json.js";
+import { assertPublicArtifactsSafe, auditPublicArtifacts } from "../public/artifact-safety.js";
+import { buildJudgeEvidenceResponse, JUDGE_EVIDENCE_API_PATH, judgeEvidenceResponseSchema } from "./judge-evidence.js";
+import {
+ PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR,
+ PUBLIC_DASHBOARD_FILES,
+ PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ parsePublicDashboardManifest,
+ readFrozenDashboardResponse,
+ sha256,
+ type PublicDashboardApiPath,
+ type PublicDashboardManifest
+} from "./public-bundle.js";
+
+export const PUBLIC_EDGE_BUNDLE_RELATIVE_DIR = "public/artifacts/edge" as const;
+export const PUBLIC_EDGE_BUNDLE_ID = "samaritan-dashboard-edge-v1" as const;
+export const PUBLIC_EDGE_BUNDLE_HASH_DOMAIN = "samaritan.dashboard-edge-bundle/v1" as const;
+export const PUBLIC_JUDGE_EVIDENCE_FILENAME = "judge-evidence.json" as const;
+export const PUBLIC_EDGE_MANIFEST_FILENAME = "manifest.json" as const;
+
+const hashSchema = z.string().regex(/^[a-f0-9]{64}$/u);
+const mainApiPathSchema = z.enum(PUBLIC_DASHBOARD_FILES.map((definition) => definition.apiPath) as [
+ PublicDashboardApiPath,
+ ...PublicDashboardApiPath[]
+]);
+const edgeApiPathSchema = z.union([mainApiPathSchema, z.literal(JUDGE_EVIDENCE_API_PATH)]);
+
+const edgeRouteSchema = z.object({
+ apiPath: edgeApiPathSchema,
+ assetPath: z.string().regex(/^\/artifacts\/(?:dashboard|edge)\/[a-z0-9-]+\.json$/u),
+ bytes: z.number().int().positive().safe(),
+ sha256: hashSchema,
+ source: z.enum(["public_dashboard_manifest", "buildJudgeEvidenceResponse"])
+}).strict();
+
+export type PublicEdgeRoute = z.infer;
+
+export const publicEdgeBundleManifestSchema = z.object({
+ schemaVersion: z.literal(1),
+ bundleId: z.literal(PUBLIC_EDGE_BUNDLE_ID),
+ generatedAt: z.string().datetime(),
+ sourceBundleSha256: hashSchema,
+ edgeBundleSha256: hashSchema,
+ routes: z.array(edgeRouteSchema).length(PUBLIC_DASHBOARD_FILES.length + 1),
+ publicDataPolicy: z.object({
+ derivedOnly: z.literal(true),
+ credentialsRequired: z.literal(false),
+ walletControlsExposed: z.literal(false),
+ judgeEvidenceSource: z.literal("verified_node_builder"),
+ buildCommitDisclosure: z.literal("omitted_from_frozen_artifact")
+ }).strict()
+}).strict();
+
+export type PublicEdgeBundleManifest = z.infer;
+
+export type ExportPublicEdgeBundleOptions = {
+ repoRoot: string;
+ outputDir?: string;
+};
+
+export type ExportPublicEdgeBundleResult = {
+ outputDir: string;
+ manifest: PublicEdgeBundleManifest;
+};
+
+export type VerifiedPublicEdgeBundle = {
+ edgeRoot: string;
+ manifest: PublicEdgeBundleManifest;
+ judgeEvidenceBody: string;
+};
+
+const expectedRoutes = [
+ ...PUBLIC_DASHBOARD_FILES.map((definition) => ({
+ apiPath: definition.apiPath,
+ assetPath: `/artifacts/dashboard/${definition.file}`,
+ source: "public_dashboard_manifest" as const
+ })),
+ {
+ apiPath: JUDGE_EVIDENCE_API_PATH,
+ assetPath: `/artifacts/edge/${PUBLIC_JUDGE_EVIDENCE_FILENAME}`,
+ source: "buildJudgeEvidenceResponse" as const
+ }
+] as const;
+
+function canonicalJson(value: unknown): string {
+ return `${stableJson(value)}\n`;
+}
+
+function edgeBundleHash(
+ generatedAt: string,
+ sourceBundleSha256: string,
+ routes: readonly PublicEdgeRoute[]
+): string {
+ return sha256(`${PUBLIC_EDGE_BUNDLE_HASH_DOMAIN}\n${stableJson({
+ schemaVersion: 1,
+ bundleId: PUBLIC_EDGE_BUNDLE_ID,
+ generatedAt,
+ sourceBundleSha256,
+ routes,
+ publicDataPolicy: {
+ derivedOnly: true,
+ credentialsRequired: false,
+ walletControlsExposed: false,
+ judgeEvidenceSource: "verified_node_builder",
+ buildCommitDisclosure: "omitted_from_frozen_artifact"
+ }
+ })}`);
+}
+
+export function parsePublicEdgeBundleManifest(value: unknown): PublicEdgeBundleManifest {
+ const manifest = publicEdgeBundleManifestSchema.parse(value);
+ for (const [index, expected] of expectedRoutes.entries()) {
+ const actual = manifest.routes[index];
+ if (
+ !actual ||
+ actual.apiPath !== expected.apiPath ||
+ actual.assetPath !== expected.assetPath ||
+ actual.source !== expected.source
+ ) {
+ throw new Error(`Public edge manifest route ${index} does not match the frozen route map`);
+ }
+ }
+ const expectedHash = edgeBundleHash(manifest.generatedAt, manifest.sourceBundleSha256, manifest.routes);
+ if (manifest.edgeBundleSha256 !== expectedHash) {
+ throw new Error("Public edge manifest bundle hash is invalid");
+ }
+ return manifest;
+}
+
+async function readPublicDashboardManifest(repoRoot: string): Promise {
+ const path = resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR, "manifest.json");
+ let value: unknown;
+ try {
+ value = JSON.parse(await readFile(path, "utf8")) as unknown;
+ } catch (error) {
+ throw new Error(`Public edge export requires the frozen dashboard manifest: ${
+ error instanceof Error ? error.message : String(error)
+ }`);
+ }
+ return parsePublicDashboardManifest(value);
+}
+
+async function copyVerifiedDashboardBundle(repoRoot: string, temporaryRepoRoot: string): Promise {
+ const sourceRoot = resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR);
+ const targetRoot = resolve(temporaryRepoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR);
+ await mkdir(targetRoot, { recursive: true });
+ for (const filename of [
+ ...PUBLIC_DASHBOARD_FILES.map((definition) => definition.file),
+ PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ "manifest.json"
+ ]) {
+ await writeFile(resolve(targetRoot, filename), await readFile(resolve(sourceRoot, filename)));
+ }
+}
+
+async function buildFrozenJudgeEvidence(repoRoot: string): Promise {
+ const temporaryRepoRoot = await mkdtemp(join(tmpdir(), "samaritan-edge-judge-"));
+ try {
+ await copyVerifiedDashboardBundle(repoRoot, temporaryRepoRoot);
+ const response = judgeEvidenceResponseSchema.parse(await buildJudgeEvidenceResponse(temporaryRepoRoot, {
+ buildCommit: null
+ }));
+ if (response.app.buildCommit !== null) {
+ throw new Error("Frozen judge evidence must omit a deployment commit rather than publish a stale one");
+ }
+ return canonicalJson(response);
+ } finally {
+ await rm(temporaryRepoRoot, { recursive: true, force: true });
+ }
+}
+
+async function atomicWrite(path: string, body: string): Promise {
+ const temporaryPath = `${path}.tmp-${process.pid}`;
+ await writeFile(temporaryPath, body, { encoding: "utf8", mode: 0o644 });
+ await rename(temporaryPath, path);
+}
+
+async function refuseUnexpectedOutputFiles(outputDir: string): Promise {
+ let names: string[];
+ try {
+ names = await readdir(outputDir);
+ } catch {
+ return;
+ }
+ const expected = new Set([PUBLIC_JUDGE_EVIDENCE_FILENAME, PUBLIC_EDGE_MANIFEST_FILENAME]);
+ const unexpected = names.filter((name) => !expected.has(name));
+ if (unexpected.length > 0) {
+ throw new Error(`Public edge export refuses output directory with unexpected files: ${unexpected.join(", ")}`);
+ }
+}
+
+export async function exportPublicEdgeBundle(
+ options: ExportPublicEdgeBundleOptions
+): Promise {
+ const repoRoot = resolve(options.repoRoot);
+ const outputDir = resolve(options.outputDir ?? resolve(repoRoot, PUBLIC_EDGE_BUNDLE_RELATIVE_DIR));
+ const sourceManifest = await readPublicDashboardManifest(repoRoot);
+ const frozenResponses = await Promise.all(
+ PUBLIC_DASHBOARD_FILES.map((definition) => readFrozenDashboardResponse(repoRoot, definition.apiPath))
+ );
+ if (frozenResponses.some((response) => response.manifest.bundleSha256 !== sourceManifest.bundleSha256)) {
+ throw new Error("Public edge export requires one coherent verified dashboard bundle");
+ }
+
+ const judgeEvidenceBody = await buildFrozenJudgeEvidence(repoRoot);
+ const routes: PublicEdgeRoute[] = [
+ ...PUBLIC_DASHBOARD_FILES.map((definition, index) => {
+ const body = frozenResponses[index]?.body;
+ const entry = sourceManifest.files[index];
+ if (!body || !entry || entry.apiPath !== definition.apiPath || entry.file !== definition.file) {
+ throw new Error(`Public edge export cannot reconcile ${definition.apiPath}`);
+ }
+ return {
+ apiPath: definition.apiPath,
+ assetPath: `/artifacts/dashboard/${definition.file}`,
+ bytes: Buffer.byteLength(body, "utf8"),
+ sha256: sha256(body),
+ source: "public_dashboard_manifest" as const
+ };
+ }),
+ {
+ apiPath: JUDGE_EVIDENCE_API_PATH,
+ assetPath: `/artifacts/edge/${PUBLIC_JUDGE_EVIDENCE_FILENAME}`,
+ bytes: Buffer.byteLength(judgeEvidenceBody, "utf8"),
+ sha256: sha256(judgeEvidenceBody),
+ source: "buildJudgeEvidenceResponse" as const
+ }
+ ];
+ const manifest = parsePublicEdgeBundleManifest({
+ schemaVersion: 1,
+ bundleId: PUBLIC_EDGE_BUNDLE_ID,
+ generatedAt: sourceManifest.generatedAt,
+ sourceBundleSha256: sourceManifest.bundleSha256,
+ edgeBundleSha256: edgeBundleHash(sourceManifest.generatedAt, sourceManifest.bundleSha256, routes),
+ routes,
+ publicDataPolicy: {
+ derivedOnly: true,
+ credentialsRequired: false,
+ walletControlsExposed: false,
+ judgeEvidenceSource: "verified_node_builder",
+ buildCommitDisclosure: "omitted_from_frozen_artifact"
+ }
+ });
+ const manifestBody = canonicalJson(manifest);
+
+ const stagingDir = await mkdtemp(join(tmpdir(), "samaritan-public-edge-"));
+ try {
+ await writeFile(resolve(stagingDir, PUBLIC_JUDGE_EVIDENCE_FILENAME), judgeEvidenceBody, "utf8");
+ await writeFile(resolve(stagingDir, PUBLIC_EDGE_MANIFEST_FILENAME), manifestBody, "utf8");
+ const audit = await auditPublicArtifacts({ allowlistedPaths: [stagingDir], cwd: stagingDir });
+ assertPublicArtifactsSafe(audit);
+ } finally {
+ await rm(stagingDir, { recursive: true, force: true });
+ }
+
+ await mkdir(outputDir, { recursive: true });
+ await refuseUnexpectedOutputFiles(outputDir);
+ await atomicWrite(resolve(outputDir, PUBLIC_JUDGE_EVIDENCE_FILENAME), judgeEvidenceBody);
+ await atomicWrite(resolve(outputDir, PUBLIC_EDGE_MANIFEST_FILENAME), manifestBody);
+ return { outputDir, manifest };
+}
+
+export async function verifyPublicEdgeBundle(repoRootInput: string): Promise {
+ const repoRoot = resolve(repoRootInput);
+ const edgeRoot = resolve(repoRoot, PUBLIC_EDGE_BUNDLE_RELATIVE_DIR);
+ const sourceManifest = await readPublicDashboardManifest(repoRoot);
+ let manifestValue: unknown;
+ try {
+ manifestValue = JSON.parse(await readFile(resolve(edgeRoot, PUBLIC_EDGE_MANIFEST_FILENAME), "utf8")) as unknown;
+ } catch (error) {
+ throw new Error(`Frozen public edge manifest is unavailable: ${
+ error instanceof Error ? error.message : String(error)
+ }`);
+ }
+ const manifest = parsePublicEdgeBundleManifest(manifestValue);
+ if (
+ manifest.generatedAt !== sourceManifest.generatedAt ||
+ manifest.sourceBundleSha256 !== sourceManifest.bundleSha256
+ ) {
+ throw new Error("Frozen public edge manifest does not commit to the current dashboard bundle");
+ }
+
+ for (const [index, definition] of PUBLIC_DASHBOARD_FILES.entries()) {
+ const route = manifest.routes[index];
+ const frozen = await readFrozenDashboardResponse(repoRoot, definition.apiPath);
+ if (
+ !route ||
+ route.bytes !== Buffer.byteLength(frozen.body, "utf8") ||
+ route.sha256 !== sha256(frozen.body)
+ ) {
+ throw new Error(`Frozen public edge route changed for ${definition.apiPath}`);
+ }
+ }
+
+ const judgeRoute = manifest.routes.at(-1);
+ if (!judgeRoute || judgeRoute.apiPath !== JUDGE_EVIDENCE_API_PATH) {
+ throw new Error("Frozen public edge bundle lacks judge evidence");
+ }
+ const judgeEvidenceBody = await readFile(resolve(edgeRoot, basename(judgeRoute.assetPath)), "utf8");
+ if (
+ Buffer.byteLength(judgeEvidenceBody, "utf8") !== judgeRoute.bytes ||
+ sha256(judgeEvidenceBody) !== judgeRoute.sha256
+ ) {
+ throw new Error("Frozen judge evidence bytes do not match the public edge manifest");
+ }
+ let judgeValue: unknown;
+ try {
+ judgeValue = JSON.parse(judgeEvidenceBody) as unknown;
+ } catch {
+ throw new Error("Frozen judge evidence is invalid JSON");
+ }
+ const judgeEvidence = judgeEvidenceResponseSchema.parse(judgeValue);
+ if (
+ judgeEvidence.generatedAt !== sourceManifest.generatedAt ||
+ judgeEvidence.app.buildCommit !== null ||
+ judgeEvidence.access.source !== "verified_frozen_public_bundle" ||
+ judgeEvidence.access.disclosure !== "derived_metadata_only"
+ ) {
+ throw new Error("Frozen judge evidence disclosure boundary changed");
+ }
+ const rebuiltJudgeEvidenceBody = await buildFrozenJudgeEvidence(repoRoot);
+ if (rebuiltJudgeEvidenceBody !== judgeEvidenceBody) {
+ throw new Error("Frozen judge evidence no longer matches buildJudgeEvidenceResponse");
+ }
+
+ const audit = await auditPublicArtifacts({
+ allowlistedPaths: [resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR), edgeRoot],
+ cwd: repoRoot
+ });
+ assertPublicArtifactsSafe(audit);
+ return { edgeRoot, manifest, judgeEvidenceBody };
+}
diff --git a/src/dash/export-edge-bundle.ts b/src/dash/export-edge-bundle.ts
new file mode 100644
index 0000000..063c681
--- /dev/null
+++ b/src/dash/export-edge-bundle.ts
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+import { resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { exportPublicEdgeBundle } from "./edge-bundle.js";
+
+const canonicalRepoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
+
+async function main(): Promise {
+ try {
+ const result = await exportPublicEdgeBundle({ repoRoot: canonicalRepoRoot });
+ process.stdout.write(
+ `Exported ${result.manifest.routes.length} verified edge routes ` +
+ `(bundle ${result.manifest.edgeBundleSha256})\n`
+ );
+ } catch (error) {
+ process.stderr.write(`Public edge export failed: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ await main();
+}
diff --git a/src/dash/export-public-bundle.ts b/src/dash/export-public-bundle.ts
new file mode 100644
index 0000000..86cbd4e
--- /dev/null
+++ b/src/dash/export-public-bundle.ts
@@ -0,0 +1,223 @@
+#!/usr/bin/env node
+import { mkdir, mkdtemp, readdir, rename, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, join, resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { stableJson } from "../domain/json.js";
+import { runSyntheticJudgeCase } from "../demo/synthetic-judge-case.js";
+import { assertPublicArtifactsSafe, auditPublicArtifacts } from "../public/artifact-safety.js";
+import { verifyDecisionReceipt } from "../proof/decision-receipt-schema.js";
+import { buildCasebookDashboardResponse } from "./casebook-projection.js";
+import { buildCommandDashboardResponse } from "./command-projection.js";
+import { buildSpainBelgiumDashboardResponse } from "./projection.js";
+import {
+ PUBLIC_DASHBOARD_BUNDLE_ID,
+ PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR,
+ PUBLIC_DASHBOARD_CANONICALIZATION,
+ PUBLIC_DASHBOARD_FILES,
+ PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ parsePublicDashboardManifest,
+ parsePublicDashboardResponse,
+ publicDashboardBundleHash,
+ sha256,
+ type PublicDashboardApiPath,
+ type PublicDashboardFilename,
+ type PublicDashboardManifest,
+ type PublicDashboardManifestDownload,
+ type PublicDashboardManifestEntry
+} from "./public-bundle.js";
+import { TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS } from "./public-contract.js";
+import { buildStudyDashboardResponse } from "./study-projection.js";
+
+const canonicalRepoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
+const OUTPUT_FILENAMES = new Set([
+ ...PUBLIC_DASHBOARD_FILES.map((definition) => definition.file),
+ PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ "manifest.json"
+]);
+
+export type ExportPublicDashboardBundleOptions = {
+ repoRoot: string;
+ outputDir?: string;
+};
+
+export type ExportPublicDashboardBundleResult = {
+ outputDir: string;
+ manifest: PublicDashboardManifest;
+};
+
+function canonicalJson(value: unknown): string {
+ return `${stableJson(value)}\n`;
+}
+
+async function atomicWrite(path: string, body: string): Promise {
+ const temporaryPath = `${path}.tmp-${process.pid}`;
+ await writeFile(temporaryPath, body, { encoding: "utf8", mode: 0o644 });
+ await rename(temporaryPath, path);
+}
+
+async function refuseUnexpectedOutputFiles(outputDir: string): Promise {
+ let names: string[];
+ try {
+ names = await readdir(outputDir);
+ } catch {
+ return;
+ }
+ const unexpected = names.filter((name) => !OUTPUT_FILENAMES.has(name));
+ if (unexpected.length > 0) {
+ throw new Error(`Public dashboard export refuses output directory with unexpected files: ${unexpected.join(", ")}`);
+ }
+}
+
+export async function exportPublicDashboardBundle(
+ options: ExportPublicDashboardBundleOptions
+): Promise {
+ const repoRoot = resolve(options.repoRoot);
+ const outputDir = resolve(options.outputDir ?? resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR));
+
+ // The corrected study, local capture outcomes, and bundled receipt supply a
+ // stable as-of time. The manifest must never predate an artifact it commits to.
+ const syntheticReport = await runSyntheticJudgeCase();
+ const syntheticReceipt = syntheticReport.receipt;
+ if (!Number.isSafeInteger(syntheticReceipt.generatedAtTsMs)) {
+ throw new Error("Synthetic decision receipt has an invalid generation timestamp");
+ }
+ const syntheticReceiptGeneratedAt = new Date(syntheticReceipt.generatedAtTsMs).toISOString();
+ const study = await buildStudyDashboardResponse(repoRoot);
+ const outcomeProbe = await buildCommandDashboardResponse(repoRoot, Date.parse("2100-01-01T00:00:00.000Z"));
+ const generatedAt = [
+ study.data.correctedHistoricalCandidate.generatedAt,
+ syntheticReceiptGeneratedAt,
+ ...outcomeProbe.data.fixtureSchedule.map((fixture) => fixture.statusUpdatedAt).filter((value): value is string => value !== null)
+ ].sort((left, right) => Date.parse(left) - Date.parse(right)).at(-1)!;
+ const asOfTsMs = Date.parse(generatedAt);
+ if (!Number.isSafeInteger(asOfTsMs)) throw new Error("Corrected historical candidate has an invalid export timestamp");
+
+ const [command, casebook, matchroom] = await Promise.all([
+ buildCommandDashboardResponse(repoRoot, asOfTsMs),
+ buildCasebookDashboardResponse(repoRoot),
+ buildSpainBelgiumDashboardResponse(repoRoot)
+ ]);
+ const responseByPath = new Map([
+ [PUBLIC_DASHBOARD_FILES[0].apiPath, command],
+ [PUBLIC_DASHBOARD_FILES[1].apiPath, casebook],
+ [PUBLIC_DASHBOARD_FILES[2].apiPath, study],
+ [PUBLIC_DASHBOARD_FILES[3].apiPath, matchroom]
+ ]);
+
+ const bodies = new Map();
+ const entries: PublicDashboardManifestEntry[] = [];
+ for (const definition of PUBLIC_DASHBOARD_FILES) {
+ const response = responseByPath.get(definition.apiPath);
+ if (!response) throw new Error(`Public dashboard export lacks ${definition.apiPath}`);
+ const parsed = parsePublicDashboardResponse(definition.apiPath, response) as { data?: { snapshotId?: unknown } };
+ if (parsed.data?.snapshotId !== definition.snapshotId) {
+ throw new Error(`Public dashboard export snapshot mismatch for ${definition.apiPath}`);
+ }
+ const body = canonicalJson(parsed);
+ bodies.set(definition.file, body);
+ entries.push({
+ apiPath: definition.apiPath,
+ file: definition.file,
+ snapshotId: definition.snapshotId,
+ sha256: sha256(body),
+ bytes: Buffer.byteLength(body, "utf8")
+ });
+ }
+
+ // Export the receipt produced by the same production-component proving path
+ // exposed through `pnpm demo`, so the CLI and downloadable judge artifact
+ // commit to one case rather than two merely similar synthetic fixtures.
+ const receiptVerification = verifyDecisionReceipt(syntheticReceipt);
+ const syntheticProof = study.data.syntheticProof;
+ if (
+ !receiptVerification.valid ||
+ !receiptVerification.synthetic ||
+ receiptVerification.lifecycleStatus !== "filled_settled" ||
+ syntheticReceipt.solanaAnchor !== null ||
+ syntheticReport.performanceUse !== "excluded_synthetic" ||
+ syntheticReport.boundaries.anthropicCalls !== 0 ||
+ syntheticReport.boundaries.txlineApiCalls !== 0 ||
+ syntheticReport.boundaries.polymarketApiCalls !== 0 ||
+ syntheticReport.boundaries.walletCalls !== 0 ||
+ syntheticReport.boundaries.solanaRpcCalls !== 0 ||
+ syntheticReport.boundaries.realOrders !== 0 ||
+ !syntheticProof.offlineVerified ||
+ syntheticProof.lifecycleStatus !== receiptVerification.lifecycleStatus ||
+ syntheticProof.performanceUse !== syntheticReport.performanceUse ||
+ syntheticProof.externalCalls !== 0 ||
+ syntheticProof.solanaAnchorStatus !== "not_submitted"
+ ) {
+ throw new Error("Synthetic decision receipt does not satisfy the frozen proving-fixture contract");
+ }
+ const syntheticReceiptBody = canonicalJson(syntheticReceipt);
+ const downloads: PublicDashboardManifestDownload[] = [{
+ id: "synthetic-decision-receipt",
+ file: PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ mediaType: "application/json",
+ sha256: sha256(syntheticReceiptBody),
+ bytes: Buffer.byteLength(syntheticReceiptBody, "utf8"),
+ synthetic: true,
+ performanceUse: "excluded_synthetic"
+ }];
+
+ const manifest = parsePublicDashboardManifest({
+ schemaVersion: 1,
+ bundleId: PUBLIC_DASHBOARD_BUNDLE_ID,
+ generatedAt,
+ canonicalization: PUBLIC_DASHBOARD_CANONICALIZATION,
+ bundleSha256: publicDashboardBundleHash(generatedAt, entries, downloads),
+ files: entries,
+ downloads,
+ publicDataPolicy: {
+ derivedOnly: true,
+ txlineProbabilityDisplay: "bucketed_movement_only",
+ txlineMovementBucketBps: TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ credentialsRequired: false,
+ walletControlsExposed: false,
+ txlineFixtureIdentifiersExposed: false
+ }
+ });
+ const manifestBody = canonicalJson(manifest);
+
+ const stagingDir = await mkdtemp(join(tmpdir(), "samaritan-public-dashboard-"));
+ try {
+ for (const [filename, body] of bodies) await writeFile(resolve(stagingDir, filename), body, "utf8");
+ await writeFile(resolve(stagingDir, PUBLIC_SYNTHETIC_RECEIPT_FILENAME), syntheticReceiptBody, "utf8");
+ await writeFile(resolve(stagingDir, "manifest.json"), manifestBody, "utf8");
+ const audit = await auditPublicArtifacts({ allowlistedPaths: [stagingDir], cwd: stagingDir });
+ assertPublicArtifactsSafe(audit);
+ } finally {
+ await rm(stagingDir, { recursive: true, force: true });
+ }
+
+ await mkdir(outputDir, { recursive: true });
+ await refuseUnexpectedOutputFiles(outputDir);
+ for (const [filename, body] of bodies) await atomicWrite(resolve(outputDir, filename), body);
+ await atomicWrite(resolve(outputDir, PUBLIC_SYNTHETIC_RECEIPT_FILENAME), syntheticReceiptBody);
+ await atomicWrite(resolve(outputDir, "manifest.json"), manifestBody);
+ return { outputDir, manifest };
+}
+
+function parseRepoRoot(args: readonly string[]): string {
+ if (args.length === 0) return canonicalRepoRoot;
+ if (args.length === 2 && args[0] === "--repo-root" && args[1]) return resolve(args[1]);
+ throw new Error("Usage: pnpm dash:export-public [-- --repo-root ]");
+}
+
+async function main(): Promise {
+ try {
+ const result = await exportPublicDashboardBundle({ repoRoot: parseRepoRoot(process.argv.slice(2)) });
+ process.stdout.write(
+ `Exported ${result.manifest.files.length} deterministic dashboard responses to ${basename(result.outputDir)} ` +
+ `(bundle ${result.manifest.bundleSha256})\n`
+ );
+ } catch (error) {
+ process.stderr.write(`Public dashboard export failed: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ await main();
+}
diff --git a/src/dash/judge-evidence.ts b/src/dash/judge-evidence.ts
new file mode 100644
index 0000000..05e82af
--- /dev/null
+++ b/src/dash/judge-evidence.ts
@@ -0,0 +1,466 @@
+import { readFile, stat } from "node:fs/promises";
+import { resolve } from "node:path";
+import { z } from "zod";
+import {
+ decisionReceiptSchema,
+ verifyDecisionReceipt
+} from "../proof/decision-receipt-schema.js";
+import {
+ CASEBOOK_API_PATH,
+ COMMAND_API_PATH,
+ PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR,
+ PUBLIC_SYNTHETIC_RECEIPT_FILENAME,
+ SPAIN_BELGIUM_API_PATH,
+ STUDY_API_PATH,
+ readFrozenDashboardResponse,
+ sha256 as dashboardSha256
+} from "./public-bundle.js";
+import { TXLINE_PULSE_API_PATH } from "./txline-pulse.js";
+
+export const JUDGE_EVIDENCE_API_PATH = "/api/judge/evidence" as const;
+
+const publicRouteSchema = z.object({
+ label: z.string().min(1),
+ route: z.string().min(1)
+}).strict();
+
+const criterionSchema = z.object({
+ id: z.enum([
+ "core_functionality_and_data_ingestion",
+ "autonomous_operation",
+ "deterministic_logic_and_architecture",
+ "innovation_and_novelty",
+ "production_readiness"
+ ]),
+ criterion: z.string().min(1),
+ evidence: z.array(publicRouteSchema).min(1)
+}).strict();
+
+const txlineEndpointSchema = z.object({
+ method: z.enum(["GET", "POST"]),
+ path: z.string().regex(/^\/(?:auth|api)\//),
+ purpose: z.string().min(1)
+}).strict();
+
+export const judgeEvidenceResponseSchema = z.object({
+ schemaVersion: z.literal(1),
+ generatedAt: z.string().datetime(),
+ app: z.object({
+ name: z.literal("Samaritan"),
+ service: z.literal("samaritan-dashboard"),
+ buildCommit: z.string().regex(/^[a-f0-9]{7,64}$/).nullable()
+ }).strict(),
+ access: z.object({
+ authenticationRequired: z.literal(false),
+ readOnly: z.literal(true),
+ source: z.literal("verified_frozen_public_bundle"),
+ disclosure: z.literal("derived_metadata_only")
+ }).strict(),
+ trackCriteria: z.array(criterionSchema).length(5),
+ txlineIntegration: z.object({
+ endpointsUsed: z.array(txlineEndpointSchema).length(10),
+ publicOutput: z.literal("derived_evidence_only")
+ }).strict(),
+ evidenceClassCounts: z.object({
+ capturedReplayFixtures: z.number().int().nonnegative(),
+ capturedReplayObservations: z.number().int().nonnegative(),
+ capturedNoTradeObservations: z.number().int().nonnegative(),
+ historicalTrainingObservations: z.number().int().nonnegative(),
+ historicalHeldoutObservations: z.number().int().nonnegative(),
+ historicalHeldoutFixtures: z.number().int().nonnegative(),
+ syntheticLifecycleReceipts: z.number().int().nonnegative(),
+ verifiedCapturedAgentLifecycleReceipts: z.number().int().nonnegative(),
+ registeredForwardPaperSignals: z.number().int().nonnegative()
+ }).strict(),
+ gates: z.object({
+ executionMode: z.literal("paper"),
+ paperProtocol: z.object({
+ id: z.string().min(1),
+ status: z.string().min(1),
+ active: z.boolean(),
+ observationStatus: z.string().min(1),
+ evidencePolicy: z.string().min(1),
+ qualifyingCounts: z.object({
+ matches: z.number().int().nonnegative(),
+ signals: z.number().int().nonnegative(),
+ filledMatches: z.number().int().nonnegative(),
+ fills: z.number().int().nonnegative(),
+ settledFills: z.number().int().nonnegative()
+ }).strict(),
+ riskLimits: z.object({
+ bankrollMicroUsd: z.number().int().positive(),
+ perTradeStakeMicroUsd: z.number().int().positive(),
+ aggregateExposureMicroUsd: z.number().int().positive(),
+ drawdownStopMicroUsd: z.number().int().positive()
+ }).strict()
+ }).strict(),
+ realMoney: z.object({
+ gate: z.literal("closed"),
+ ordersEnabled: z.literal(false)
+ }).strict()
+ }).strict(),
+ syntheticReceipt: z.object({
+ route: z.literal(`/artifacts/dashboard/${PUBLIC_SYNTHETIC_RECEIPT_FILENAME}`),
+ orderedEventCount: z.number().int().positive(),
+ verificationStatus: z.literal("offline_verified"),
+ lifecycleStatus: z.string().min(1),
+ synthetic: z.literal(true),
+ externalCalls: z.literal(0),
+ performanceUse: z.literal("excluded_synthetic"),
+ solanaAnchorStatus: z.literal("not_submitted")
+ }).strict(),
+ publicLimitations: z.array(z.string().min(1)).length(6),
+ documentation: z.object({
+ technicalOverview: z.literal("https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md"),
+ txlineApiFeedback: z.literal("https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/txline-api-feedback.md"),
+ repository: z.literal("https://github.com/Pavilion-devs/samaritan")
+ }).strict()
+}).strict();
+
+export type JudgeEvidenceResponse = z.infer;
+
+type JsonRecord = Record;
+
+function record(value: unknown, label: string): JsonRecord {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
+ throw new Error(`Judge evidence requires ${label}`);
+ }
+ return value as JsonRecord;
+}
+
+function array(value: unknown, label: string): unknown[] {
+ if (!Array.isArray(value)) throw new Error(`Judge evidence requires ${label}`);
+ return value;
+}
+
+function string(value: unknown, label: string): string {
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Judge evidence requires ${label}`);
+ return value;
+}
+
+function boolean(value: unknown, label: string): boolean {
+ if (typeof value !== "boolean") throw new Error(`Judge evidence requires ${label}`);
+ return value;
+}
+
+function nonnegativeInteger(value: unknown, label: string): number {
+ if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(`Judge evidence requires ${label}`);
+ return value as number;
+}
+
+function positiveInteger(value: unknown, label: string): number {
+ const parsed = nonnegativeInteger(value, label);
+ if (parsed === 0) throw new Error(`Judge evidence requires positive ${label}`);
+ return parsed;
+}
+
+function literal(value: unknown, expected: T, label: string): T {
+ if (value !== expected) throw new Error(`Judge evidence requires ${label}`);
+ return expected;
+}
+
+function qualifyingCounts(protocol: JsonRecord): JudgeEvidenceResponse["gates"]["paperProtocol"]["qualifyingCounts"] {
+ const counts = record(protocol.qualifyingCounts, "paper protocol qualifying counts");
+ const parsed = {
+ matches: nonnegativeInteger(counts.matches, "paper protocol qualifying matches"),
+ signals: nonnegativeInteger(counts.signals, "paper protocol qualifying signals"),
+ filledMatches: nonnegativeInteger(counts.filledMatches, "paper protocol qualifying filled matches"),
+ fills: nonnegativeInteger(counts.fills, "paper protocol qualifying fills"),
+ settledFills: nonnegativeInteger(counts.settledFills, "paper protocol qualifying settled fills")
+ };
+ if (parsed.filledMatches > parsed.matches || parsed.fills > parsed.signals || parsed.settledFills > parsed.fills) {
+ throw new Error("Judge evidence paper protocol counts do not reconcile");
+ }
+ return parsed;
+}
+
+function safeCommit(value: string | undefined): string | null {
+ const normalized = value?.trim().toLowerCase();
+ return normalized && /^[a-f0-9]{7,64}$/.test(normalized) ? normalized : null;
+}
+
+async function gitDirectory(repoRoot: string): Promise {
+ const marker = resolve(repoRoot, ".git");
+ try {
+ if ((await stat(marker)).isDirectory()) return marker;
+ const contents = await readFile(marker, "utf8");
+ const match = /^gitdir:\s*(.+)\s*$/i.exec(contents);
+ return match?.[1] ? resolve(repoRoot, match[1]) : null;
+ } catch {
+ return null;
+ }
+}
+
+async function localGitCommit(repoRoot: string): Promise {
+ const directory = await gitDirectory(repoRoot);
+ if (!directory) return null;
+ try {
+ const head = (await readFile(resolve(directory, "HEAD"), "utf8")).trim();
+ const detached = safeCommit(head);
+ if (detached) return detached;
+ const match = /^ref:\s*(refs\/[A-Za-z0-9._/-]+)$/.exec(head);
+ const ref = match?.[1];
+ if (!ref || ref.includes("..")) return null;
+ try {
+ return safeCommit(await readFile(resolve(directory, ref), "utf8"));
+ } catch {
+ const packed = await readFile(resolve(directory, "packed-refs"), "utf8");
+ const line = packed.split("\n").find((candidate) => candidate.endsWith(` ${ref}`));
+ return safeCommit(line?.split(" ", 1)[0]);
+ }
+ } catch {
+ return null;
+ }
+}
+
+async function buildCommit(repoRoot: string): Promise {
+ for (const candidate of [
+ process.env.SAMARITAN_BUILD_COMMIT,
+ process.env.VERCEL_GIT_COMMIT_SHA,
+ process.env.RENDER_GIT_COMMIT,
+ process.env.GITHUB_SHA,
+ process.env.COMMIT_SHA
+ ]) {
+ const commit = safeCommit(candidate);
+ if (commit) return commit;
+ }
+ return localGitCommit(repoRoot);
+}
+
+const TRACK_CRITERIA: JudgeEvidenceResponse["trackCriteria"] = [
+ {
+ id: "core_functionality_and_data_ingestion",
+ criterion: "Core Functionality & Data Ingestion",
+ evidence: [
+ { label: "Captured TXLine-derived replay", route: "/matchroom" },
+ { label: "Derived evidence index API", route: CASEBOOK_API_PATH },
+ { label: "Live derived TXLine connectivity pulse", route: TXLINE_PULSE_API_PATH },
+ {
+ label: "Integration documentation",
+ route: "https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md"
+ }
+ ]
+ },
+ {
+ id: "autonomous_operation",
+ criterion: "Autonomous Operation",
+ evidence: [
+ { label: "Bounded lifecycle proof", route: "/proof" },
+ { label: "Offline-verifiable synthetic receipt", route: `/artifacts/dashboard/${PUBLIC_SYNTHETIC_RECEIPT_FILENAME}` }
+ ]
+ },
+ {
+ id: "deterministic_logic_and_architecture",
+ criterion: "Logic & Code Architecture",
+ evidence: [
+ { label: "Interactive receipt verifier", route: "/proof" },
+ { label: "Decision evidence index", route: "/casebook" },
+ { label: "Ordered lifecycle receipt", route: `/artifacts/dashboard/${PUBLIC_SYNTHETIC_RECEIPT_FILENAME}` }
+ ]
+ },
+ {
+ id: "innovation_and_novelty",
+ criterion: "Innovation / Novelty",
+ evidence: [
+ { label: "Disciplined no-trade case", route: "/matchroom" },
+ { label: "Separated research and proving fixture", route: "/study" }
+ ]
+ },
+ {
+ id: "production_readiness",
+ criterion: "Production Readiness",
+ evidence: [
+ { label: "No-login judge surface", route: "/" },
+ { label: "Read-only service health", route: "/api/v1/health" },
+ { label: "Fail-closed evidence summary", route: JUDGE_EVIDENCE_API_PATH }
+ ]
+ }
+];
+
+const TXLINE_ENDPOINTS: JudgeEvidenceResponse["txlineIntegration"]["endpointsUsed"] = [
+ { method: "POST", path: "/auth/guest/start", purpose: "Start the guest authentication session" },
+ { method: "POST", path: "/api/token/activate", purpose: "Activate the subscribed data token" },
+ { method: "GET", path: "/api/fixtures/snapshot", purpose: "Discover exact fixture and kickoff identities" },
+ { method: "GET", path: "/api/odds/stream", purpose: "Consume live odds SSE" },
+ { method: "GET", path: "/api/scores/stream", purpose: "Consume live score and action SSE" },
+ { method: "GET", path: "/api/odds/snapshot/{fixtureId}", purpose: "Backfill current odds state during recovery" },
+ { method: "GET", path: "/api/scores/snapshot/{fixtureId}", purpose: "Backfill current score state during recovery" },
+ { method: "GET", path: "/api/odds/updates/{epochDay}/{hourOfDay}/{interval}", purpose: "Build historical odds replay input" },
+ { method: "GET", path: "/api/scores/updates/{epochDay}/{hourOfDay}/{interval}", purpose: "Build aligned historical score input" },
+ { method: "GET", path: "/api/scores/historical/{fixtureId}", purpose: "Recover retained fixture score sequences" }
+];
+
+export type BuildJudgeEvidenceResponseOptions = {
+ buildCommit?: string | null;
+};
+
+export async function buildJudgeEvidenceResponse(
+ repoRoot: string,
+ options: BuildJudgeEvidenceResponseOptions = {}
+): Promise {
+ const frozenResponses = await Promise.all([
+ readFrozenDashboardResponse(repoRoot, COMMAND_API_PATH),
+ readFrozenDashboardResponse(repoRoot, CASEBOOK_API_PATH),
+ readFrozenDashboardResponse(repoRoot, STUDY_API_PATH),
+ readFrozenDashboardResponse(repoRoot, SPAIN_BELGIUM_API_PATH)
+ ]);
+ const [commandFrozen, casebookFrozen, studyFrozen, matchroomFrozen] = frozenResponses;
+ const manifest = commandFrozen.manifest;
+ if (frozenResponses.some((response) => response.manifest.bundleSha256 !== manifest.bundleSha256)) {
+ throw new Error("Judge evidence requires one coherent frozen bundle");
+ }
+
+ const command = record(record(JSON.parse(commandFrozen.body) as unknown, "command response").data, "command data");
+ const casebook = record(record(JSON.parse(casebookFrozen.body) as unknown, "casebook response").data, "casebook data");
+ const study = record(record(JSON.parse(studyFrozen.body) as unknown, "study response").data, "study data");
+ const matchroom = record(record(JSON.parse(matchroomFrozen.body) as unknown, "matchroom response").data, "matchroom data");
+
+ for (const [label, data] of [
+ ["command", command],
+ ["casebook", casebook],
+ ["study", study],
+ ["matchroom", matchroom]
+ ] as const) {
+ literal(data.executionMode, "paper", `${label} paper mode`);
+ literal(data.realMoneyGate, "closed", `${label} real-money gate`);
+ literal(data.tradeable, false, `${label} tradeability`);
+ const policy = record(data.publicDataPolicy, `${label} public-data policy`);
+ literal(policy.derivedOnly, true, `${label} derived-only policy`);
+ literal(policy.credentialsRequired, false, `${label} no-login policy`);
+ literal(policy.walletControlsExposed, false, `${label} wallet-control policy`);
+ literal(policy.txlineProbabilityDisplay, "bucketed_movement_only", `${label} TXLine disclosure policy`);
+ literal(policy.txlineFixtureIdentifiersExposed, false, `${label} TXLine fixture-identifier policy`);
+ }
+
+ const corpus = record(casebook.corpus, "casebook corpus");
+ const statistics = record(casebook.statistics, "casebook statistics");
+ const cases = array(casebook.cases, "casebook cases");
+ const capturedObservations = nonnegativeInteger(statistics.totalCases, "captured observation count");
+ const corpusObservations = nonnegativeInteger(corpus.marketEventCases, "captured corpus count");
+ if (cases.length !== capturedObservations || corpusObservations !== capturedObservations) {
+ throw new Error("Judge evidence captured observation counts do not reconcile");
+ }
+ literal(statistics.executedCases, 0, "captured executed-case count");
+ literal(statistics.capitalMovedMicros, 0, "captured capital-moved count");
+ const capturedDecision = record(matchroom.decision, "captured replay decision");
+ literal(capturedDecision.ordersPlaced, 0, "captured replay order count");
+ literal(capturedDecision.capitalMovedMicros, 0, "captured replay capital-moved count");
+ literal(capturedDecision.walletAccessed, false, "captured replay wallet-access flag");
+
+ const corrected = record(study.correctedHistoricalCandidate, "historical candidate");
+ literal(corrected.evidenceClass, "historical_sampled_price_signal_research", "historical evidence class");
+ literal(corrected.executable, false, "historical execution boundary");
+ const protocol = record(study.protocol, "paper protocol");
+ const risk = record(protocol.risk, "paper protocol risk limits");
+ const counts = qualifyingCounts(protocol);
+
+ const syntheticProof = record(study.syntheticProof, "synthetic proof metadata");
+ literal(syntheticProof.offlineVerified, true, "synthetic receipt verification marker");
+ literal(syntheticProof.externalCalls, 0, "synthetic external-call count");
+ literal(syntheticProof.performanceUse, "excluded_synthetic", "synthetic performance boundary");
+ literal(syntheticProof.solanaAnchorStatus, "not_submitted", "synthetic anchor boundary");
+
+ const download = manifest.downloads.find((candidate) => candidate.file === PUBLIC_SYNTHETIC_RECEIPT_FILENAME);
+ if (!download) throw new Error("Judge evidence requires the frozen synthetic receipt");
+ const receiptBody = await readFile(
+ resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR, PUBLIC_SYNTHETIC_RECEIPT_FILENAME),
+ "utf8"
+ );
+ if (Buffer.byteLength(receiptBody, "utf8") !== download.bytes || dashboardSha256(receiptBody) !== download.sha256) {
+ throw new Error("Judge evidence synthetic receipt does not match the frozen manifest");
+ }
+ let receiptValue: unknown;
+ try {
+ receiptValue = JSON.parse(receiptBody) as unknown;
+ } catch {
+ throw new Error("Judge evidence synthetic receipt is invalid JSON");
+ }
+ const receipt = decisionReceiptSchema.parse(receiptValue);
+ const receiptVerification = verifyDecisionReceipt(receipt);
+ if (
+ !receiptVerification.synthetic ||
+ receiptVerification.lifecycleStatus !== string(syntheticProof.lifecycleStatus, "synthetic lifecycle status") ||
+ receipt.solanaAnchor !== null ||
+ string(syntheticProof.path, "synthetic receipt route") !== `/artifacts/dashboard/${PUBLIC_SYNTHETIC_RECEIPT_FILENAME}`
+ ) {
+ throw new Error("Judge evidence synthetic receipt metadata does not reconcile");
+ }
+
+ return judgeEvidenceResponseSchema.parse({
+ schemaVersion: 1,
+ generatedAt: manifest.generatedAt,
+ app: {
+ name: "Samaritan",
+ service: "samaritan-dashboard",
+ buildCommit: Object.prototype.hasOwnProperty.call(options, "buildCommit")
+ ? options.buildCommit ?? null
+ : await buildCommit(repoRoot)
+ },
+ access: {
+ authenticationRequired: false,
+ readOnly: true,
+ source: "verified_frozen_public_bundle",
+ disclosure: "derived_metadata_only"
+ },
+ trackCriteria: TRACK_CRITERIA,
+ txlineIntegration: {
+ endpointsUsed: TXLINE_ENDPOINTS,
+ publicOutput: "derived_evidence_only"
+ },
+ evidenceClassCounts: {
+ capturedReplayFixtures: nonnegativeInteger(corpus.fixtureCount, "captured fixture count"),
+ capturedReplayObservations: capturedObservations,
+ capturedNoTradeObservations: nonnegativeInteger(statistics.noTradeCases, "captured no-trade count"),
+ historicalTrainingObservations: nonnegativeInteger(corrected.trainingNormalizedCases, "historical training count"),
+ historicalHeldoutObservations: nonnegativeInteger(corrected.heldoutNormalizedCases, "historical heldout count"),
+ historicalHeldoutFixtures: nonnegativeInteger(corrected.heldoutFixtures, "historical heldout fixture count"),
+ syntheticLifecycleReceipts: manifest.downloads.filter((candidate) => candidate.synthetic).length,
+ verifiedCapturedAgentLifecycleReceipts: manifest.downloads.filter((candidate) => !candidate.synthetic).length,
+ registeredForwardPaperSignals: counts.signals
+ },
+ gates: {
+ executionMode: "paper",
+ paperProtocol: {
+ id: string(protocol.version, "paper protocol id"),
+ status: string(protocol.status, "paper protocol status"),
+ active: boolean(protocol.active, "paper protocol active flag"),
+ observationStatus: string(protocol.observationStatus, "paper protocol observation status"),
+ evidencePolicy: string(protocol.evidencePolicy, "paper protocol evidence policy"),
+ qualifyingCounts: counts,
+ riskLimits: {
+ bankrollMicroUsd: positiveInteger(risk.bankrollMicroUsd, "paper bankroll"),
+ perTradeStakeMicroUsd: positiveInteger(risk.perTradeStakeMicroUsd, "paper stake"),
+ aggregateExposureMicroUsd: positiveInteger(risk.aggregateExposureMicroUsd, "paper exposure cap"),
+ drawdownStopMicroUsd: positiveInteger(risk.drawdownStopMicroUsd, "paper drawdown stop")
+ }
+ },
+ realMoney: {
+ gate: "closed",
+ ordersEnabled: false
+ }
+ },
+ syntheticReceipt: {
+ route: `/artifacts/dashboard/${PUBLIC_SYNTHETIC_RECEIPT_FILENAME}`,
+ orderedEventCount: receipt.lifecycle.orderedEventKinds.length,
+ verificationStatus: "offline_verified",
+ lifecycleStatus: receiptVerification.lifecycleStatus,
+ synthetic: true,
+ externalCalls: 0,
+ performanceUse: "excluded_synthetic",
+ solanaAnchorStatus: "not_submitted"
+ },
+ publicLimitations: [
+ "The judge surface is a frozen evidence bundle, not a live private-data relay.",
+ "The captured replay lane is retrospective feasibility evidence and did not enter the agent or execution runtime.",
+ "Historical research lacks executable order-book depth and is not fill or profitability evidence.",
+ "The complete lifecycle fixture uses deterministic model stubs, makes zero external calls, and is excluded from performance claims.",
+ "Real-money execution is disabled; the registered study is forward paper observation only.",
+ "The receipt is verified offline and has not been submitted to Solana."
+ ],
+ documentation: {
+ technicalOverview: "https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/technical-overview.md",
+ txlineApiFeedback: "https://github.com/Pavilion-devs/samaritan/blob/main/docs/submission/txline-api-feedback.md",
+ repository: "https://github.com/Pavilion-devs/samaritan"
+ }
+ });
+}
diff --git a/src/dash/projection.ts b/src/dash/projection.ts
new file mode 100644
index 0000000..2153af0
--- /dev/null
+++ b/src/dash/projection.ts
@@ -0,0 +1,590 @@
+import { createHash } from "node:crypto";
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { z } from "zod";
+import {
+ SPAIN_BELGIUM_MATCHROOM_ID,
+ TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ type DashboardApiResponse,
+ type EvidenceRow,
+ type MatchroomSnapshot,
+ type PublicBookPoint,
+ type ReplayState,
+ type ReplayStepId
+} from "./public-contract.js";
+
+const probabilitySchema = z.number().min(0).max(1);
+// This historical feasibility artifact never entered an execution runtime.
+// These literal boundary values are structural non-execution markers, not
+// ledger-derived trading/account balances.
+const NON_EXECUTION_CAPITAL_MOVED_MICROS = 0 as const;
+const NON_EXECUTION_ORDERS_PLACED = 0 as const;
+const RESEARCH_PATH_WALLET_ACCESSED = false as const;
+
+const outcomeSchema = z.object({
+ outcome: z.string(),
+ consensusProbability: probabilitySchema.nullable(),
+ bestBid: probabilitySchema.nullable(),
+ bestAsk: probabilitySchema.nullable(),
+ spread: probabilitySchema.nullable()
+});
+
+const snapshotSchema = z.object({
+ horizonMs: z.number().int(),
+ book: z.object({
+ outcomes: z.array(outcomeSchema)
+ })
+});
+
+const marketSchema = z.object({
+ marketKey: z.string().min(1),
+ family: z.enum(["match_result", "total_goals"]),
+ lineMilli: z.number().int().nullable(),
+ firstMaterialMoveLatencyMs: z.number().int().nullable(),
+ preTriggerFiveSecondMove: probabilitySchema.nullable(),
+ classification: z.enum(["polymarket_moved_before_txline", "no_material_reprice_in_window"]),
+ snapshots: z.array(snapshotSchema)
+});
+
+const studySchema = z.object({
+ generatedAt: z.string().datetime(),
+ status: z.literal("research_evidence_only"),
+ tradeable: z.literal(false),
+ fixtureId: z.string(),
+ feedHealth: z.object({
+ polymarketOutages: z.object({
+ outages: z.array(z.object({
+ startedAt: z.string().datetime(),
+ endedAt: z.string().datetime().nullable(),
+ durationMs: z.number().int().nullable()
+ })),
+ totalDowntimeMs: z.number().int().nonnegative(),
+ maximumDowntimeMs: z.number().int().nonnegative()
+ })
+ }),
+ gateReadout: z.object({
+ marketEventCases: z.number().int().nonnegative(),
+ polymarketMovedBeforeTxline: z.number().int().nonnegative(),
+ postTxlineRepriceObserved: z.number().int().nonnegative(),
+ noMaterialRepriceInWindow: z.number().int().nonnegative(),
+ staleQuoteHypothesis: z.literal("not_supported_by_this_match")
+ }),
+ goals: z.array(z.object({
+ participant: z.number().int().nullable(),
+ clockSeconds: z.number().int().nonnegative(),
+ firstSeenObservedTsMs: z.number().int(),
+ txlineFirstSeenLatencyMs: z.number().int(),
+ markets: z.array(marketSchema)
+ })).min(1)
+});
+
+const captureConfigSchema = z.object({
+ captureId: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ tradeable: z.literal(false),
+ txline: z.object({
+ fixtureId: z.string(),
+ home: z.string(),
+ away: z.string(),
+ kickoffUtc: z.string().datetime()
+ }),
+ polymarket: z.object({
+ eventSlug: z.string(),
+ home: z.string(),
+ away: z.string(),
+ kickoffUtc: z.string().datetime(),
+ matchResultRulesPeriod: z.literal("first_90_minutes_plus_stoppage_time")
+ })
+});
+
+const manifestSchema = z.object({
+ runId: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ status: z.literal("verified"),
+ fixtureId: z.string(),
+ eventSlug: z.string(),
+ verification: z.object({
+ replayMode: z.literal("capture-order-per-source"),
+ identityParity: z.literal(true),
+ identityHash: z.string().regex(/^[a-f0-9]{64}$/),
+ headHash: z.string().regex(/^[a-f0-9]{64}$/),
+ rows: z.number().int().positive()
+ })
+});
+
+type ParsedSnapshot = z.infer;
+type ParsedStudy = z.infer;
+type ParsedGoal = ParsedStudy["goals"][number];
+type ParsedMarket = ParsedGoal["markets"][number];
+type ParsedCaptureConfig = z.infer;
+type ParsedManifest = z.infer;
+
+export const MATCHROOM_EXEMPLAR_SELECTION_POLICY =
+ "earliest_pretrigger_match_result_then_largest_pretrigger_ask_move" as const;
+
+export type SpainBelgiumFeasibilityCase = {
+ caseId: string;
+ goalOrdinal: number;
+ goalClockSeconds: number;
+ occurredAt: string;
+ marketFamily: "match_result" | "total_goals";
+ lineMilli: number | null;
+ marketLabel: string;
+ classification: "polymarket_moved_before_txline" | "no_material_reprice_in_window";
+ preTriggerMarketMoveBps: number;
+ selectedExemplar: boolean;
+};
+
+export type SpainBelgiumFeasibilityCorpus = {
+ corpusCommitment: string;
+ corpusAssurance: "local_file_sha256_not_capture_manifest_membership";
+ generatedAt: string;
+ fixtureRef: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ home: { name: string; code: string };
+ away: { name: string; code: string };
+ goalEvents: number;
+ marketEventCases: number;
+ movedBeforeTxlineCases: number;
+ noMaterialRepriceCases: number;
+ cleanStaleWindows: 0;
+ selectionPolicy: typeof MATCHROOM_EXEMPLAR_SELECTION_POLICY;
+ selectedCaseId: string;
+ cases: SpainBelgiumFeasibilityCase[];
+};
+
+const replayStateConfiguration: Record = {
+ pre: {
+ horizonMs: -5000,
+ label: "T−5 seconds",
+ conclusionTitle: "Repricing had begun.",
+ conclusionBody: "Polymarket was already moving before TXLine delivered the goal.",
+ decisionExplanation: "The market was moving before the trigger entered Samaritan's event bus."
+ },
+ goal: {
+ horizonMs: 0,
+ label: "Goal first seen",
+ conclusionTitle: "Polymarket moved first.",
+ conclusionBody: "It repriced before TXLine delivered the goal. No stale quote remained.",
+ decisionExplanation: "The candidate arrived after the market had already moved. This retrospective feasibility observation never entered an execution runtime."
+ },
+ post: {
+ horizonMs: 30000,
+ label: "T+30 seconds",
+ conclusionTitle: "The window stayed invalid.",
+ conclusionBody: "A tighter spread did not restore an opportunity that disappeared before the trigger.",
+ decisionExplanation: "The spread tightened after repricing, but the candidate remained ineligible for execution."
+ }
+};
+
+function requiredNumber(value: number | null, field: string): number {
+ if (value === null) throw new Error(`Dashboard projection requires ${field}`);
+ return value;
+}
+
+function bucketedConsensusMoveBps(value: number, baseline: number): number {
+ const exactMoveBps = (value - baseline) * 10_000;
+ const bucketed = Math.round(exactMoveBps / TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS) * TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS;
+ return Object.is(bucketed, -0) ? 0 : bucketed;
+}
+
+function outcomeAt(snapshot: ParsedSnapshot, outcomeName: string): z.infer {
+ const outcome = snapshot.book.outcomes.find((candidate) => candidate.outcome === outcomeName);
+ if (!outcome) throw new Error(`Dashboard projection is missing ${outcomeName} outcome at ${snapshot.horizonMs}ms`);
+ return outcome;
+}
+
+function findSnapshot(snapshots: readonly ParsedSnapshot[], horizonMs: number): ParsedSnapshot {
+ const snapshot = snapshots.find((candidate) => candidate.horizonMs === horizonMs);
+ if (!snapshot) throw new Error(`Dashboard projection is missing ${horizonMs}ms snapshot`);
+ return snapshot;
+}
+
+function clockLabel(clockSeconds: number): string {
+ const minutes = Math.floor(clockSeconds / 60);
+ const seconds = clockSeconds % 60;
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
+}
+
+function titleCase(value: string): string {
+ return value.length === 0 ? value : `${value[0]!.toUpperCase()}${value.slice(1).replaceAll("_", " ")}`;
+}
+
+function marketLabel(market: ParsedMarket): string {
+ if (market.family === "match_result") {
+ if (market.lineMilli !== null) throw new Error("Dashboard projection requires a null Match Result line");
+ return "Match result";
+ }
+ if (market.lineMilli === null) throw new Error("Dashboard projection requires a totals line");
+ return `Full-time total · O/U ${(market.lineMilli / 1_000).toFixed(1)}`;
+}
+
+function marketCaseId(goalOrdinal: number, market: ParsedMarket): string {
+ const marketCode = market.family === "match_result" ? "MR" : `TG-${market.lineMilli ?? "UNKNOWN"}`;
+ return `SB-20260710-G${goalOrdinal.toString().padStart(2, "0")}-${marketCode}`;
+}
+
+function orderedGoals(study: ParsedStudy): ParsedGoal[] {
+ return [...study.goals].sort((left, right) =>
+ left.firstSeenObservedTsMs - right.firstSeenObservedTsMs ||
+ left.clockSeconds - right.clockSeconds
+ );
+}
+
+function orderedMarkets(goal: ParsedGoal): ParsedMarket[] {
+ return [...goal.markets].sort((left, right) => {
+ if (left.family !== right.family) return left.family === "match_result" ? -1 : 1;
+ return (left.lineMilli ?? -1) - (right.lineMilli ?? -1) || left.marketKey.localeCompare(right.marketKey);
+ });
+}
+
+function selectExemplar(study: ParsedStudy): { goal: ParsedGoal; market: ParsedMarket; goalOrdinal: number } {
+ for (const [goalIndex, goal] of orderedGoals(study).entries()) {
+ const market = orderedMarkets(goal).find((candidate) =>
+ candidate.family === "match_result" &&
+ candidate.classification === "polymarket_moved_before_txline"
+ );
+ if (market) return { goal, market, goalOrdinal: goalIndex + 1 };
+ }
+ throw new Error("Dashboard projection requires a pre-trigger Match Result exemplar");
+}
+
+function selectExemplarOutcome(market: ParsedMarket): string {
+ const pre = findSnapshot(market.snapshots, -5000);
+ const trigger = findSnapshot(market.snapshots, 0);
+ const candidates = pre.book.outcomes.flatMap((preOutcome) => {
+ const triggerOutcome = trigger.book.outcomes.find((candidate) => candidate.outcome === preOutcome.outcome);
+ if (!triggerOutcome || preOutcome.bestAsk === null || triggerOutcome.bestAsk === null) return [];
+ return [{
+ outcome: preOutcome.outcome,
+ absoluteAskMove: Math.abs(triggerOutcome.bestAsk - preOutcome.bestAsk)
+ }];
+ }).sort((left, right) => right.absoluteAskMove - left.absoluteAskMove || left.outcome.localeCompare(right.outcome));
+ const selected = candidates[0];
+ if (!selected) throw new Error("Dashboard projection requires a comparable pre-trigger outcome");
+ return selected.outcome;
+}
+
+function teamCodes(eventSlug: string): { home: string; away: string } {
+ const match = /^fifwc-([a-z0-9]{3})-([a-z0-9]{3})-\d{4}-\d{2}-\d{2}$/i.exec(eventSlug);
+ if (!match) throw new Error("Dashboard projection requires a canonical World Cup event slug");
+ return { home: match[1]!.toUpperCase(), away: match[2]!.toUpperCase() };
+}
+
+function scoreAtGoal(goals: readonly ParsedGoal[], selected: ParsedGoal): { home: number; away: number } {
+ let home = 0;
+ let away = 0;
+ for (const goal of goals) {
+ if (goal.firstSeenObservedTsMs > selected.firstSeenObservedTsMs) break;
+ if (goal.participant === 1) home += 1;
+ if (goal.participant === 2) away += 1;
+ }
+ return { home, away };
+}
+
+function scoringTeam(config: ParsedCaptureConfig, goal: ParsedGoal): string {
+ if (goal.participant === 1) return config.txline.home;
+ if (goal.participant === 2) return config.txline.away;
+ throw new Error("Dashboard projection requires the selected goal's participant identity");
+}
+
+function verifyCorpus(study: ParsedStudy): void {
+ const markets = study.goals.flatMap((goal) => goal.markets);
+ const moved = markets.filter((market) => market.classification === "polymarket_moved_before_txline").length;
+ const noMaterial = markets.filter((market) => market.classification === "no_material_reprice_in_window").length;
+ if (
+ markets.length !== study.gateReadout.marketEventCases ||
+ moved !== study.gateReadout.polymarketMovedBeforeTxline ||
+ noMaterial !== study.gateReadout.noMaterialRepriceInWindow ||
+ moved + noMaterial !== markets.length ||
+ study.gateReadout.postTxlineRepriceObserved !== 0
+ ) {
+ throw new Error("Dashboard projection failed closed: feasibility corpus does not reconcile with gate readout");
+ }
+ for (const market of markets) marketLabel(market);
+}
+
+async function loadProjectionEvidence(repoRoot: string): Promise<{
+ study: ParsedStudy;
+ config: ParsedCaptureConfig;
+ manifest: ParsedManifest;
+ corpusCommitment: string;
+}> {
+ const [studyRaw, configRaw, manifestRaw] = await Promise.all([
+ readFile(resolve(repoRoot, "data/research/paired-spain-belgium-2026-07-10-live-lane.json"), "utf8"),
+ readFile(resolve(repoRoot, "config/captures/spain-belgium-2026-07-10.json"), "utf8"),
+ readFile(resolve(repoRoot, "data/live/paired-spain-belgium-2026-07-10/analysis-manifest.json"), "utf8")
+ ]);
+ const study = studySchema.parse(JSON.parse(studyRaw));
+ const config = captureConfigSchema.parse(JSON.parse(configRaw));
+ const manifest = manifestSchema.parse(JSON.parse(manifestRaw));
+
+ if (
+ study.fixtureId !== config.txline.fixtureId ||
+ study.fixtureId !== manifest.fixtureId ||
+ config.polymarket.eventSlug !== manifest.eventSlug ||
+ config.txline.home !== config.polymarket.home ||
+ config.txline.away !== config.polymarket.away ||
+ config.txline.kickoffUtc !== config.polymarket.kickoffUtc
+ ) {
+ throw new Error("Dashboard projection failed closed: capture identity mismatch");
+ }
+ verifyCorpus(study);
+ return {
+ study,
+ config,
+ manifest,
+ corpusCommitment: createHash("sha256").update(studyRaw).digest("hex")
+ };
+}
+
+function createReplayState(
+ id: ReplayStepId,
+ snapshot: ParsedSnapshot,
+ firstSeenObservedTsMs: number,
+ consensusBaseline: number,
+ outcomeName: string
+): ReplayState {
+ const configuration = replayStateConfiguration[id];
+ const outcome = outcomeAt(snapshot, outcomeName);
+ const consensusProbability = requiredNumber(outcome.consensusProbability, `${id} consensus probability`);
+ const bestBid = requiredNumber(outcome.bestBid, `${id} best bid`);
+ const bestAsk = requiredNumber(outcome.bestAsk, `${id} best ask`);
+ return {
+ id,
+ label: configuration.label,
+ offsetMs: configuration.horizonMs,
+ observedAt: new Date(firstSeenObservedTsMs + configuration.horizonMs).toISOString(),
+ consensusMoveFromBaselineBps: bucketedConsensusMoveBps(consensusProbability, consensusBaseline),
+ bestBid,
+ bestAsk,
+ spread: requiredNumber(outcome.spread, `${id} spread`),
+ conclusionTitle: configuration.conclusionTitle,
+ conclusionBody: configuration.conclusionBody,
+ decisionExplanation: configuration.decisionExplanation
+ };
+}
+
+function createEvidence(
+ states: Record,
+ scoringTeamName: string,
+ goalClockLabel: string,
+ outcomeName: string
+): EvidenceRow[] {
+ return [
+ {
+ replayStateId: "pre",
+ observedAt: states.pre.observedAt,
+ offsetLabel: "T−5.000s",
+ source: "Polymarket",
+ observation: `${titleCase(outcomeName)} ask begins repricing`,
+ bestAsk: states.pre.bestAsk,
+ assessment: "Moved first"
+ },
+ {
+ replayStateId: "goal",
+ observedAt: states.goal.observedAt,
+ offsetLabel: "First seen",
+ source: "TXLine",
+ observation: `${scoringTeamName} goal delivered at ${goalClockLabel}`,
+ bestAsk: states.goal.bestAsk,
+ assessment: "Pass"
+ },
+ {
+ replayStateId: "post",
+ observedAt: states.post.observedAt,
+ offsetLabel: "T+30.000s",
+ source: "Samaritan",
+ observation: "Spread tightens after repricing",
+ bestAsk: states.post.bestAsk,
+ assessment: "No trade"
+ }
+ ];
+}
+
+export async function buildSpainBelgiumFeasibilityCorpus(repoRoot: string): Promise {
+ const { study, config, manifest, corpusCommitment } = await loadProjectionEvidence(repoRoot);
+ const goals = orderedGoals(study);
+ const exemplar = selectExemplar(study);
+ const codes = teamCodes(manifest.eventSlug);
+ const selectedCaseId = marketCaseId(exemplar.goalOrdinal, exemplar.market);
+ const cases = goals.flatMap((goal, goalIndex) => orderedMarkets(goal).map((market) => {
+ const goalOrdinal = goalIndex + 1;
+ const caseId = marketCaseId(goalOrdinal, market);
+ return {
+ caseId,
+ goalOrdinal,
+ goalClockSeconds: goal.clockSeconds,
+ occurredAt: new Date(goal.firstSeenObservedTsMs).toISOString(),
+ marketFamily: market.family,
+ lineMilli: market.lineMilli,
+ marketLabel: marketLabel(market),
+ classification: market.classification,
+ preTriggerMarketMoveBps: Math.round(requiredNumber(market.preTriggerFiveSecondMove, "case pre-trigger move") * 10_000),
+ selectedExemplar: caseId === selectedCaseId
+ } satisfies SpainBelgiumFeasibilityCase;
+ }));
+ if (new Set(cases.map((item) => item.caseId)).size !== cases.length) {
+ throw new Error("Dashboard projection failed closed: duplicate feasibility case identity");
+ }
+ if (cases.filter((item) => item.selectedExemplar).length !== 1) {
+ throw new Error("Dashboard projection failed closed: selected exemplar is not unique");
+ }
+ return {
+ corpusCommitment,
+ corpusAssurance: "local_file_sha256_not_capture_manifest_membership",
+ generatedAt: study.generatedAt,
+ fixtureRef: SPAIN_BELGIUM_MATCHROOM_ID,
+ home: { name: config.txline.home, code: codes.home },
+ away: { name: config.txline.away, code: codes.away },
+ goalEvents: goals.length,
+ marketEventCases: study.gateReadout.marketEventCases,
+ movedBeforeTxlineCases: study.gateReadout.polymarketMovedBeforeTxline,
+ noMaterialRepriceCases: study.gateReadout.noMaterialRepriceInWindow,
+ cleanStaleWindows: 0,
+ selectionPolicy: MATCHROOM_EXEMPLAR_SELECTION_POLICY,
+ selectedCaseId,
+ cases
+ };
+}
+
+export async function buildSpainBelgiumMatchroomSnapshot(repoRoot: string): Promise {
+ const { study, config, manifest, corpusCommitment } = await loadProjectionEvidence(repoRoot);
+ const goals = orderedGoals(study);
+ const exemplar = selectExemplar(study);
+ const selectedOutcome = selectExemplarOutcome(exemplar.market);
+ const codes = teamCodes(manifest.eventSlug);
+ const score = scoreAtGoal(goals, exemplar.goal);
+ const selectedGoalClockLabel = clockLabel(exemplar.goal.clockSeconds);
+ const consensusBaseline = requiredNumber(
+ outcomeAt(findSnapshot(exemplar.market.snapshots, -5000), selectedOutcome).consensusProbability,
+ "consensus movement baseline"
+ );
+ const states = {
+ pre: createReplayState("pre", findSnapshot(exemplar.market.snapshots, -5000), exemplar.goal.firstSeenObservedTsMs, consensusBaseline, selectedOutcome),
+ goal: createReplayState("goal", findSnapshot(exemplar.market.snapshots, 0), exemplar.goal.firstSeenObservedTsMs, consensusBaseline, selectedOutcome),
+ post: createReplayState("post", findSnapshot(exemplar.market.snapshots, 30000), exemplar.goal.firstSeenObservedTsMs, consensusBaseline, selectedOutcome)
+ } satisfies Record;
+
+ const chart: PublicBookPoint[] = exemplar.market.snapshots.map((snapshot) => {
+ const outcome = outcomeAt(snapshot, selectedOutcome);
+ return {
+ offsetMs: snapshot.horizonMs,
+ bestBid: requiredNumber(outcome.bestBid, "chart best bid"),
+ bestAsk: requiredNumber(outcome.bestAsk, "chart best ask"),
+ spread: requiredNumber(outcome.spread, "chart spread"),
+ available: true
+ };
+ });
+
+ const firstSeenAt = new Date(exemplar.goal.firstSeenObservedTsMs).toISOString();
+ const windowStart = exemplar.goal.firstSeenObservedTsMs - 5000;
+ const windowEnd = exemplar.goal.firstSeenObservedTsMs + 30000;
+ const availabilityGaps = study.feedHealth.polymarketOutages.outages.flatMap((outage) => {
+ if (outage.endedAt === null || outage.durationMs === null) return [];
+ const start = Date.parse(outage.startedAt);
+ const end = Date.parse(outage.endedAt);
+ return end < windowStart || start > windowEnd ? [] : [{
+ startedAt: outage.startedAt,
+ endedAt: outage.endedAt,
+ durationMs: outage.durationMs
+ }];
+ });
+
+ return {
+ schemaVersion: 2,
+ snapshotId: SPAIN_BELGIUM_MATCHROOM_ID,
+ caseId: marketCaseId(exemplar.goalOrdinal, exemplar.market),
+ casebookCaseCount: study.gateReadout.marketEventCases,
+ generatedAt: study.generatedAt,
+ mode: "captured_replay",
+ executionMode: "paper",
+ realMoneyGate: "closed",
+ tradeable: false,
+ match: {
+ fixtureRef: SPAIN_BELGIUM_MATCHROOM_ID,
+ eventSlug: manifest.eventSlug,
+ competition: "World Cup",
+ stage: "Captured fixture",
+ kickoffUtc: config.txline.kickoffUtc,
+ originalMatchDate: config.txline.kickoffUtc.slice(0, 10),
+ home: { name: config.txline.home, code: codes.home },
+ away: { name: config.txline.away, code: codes.away },
+ scoreAtCursor: score,
+ goalOrdinal: exemplar.goalOrdinal,
+ clockSeconds: exemplar.goal.clockSeconds,
+ clockLabel: selectedGoalClockLabel
+ },
+ market: {
+ family: "match_result",
+ outcome: selectedOutcome,
+ label: `Match result · ${titleCase(selectedOutcome)}`,
+ period: "90 minutes plus stoppage time",
+ mappingStatus: "research_only"
+ },
+ replay: {
+ firstSeenAt,
+ firstSeenLatencyMs: exemplar.goal.txlineFirstSeenLatencyMs,
+ firstMaterialMoveLatencyMs: requiredNumber(exemplar.market.firstMaterialMoveLatencyMs, "material move latency"),
+ preTriggerMarketMoveBps: Math.round(requiredNumber(exemplar.market.preTriggerFiveSecondMove, "pre-trigger move") * 10_000),
+ activeStateId: "goal",
+ states: [states.pre, states.goal, states.post],
+ chart,
+ availabilityGaps
+ },
+ decision: {
+ disposition: "no_trade",
+ semanticStatus: "disciplined_pass",
+ label: "No trade",
+ primaryReason: "Market moved before signal",
+ explanation: states.goal.decisionExplanation,
+ capitalMovedMicros: NON_EXECUTION_CAPITAL_MOVED_MICROS,
+ ordersPlaced: NON_EXECUTION_ORDERS_PLACED,
+ walletAccessed: RESEARCH_PATH_WALLET_ACCESSED,
+ stages: [
+ { id: "signal", label: "Goal observed", detail: "STALE_QUOTE feasibility · research only", status: "complete", timingLabel: "+0ms" },
+ { id: "evidence", label: "Evidence checked", detail: "First material update", status: "complete", timingLabel: `+${requiredNumber(exemplar.market.firstMaterialMoveLatencyMs, "material move latency")}ms` },
+ { id: "pass", label: "Opportunity passed", detail: "No clean stale window", status: "passed", timingLabel: "Final" },
+ { id: "execution", label: "Execution not reached", detail: "Research path ended before an execution runtime", status: "locked", timingLabel: "N/A" }
+ ]
+ },
+ evidence: createEvidence(
+ states,
+ scoringTeam(config, exemplar.goal),
+ selectedGoalClockLabel,
+ selectedOutcome
+ ),
+ proof: {
+ captureStatus: "verified",
+ identityParity: manifest.verification.identityParity,
+ identityHash: manifest.verification.identityHash,
+ headHash: manifest.verification.headHash,
+ canonicalEvents: manifest.verification.rows,
+ replayMode: manifest.verification.replayMode,
+ feedOutageCount: study.feedHealth.polymarketOutages.outages.length,
+ feedDowntimeMs: study.feedHealth.polymarketOutages.totalDowntimeMs,
+ maximumFeedDowntimeMs: study.feedHealth.polymarketOutages.maximumDowntimeMs,
+ gateCases: study.gateReadout.marketEventCases,
+ movedBeforeTxlineCases: study.gateReadout.polymarketMovedBeforeTxline,
+ noMaterialRepriceCases: study.gateReadout.noMaterialRepriceInWindow,
+ cleanStaleWindows: 0,
+ corpusCommitment,
+ corpusAssurance: "local_file_sha256_not_capture_manifest_membership"
+ },
+ publicDataPolicy: {
+ derivedOnly: true,
+ txlineProbabilityDisplay: "bucketed_movement_only",
+ txlineMovementBucketBps: TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ txlineFixtureIdentifiersExposed: false,
+ credentialsRequired: false,
+ walletControlsExposed: false
+ }
+ };
+}
+
+export async function buildSpainBelgiumDashboardResponse(repoRoot: string): Promise {
+ return { data: await buildSpainBelgiumMatchroomSnapshot(repoRoot) };
+}
diff --git a/src/dash/public-bundle.ts b/src/dash/public-bundle.ts
new file mode 100644
index 0000000..6147b6b
--- /dev/null
+++ b/src/dash/public-bundle.ts
@@ -0,0 +1,729 @@
+import { createHash } from "node:crypto";
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { z } from "zod";
+import { stableJson } from "../domain/json.js";
+import {
+ CASEBOOK_SNAPSHOT_ID,
+ COMMAND_SNAPSHOT_ID,
+ FORWARD_PAPER_CONFIG_HASH,
+ FORWARD_PAPER_PROTOCOL_ID,
+ FORWARD_PAPER_REGISTERED_AT,
+ SPAIN_BELGIUM_MATCHROOM_ID,
+ STUDY_SNAPSHOT_ID,
+ TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS
+} from "./public-contract.js";
+
+export const SPAIN_BELGIUM_API_PATH = `/api/v1/matchroom/${SPAIN_BELGIUM_MATCHROOM_ID}` as const;
+export const COMMAND_API_PATH = "/api/v1/command" as const;
+export const CASEBOOK_API_PATH = "/api/v1/casebook" as const;
+export const STUDY_API_PATH = "/api/v1/study" as const;
+export const PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR = "public/artifacts/dashboard" as const;
+export const PUBLIC_DASHBOARD_BUNDLE_ID = "samaritan-dashboard-public-v1" as const;
+export const PUBLIC_DASHBOARD_CANONICALIZATION = "samaritan-stable-json-v1" as const;
+export const PUBLIC_DASHBOARD_BUNDLE_HASH_DOMAIN = "samaritan.dashboard-public-bundle/v1" as const;
+export const PUBLIC_SYNTHETIC_RECEIPT_FILENAME = "synthetic-decision-receipt.json" as const;
+
+export const PUBLIC_DASHBOARD_FILES = [
+ { apiPath: COMMAND_API_PATH, file: "command.json", snapshotId: COMMAND_SNAPSHOT_ID },
+ { apiPath: CASEBOOK_API_PATH, file: "casebook.json", snapshotId: CASEBOOK_SNAPSHOT_ID },
+ { apiPath: STUDY_API_PATH, file: "study.json", snapshotId: STUDY_SNAPSHOT_ID },
+ { apiPath: SPAIN_BELGIUM_API_PATH, file: "matchroom-spain-belgium.json", snapshotId: SPAIN_BELGIUM_MATCHROOM_ID }
+] as const;
+
+export type PublicDashboardApiPath = typeof PUBLIC_DASHBOARD_FILES[number]["apiPath"];
+export type PublicDashboardFilename = typeof PUBLIC_DASHBOARD_FILES[number]["file"];
+
+const isoSchema = z.string().datetime();
+const hashSchema = z.string().regex(/^[a-f0-9]{64}$/);
+const probabilitySchema = z.number().finite().min(0).max(1);
+const nonnegativeIntegerSchema = z.number().int().nonnegative().safe();
+const studyCountsSchema = z.object({
+ matches: nonnegativeIntegerSchema,
+ signals: nonnegativeIntegerSchema,
+ filledMatches: nonnegativeIntegerSchema,
+ fills: nonnegativeIntegerSchema,
+ settledFills: nonnegativeIntegerSchema
+}).strict().superRefine((counts, context) => {
+ if (counts.filledMatches > counts.matches || counts.fills > counts.signals || counts.settledFills > counts.fills) {
+ context.addIssue({ code: "custom", message: "Study counts do not reconcile" });
+ }
+});
+
+export const publicDataPolicySchema = z.object({
+ derivedOnly: z.literal(true),
+ txlineProbabilityDisplay: z.literal("bucketed_movement_only"),
+ txlineMovementBucketBps: z.literal(TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS),
+ txlineFixtureIdentifiersExposed: z.literal(false),
+ credentialsRequired: z.literal(false),
+ walletControlsExposed: z.literal(false)
+}).strict();
+
+const publicBookPointSchema = z.object({
+ offsetMs: z.number().int(),
+ bestBid: probabilitySchema,
+ bestAsk: probabilitySchema,
+ spread: probabilitySchema,
+ available: z.literal(true)
+}).strict();
+
+const replayStateSchema = z.object({
+ id: z.enum(["pre", "goal", "post"]),
+ label: z.string().min(1),
+ offsetMs: z.number().int(),
+ observedAt: isoSchema,
+ consensusMoveFromBaselineBps: z.number().int().multipleOf(TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS),
+ bestBid: probabilitySchema,
+ bestAsk: probabilitySchema,
+ spread: probabilitySchema,
+ conclusionTitle: z.string().min(1),
+ conclusionBody: z.string().min(1),
+ decisionExplanation: z.string().min(1)
+}).strict();
+
+const decisionStageSchema = z.object({
+ id: z.enum(["signal", "evidence", "pass", "execution"]),
+ label: z.string().min(1),
+ detail: z.string().min(1),
+ status: z.enum(["complete", "passed", "locked"]),
+ timingLabel: z.string().min(1)
+}).strict();
+
+const evidenceRowSchema = z.object({
+ replayStateId: z.enum(["pre", "goal", "post"]),
+ observedAt: isoSchema,
+ offsetLabel: z.string().min(1),
+ source: z.enum(["Polymarket", "TXLine", "Samaritan"]),
+ observation: z.string().min(1),
+ bestAsk: probabilitySchema,
+ assessment: z.enum(["Moved first", "Pass", "No trade"])
+}).strict();
+
+const matchSchema = z.object({
+ fixtureRef: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ eventSlug: z.string().min(1),
+ competition: z.literal("World Cup"),
+ stage: z.literal("Captured fixture"),
+ kickoffUtc: isoSchema,
+ originalMatchDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
+ home: z.object({ name: z.string().min(1), code: z.string().regex(/^[A-Z0-9]{3}$/) }).strict(),
+ away: z.object({ name: z.string().min(1), code: z.string().regex(/^[A-Z0-9]{3}$/) }).strict(),
+ scoreAtCursor: z.object({ home: nonnegativeIntegerSchema, away: nonnegativeIntegerSchema }).strict(),
+ goalOrdinal: z.number().int().positive().safe(),
+ clockSeconds: nonnegativeIntegerSchema,
+ clockLabel: z.string().min(1)
+}).strict();
+
+const decisionSchema = z.object({
+ disposition: z.literal("no_trade"),
+ semanticStatus: z.literal("disciplined_pass"),
+ label: z.literal("No trade"),
+ primaryReason: z.literal("Market moved before signal"),
+ explanation: z.string().min(1),
+ capitalMovedMicros: z.literal(0),
+ ordersPlaced: z.literal(0),
+ walletAccessed: z.literal(false),
+ stages: z.array(decisionStageSchema).length(4)
+}).strict();
+
+const proofSchema = z.object({
+ captureStatus: z.literal("verified"),
+ identityParity: z.literal(true),
+ identityHash: hashSchema,
+ headHash: hashSchema,
+ canonicalEvents: z.number().int().positive().safe(),
+ replayMode: z.literal("capture-order-per-source"),
+ feedOutageCount: nonnegativeIntegerSchema,
+ feedDowntimeMs: nonnegativeIntegerSchema,
+ maximumFeedDowntimeMs: nonnegativeIntegerSchema,
+ gateCases: nonnegativeIntegerSchema,
+ movedBeforeTxlineCases: nonnegativeIntegerSchema,
+ noMaterialRepriceCases: nonnegativeIntegerSchema,
+ cleanStaleWindows: z.literal(0),
+ corpusCommitment: hashSchema,
+ corpusAssurance: z.literal("local_file_sha256_not_capture_manifest_membership")
+}).strict();
+
+export const matchroomApiResponseSchema = z.object({
+ data: z.object({
+ schemaVersion: z.literal(2),
+ snapshotId: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ caseId: z.string().regex(/^SB-20260710-G\d{2}-(?:MR|TG-\d+)$/),
+ casebookCaseCount: z.number().int().positive().safe(),
+ generatedAt: isoSchema,
+ mode: z.literal("captured_replay"),
+ executionMode: z.literal("paper"),
+ realMoneyGate: z.literal("closed"),
+ tradeable: z.literal(false),
+ match: matchSchema,
+ market: z.object({
+ family: z.literal("match_result"),
+ outcome: z.enum(["home", "draw", "away"]),
+ label: z.string().min(1),
+ period: z.literal("90 minutes plus stoppage time"),
+ mappingStatus: z.literal("research_only")
+ }).strict(),
+ replay: z.object({
+ firstSeenAt: isoSchema,
+ firstSeenLatencyMs: z.number().int(),
+ firstMaterialMoveLatencyMs: z.number().int(),
+ preTriggerMarketMoveBps: z.number().int(),
+ activeStateId: z.literal("goal"),
+ states: z.array(replayStateSchema).length(3),
+ chart: z.array(publicBookPointSchema).min(1),
+ availabilityGaps: z.array(z.object({
+ startedAt: isoSchema,
+ endedAt: isoSchema,
+ durationMs: nonnegativeIntegerSchema
+ }).strict())
+ }).strict(),
+ decision: decisionSchema,
+ evidence: z.array(evidenceRowSchema).length(3),
+ proof: proofSchema,
+ publicDataPolicy: publicDataPolicySchema
+ }).strict()
+}).strict().superRefine((value, context) => {
+ if (value.data.casebookCaseCount !== value.data.proof.gateCases) {
+ context.addIssue({
+ code: "custom",
+ message: "Matchroom case count must reconcile with the gate corpus"
+ });
+ }
+});
+
+const commandCaseSchema = z.object({
+ caseId: z.string().min(1),
+ matchroomId: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ fixtureRef: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ fixtureLabel: z.string().min(1),
+ home: matchSchema.shape.home,
+ away: matchSchema.shape.away,
+ occurredAt: isoSchema,
+ marketLabel: z.string().min(1),
+ marketOutcomeLabel: z.string().min(1),
+ candidateLabel: z.literal("Live-lane gate readout"),
+ disposition: z.literal("no_trade"),
+ dispositionLabel: z.string().min(1),
+ reason: z.string().min(1),
+ evidenceStatus: z.literal("verified_replay"),
+ preTriggerMarketMoveBps: z.number().int(),
+ consensusMoveFromBaselineBps: z.number().int().multipleOf(TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS),
+ bestAsk: probabilitySchema,
+ canonicalEvents: z.number().int().positive().safe(),
+ capitalMovedMicros: z.literal(0),
+ ordersPlaced: z.literal(0),
+ walletAccessed: z.literal(false)
+}).strict();
+
+export const commandApiResponseSchema = z.object({
+ data: z.object({
+ schemaVersion: z.literal(3),
+ snapshotId: z.literal(COMMAND_SNAPSHOT_ID),
+ generatedAt: isoSchema,
+ mode: z.literal("offline_artifact"),
+ executionMode: z.literal("paper"),
+ realMoneyGate: z.literal("closed"),
+ tradeable: z.literal(false),
+ system: z.object({
+ posture: z.enum(["standing_by", "capture_window", "awaiting_verification"]),
+ label: z.string().min(1),
+ detail: z.string().min(1),
+ feeds: z.array(z.object({
+ id: z.enum(["txline", "polymarket", "decision_ledger", "replay_proof"]),
+ label: z.string().min(1),
+ status: z.enum(["scheduled", "running", "complete", "failed", "unknown", "initialized", "verified"]),
+ statusLabel: z.string().min(1),
+ detail: z.string().min(1)
+ }).strict()).length(4)
+ }).strict(),
+ featuredCase: commandCaseSchema.extend({
+ scoreLabel: z.string().min(1),
+ scoreAtCursor: matchSchema.shape.scoreAtCursor,
+ clockSeconds: nonnegativeIntegerSchema,
+ clockLabel: z.string().min(1),
+ conclusion: z.string().min(1),
+ identityParity: z.literal(true),
+ chart: z.array(publicBookPointSchema).min(1)
+ }).strict(),
+ fixtureSchedule: z.array(z.object({
+ captureId: z.string().regex(/^paired-[a-z0-9-]+$/),
+ home: z.object({ name: z.string().min(1), code: z.string().min(2) }).strict(),
+ away: z.object({ name: z.string().min(1), code: z.string().min(2) }).strict(),
+ kickoffUtc: isoSchema,
+ captureStartUtc: isoSchema,
+ captureEndUtc: isoSchema,
+ signalCutoffUtc: isoSchema,
+ eventSlug: z.string().min(1),
+ phase: z.enum(["scheduled", "running", "complete", "failed", "unknown"]),
+ statusLabel: z.string().min(1),
+ statusDetail: z.string().min(1),
+ statusSource: z.enum(["analysis_manifest", "supervisor_status", "reviewed_config", "none"]),
+ statusUpdatedAt: isoSchema.nullable(),
+ terminalEvidence: z.object({
+ windowStartUtc: isoSchema,
+ windowEndUtc: isoSchema,
+ synchronizedStartUtc: isoSchema,
+ synchronizedEndUtc: isoSchema,
+ streamCount: z.literal(3)
+ }).strict().nullable(),
+ identityStatus: z.enum(["exact_match_confirmed", "historical_reviewed_config"]),
+ captureOnly: z.literal(true),
+ tradeable: z.literal(false)
+ }).strict()).min(1),
+ recentCases: z.array(commandCaseSchema).length(1),
+ additionalCaseState: z.object({
+ status: z.literal("waiting_for_eligible_capture"),
+ label: z.literal("Registered v2 awaits fresh eligible evidence"),
+ detail: z.string().min(1)
+ }).strict(),
+ study: z.object({
+ protocolVersion: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ protocolStatus: z.literal("registered"),
+ configHash: z.literal(FORWARD_PAPER_CONFIG_HASH),
+ registeredAt: z.literal(FORWARD_PAPER_REGISTERED_AT),
+ status: z.literal("active_forward_paper"),
+ statusLabel: z.literal("V2 registered"),
+ observationStatus: z.enum(["awaiting_fresh_evidence", "collecting_forward_evidence"]),
+ qualifyingCounts: studyCountsSchema,
+ requiredFilledMatches: z.number().int().positive(),
+ requiredFills: z.number().int().positive(),
+ bountyStatus: z.literal("exploratory"),
+ stoppingRuleMet: z.boolean(),
+ realMoneyGate: z.literal("closed"),
+ reason: z.string().min(1),
+ historicalV1: z.object({
+ protocolVersion: z.literal("paper-study-v1-2026-07-12"),
+ protocolStatus: z.literal("invalidated_suspended"),
+ configHash: hashSchema,
+ startedAt: isoSchema,
+ zeroObservationAudit: z.literal(true)
+ }).strict()
+ }).strict(),
+ proof: z.object({
+ replayIdentityParity: z.literal(true),
+ replayIdentityHash: hashSchema,
+ canonicalEvents: z.number().int().positive().safe(),
+ bountyLedgerValid: z.literal(true),
+ bountyLedgerRows: z.number().int().positive(),
+ longRunLedgerValid: z.literal(true),
+ longRunLedgerRows: z.number().int().positive(),
+ evidenceFixtures: nonnegativeIntegerSchema,
+ pairedBookReplays: nonnegativeIntegerSchema,
+ signalResearchOnly: nonnegativeIntegerSchema
+ }).strict(),
+ sourceFreshness: z.object({
+ paperReportGeneratedAt: isoSchema,
+ fixtureUniverseGeneratedAt: isoSchema,
+ replayGeneratedAt: isoSchema
+ }).strict(),
+ publicDataPolicy: publicDataPolicySchema
+ }).strict()
+}).strict();
+
+const casebookSummarySchema = z.object({
+ caseId: z.string().regex(/^SB-20260710-G\d{2}-(?:MR|TG-\d+)$/),
+ matchroomId: z.literal(SPAIN_BELGIUM_MATCHROOM_ID).nullable(),
+ selectedExemplar: z.boolean(),
+ goalOrdinal: z.number().int().positive().safe(),
+ goalClockSeconds: nonnegativeIntegerSchema,
+ occurredAt: isoSchema,
+ fixtureRef: z.literal(SPAIN_BELGIUM_MATCHROOM_ID),
+ fixtureLabel: z.string().min(1),
+ homeCode: z.string().min(2),
+ awayCode: z.string().min(2),
+ marketFamily: z.enum(["Match result", "Full-time total"]),
+ marketLabel: z.string().min(1),
+ lineMilli: z.number().int().nullable(),
+ classification: z.enum(["polymarket_moved_before_txline", "no_material_reprice_in_window"]),
+ detector: z.literal("STALE_QUOTE_FEASIBILITY"),
+ disposition: z.string().min(1),
+ executionOutcome: z.string().min(1),
+ evidenceLane: z.literal("Research only"),
+ source: z.literal("Captured replay"),
+ verificationStatus: z.literal("Internally reconciled"),
+ reason: z.string().min(1),
+ preTriggerMarketMoveBps: z.number().int()
+}).strict();
+
+export const casebookApiResponseSchema = z.object({
+ data: z.object({
+ schemaVersion: z.literal(3),
+ snapshotId: z.literal(CASEBOOK_SNAPSHOT_ID),
+ generatedAt: isoSchema,
+ mode: z.literal("offline_artifact"),
+ executionMode: z.literal("paper"),
+ realMoneyGate: z.literal("closed"),
+ tradeable: z.literal(false),
+ statistics: z.object({
+ totalCases: z.number().int().positive().safe(),
+ noTradeCases: z.number().int().positive().safe(),
+ executedCases: z.literal(0),
+ reconciledCases: z.number().int().positive().safe(),
+ capitalMovedMicros: z.literal(0)
+ }).strict(),
+ corpus: z.object({
+ unit: z.literal("goal_market_feasibility_observation"),
+ coverage: z.literal("all_reported_goal_market_cases"),
+ captureReplays: z.literal(1),
+ fixtureCount: z.literal(1),
+ goalEvents: z.number().int().positive().safe(),
+ marketEventCases: z.number().int().positive().safe(),
+ movedBeforeTxlineCases: nonnegativeIntegerSchema,
+ noMaterialRepriceCases: nonnegativeIntegerSchema,
+ cleanStaleWindows: z.literal(0),
+ commitment: hashSchema,
+ assurance: z.literal("local_file_sha256_not_capture_manifest_membership"),
+ selectedExemplar: z.object({
+ caseId: z.string().min(1),
+ policy: z.literal("earliest_pretrigger_match_result_then_largest_pretrigger_ask_move"),
+ detail: z.string().min(1)
+ }).strict()
+ }).strict(),
+ filterOptions: z.object({
+ fixtures: z.array(z.string()),
+ marketFamilies: z.array(z.string()),
+ detectors: z.array(z.string()),
+ dispositions: z.array(z.string()),
+ executionOutcomes: z.array(z.string()),
+ evidenceLanes: z.array(z.string()),
+ sources: z.array(z.string())
+ }).strict(),
+ cases: z.array(casebookSummarySchema).min(1),
+ selectedCase: z.object({
+ summary: casebookSummarySchema,
+ match: matchSchema,
+ decision: decisionSchema,
+ lifecycle: z.array(decisionStageSchema).length(4),
+ evidenceReadout: z.object({
+ consensusMoveFromBaselineBps: z.number().int().multipleOf(TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS),
+ bestBid: probabilitySchema,
+ bestAsk: probabilitySchema,
+ spread: probabilitySchema,
+ preTriggerMarketMoveBps: z.number().int(),
+ movementConclusion: z.literal("Polymarket moved before TXLine")
+ }).strict(),
+ analysis: z.object({
+ thesisStatus: z.literal("not_requested"),
+ thesisReason: z.string().min(1),
+ invalidation: z.string().min(1),
+ costStatus: z.literal("not_applicable"),
+ costMicros: z.literal(0),
+ costReason: z.string().min(1)
+ }).strict(),
+ evidence: z.array(evidenceRowSchema).length(3),
+ proof: proofSchema
+ }).strict(),
+ nextEvidence: z.object({
+ status: z.literal("waiting_for_verified_capture"),
+ label: z.string().min(1),
+ detail: z.string().min(1)
+ }).strict(),
+ publicDataPolicy: publicDataPolicySchema
+ }).strict()
+}).strict().superRefine((value, context) => {
+ const { data } = value;
+ const selectedRows = data.cases.filter((item) => item.selectedExemplar);
+ if (
+ data.statistics.totalCases !== data.cases.length ||
+ data.statistics.noTradeCases !== data.cases.length ||
+ data.statistics.reconciledCases !== data.cases.length ||
+ data.corpus.marketEventCases !== data.cases.length ||
+ data.corpus.movedBeforeTxlineCases + data.corpus.noMaterialRepriceCases !== data.cases.length ||
+ selectedRows.length !== 1 ||
+ selectedRows[0]?.caseId !== data.corpus.selectedExemplar.caseId ||
+ data.selectedCase.summary.caseId !== data.corpus.selectedExemplar.caseId ||
+ data.selectedCase.summary.matchroomId !== SPAIN_BELGIUM_MATCHROOM_ID ||
+ data.cases.some((item) => item.selectedExemplar !== (item.matchroomId !== null))
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Casebook corpus counts or exemplar identity do not reconcile"
+ });
+ }
+ for (const item of data.cases) {
+ const lineIsValid = item.marketFamily === "Match result" ? item.lineMilli === null : item.lineMilli !== null;
+ if (!lineIsValid) {
+ context.addIssue({ code: "custom", message: `Casebook market line is invalid for ${item.caseId}` });
+ }
+ }
+});
+
+export const correctedHistoricalCandidateSchema = z.object({
+ schemaVersion: z.literal(4),
+ generatedAt: isoSchema,
+ protocolId: z.literal("historical-gate-causal-economic-v4-2026-07-14"),
+ configurationHash: hashSchema,
+ status: z.literal("historical_signal_candidate_for_forward_paper_review"),
+ sourceRegistrationAtGeneration: z.literal("engineering_candidate_unregistered"),
+ activeStudyAtGeneration: z.literal(false),
+ detector: z.literal("CONSENSUS_MOVE"),
+ marketFamily: z.literal("Full-time totals"),
+ trainingNormalizedCases: z.literal(135),
+ heldoutNormalizedCases: z.literal(38),
+ heldoutFixtures: z.literal(18),
+ costProxyBps: z.literal(100),
+ meanNetAfterCostProxyBps: z.literal(132.7),
+ matchClustered95Bps: z.object({
+ iterations: z.literal(10_000),
+ cluster: z.literal("fixture"),
+ low: z.literal(14.3),
+ high: z.literal(243.9)
+ }).strict(),
+ evidenceClass: z.literal("historical_sampled_price_signal_research"),
+ executionEvidence: z.literal("not_established_no_historical_bid_ask_or_depth"),
+ executable: z.literal(false),
+ claimBoundary: z.literal("Historical sampled-price evidence justified review only; the separate v2 registration does not turn it into alpha, fill proof, profitability, or permission for real money.")
+}).strict();
+
+export const syntheticProofReceiptSchema = z.object({
+ label: z.literal("Synthetic full-lifecycle proving fixture"),
+ path: z.literal("/artifacts/dashboard/synthetic-decision-receipt.json"),
+ lifecycleStatus: z.literal("filled_settled"),
+ offlineVerified: z.literal(true),
+ performanceUse: z.literal("excluded_synthetic"),
+ externalCalls: z.literal(0),
+ solanaAnchorStatus: z.literal("not_submitted"),
+ explanation: z.literal("Closed-world production-component demo; separate from historical evidence and excluded from every performance claim.")
+}).strict();
+
+export const studyApiResponseSchema = z.object({
+ data: z.object({
+ schemaVersion: z.literal(3),
+ snapshotId: z.literal(STUDY_SNAPSHOT_ID),
+ generatedAt: isoSchema,
+ mode: z.literal("offline_artifact"),
+ executionMode: z.literal("paper"),
+ realMoneyGate: z.literal("closed"),
+ tradeable: z.literal(false),
+ protocol: z.object({
+ version: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ status: z.literal("registered"),
+ activity: z.literal("active_forward_paper"),
+ active: z.literal(true),
+ registeredAt: z.literal(FORWARD_PAPER_REGISTERED_AT),
+ configHash: z.literal(FORWARD_PAPER_CONFIG_HASH),
+ realMoneyGate: z.literal("closed"),
+ observationStatus: z.enum(["awaiting_fresh_evidence", "collecting_forward_evidence"]),
+ evidencePolicy: z.literal("fresh_forward_only"),
+ qualifyingCounts: studyCountsSchema,
+ candidate: z.object({
+ detector: z.literal("CONSENSUS_MOVE"),
+ marketFamily: z.literal("Full-time totals only"),
+ moveAbsZ: z.literal(1),
+ cusumThresholdBps: z.literal(10),
+ minimumGapBps: z.literal(100),
+ minimumUpdates: z.literal(5),
+ selector: z.literal("Closest to even"),
+ minimumCoveragePoints: z.literal(1_000),
+ maximumDistanceFromEven: z.literal(0.15)
+ }).strict(),
+ evaluation: z.object({
+ unitOfAnalysis: z.literal("match"),
+ primaryEndpoint: z.literal("Executable CLV net of measured costs"),
+ minimumFilledMatches: z.literal(20),
+ minimumFills: z.literal(40),
+ targetMatches: z.literal(30),
+ bootstrapIterations: z.literal(10_000),
+ bootstrapSeed: z.literal(20_260_714),
+ randomDirectionControl: z.literal("Seeded matched-cost sign flip")
+ }).strict(),
+ risk: z.object({
+ bankrollMicroUsd: z.literal(50_000_000),
+ perTradeStakeMicroUsd: z.literal(3_000_000),
+ aggregateExposureMicroUsd: z.literal(15_000_000),
+ drawdownStopMicroUsd: z.literal(20_000_000)
+ }).strict(),
+ guardrailThresholds: z.object({
+ minimumFillRate: z.literal(0.6),
+ maximumMeanSlippageBps: z.literal(100),
+ maximumDrawdownMicroUsd: z.literal(20_000_000),
+ selectedDepthRequired: z.literal(true)
+ }).strict()
+ }).strict(),
+ historicalV1: z.object({
+ protocolVersion: z.literal("paper-study-v1-2026-07-12"),
+ status: z.literal("invalidated_suspended"),
+ active: z.literal(false),
+ invalidatedBeforeObservations: z.literal(true),
+ configHash: hashSchema,
+ startedAt: isoSchema,
+ lanes: z.object({
+ bounty: z.object({
+ label: z.literal("Preserved invalidated v1 bounty ledger"),
+ sourceStatus: z.literal("exploratory"),
+ statusLabel: z.literal("V1 invalidated"),
+ reason: z.string().min(1),
+ counts: studyCountsSchema,
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: hashSchema }).strict(),
+ canSatisfyGate: z.literal(false)
+ }).strict(),
+ longRun: z.object({
+ label: z.literal("Preserved invalidated v1 long-run ledger"),
+ sourceStatus: z.literal("sealed"),
+ statusLabel: z.literal("V1 invalidated"),
+ reason: z.string().min(1),
+ counts: studyCountsSchema,
+ stoppingRuleMet: z.literal(false),
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: hashSchema }).strict(),
+ canSatisfyGate: z.literal(false)
+ }).strict()
+ }).strict(),
+ results: z.object({
+ visibility: z.literal("sealed"),
+ rows: z.null(),
+ endpoints: z.null(),
+ guardrails: z.null()
+ }).strict()
+ }).strict(),
+ results: z.object({
+ visibility: z.literal("sealed"),
+ rows: z.null(),
+ endpoints: z.null(),
+ guardrails: z.null()
+ }).strict(),
+ correctedHistoricalCandidate: correctedHistoricalCandidateSchema,
+ syntheticProof: syntheticProofReceiptSchema,
+ fixtureUniverse: z.object({
+ generatedAt: isoSchema,
+ evidenceFixtures: nonnegativeIntegerSchema,
+ pairedBookReplays: nonnegativeIntegerSchema,
+ executableBookReplays: nonnegativeIntegerSchema,
+ signalResearchOnly: nonnegativeIntegerSchema,
+ longRunEligible: nonnegativeIntegerSchema
+ }).strict(),
+ decisionRules: z.object({
+ accept: z.array(z.string().min(1)).min(1),
+ reject: z.array(z.string().min(1)).min(1),
+ inconclusive: z.array(z.string().min(1)).min(1)
+ }).strict(),
+ publicDataPolicy: publicDataPolicySchema
+ }).strict()
+}).strict();
+
+const manifestEntrySchema = z.object({
+ apiPath: z.enum([COMMAND_API_PATH, CASEBOOK_API_PATH, STUDY_API_PATH, SPAIN_BELGIUM_API_PATH]),
+ file: z.enum(["command.json", "casebook.json", "study.json", "matchroom-spain-belgium.json"]),
+ snapshotId: z.enum([COMMAND_SNAPSHOT_ID, CASEBOOK_SNAPSHOT_ID, STUDY_SNAPSHOT_ID, SPAIN_BELGIUM_MATCHROOM_ID]),
+ sha256: hashSchema,
+ bytes: z.number().int().positive().safe()
+}).strict();
+
+export type PublicDashboardManifestEntry = z.infer;
+
+const manifestDownloadSchema = z.object({
+ id: z.literal("synthetic-decision-receipt"),
+ file: z.literal(PUBLIC_SYNTHETIC_RECEIPT_FILENAME),
+ mediaType: z.literal("application/json"),
+ sha256: hashSchema,
+ bytes: z.number().int().positive().safe(),
+ synthetic: z.literal(true),
+ performanceUse: z.literal("excluded_synthetic")
+}).strict();
+
+export type PublicDashboardManifestDownload = z.infer;
+
+export const publicDashboardManifestSchema = z.object({
+ schemaVersion: z.literal(1),
+ bundleId: z.literal(PUBLIC_DASHBOARD_BUNDLE_ID),
+ generatedAt: isoSchema,
+ canonicalization: z.literal(PUBLIC_DASHBOARD_CANONICALIZATION),
+ bundleSha256: hashSchema,
+ files: z.array(manifestEntrySchema).length(PUBLIC_DASHBOARD_FILES.length),
+ downloads: z.array(manifestDownloadSchema).length(1),
+ publicDataPolicy: publicDataPolicySchema
+}).strict();
+
+export type PublicDashboardManifest = z.infer;
+
+export function sha256(value: string | Buffer): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+export function publicDashboardBundleHash(
+ generatedAt: string,
+ entries: readonly PublicDashboardManifestEntry[],
+ downloads: readonly PublicDashboardManifestDownload[]
+): string {
+ return sha256(`${PUBLIC_DASHBOARD_BUNDLE_HASH_DOMAIN}\n${stableJson({
+ schemaVersion: 1,
+ bundleId: PUBLIC_DASHBOARD_BUNDLE_ID,
+ generatedAt,
+ canonicalization: PUBLIC_DASHBOARD_CANONICALIZATION,
+ files: entries,
+ downloads,
+ publicDataPolicy: {
+ derivedOnly: true,
+ txlineProbabilityDisplay: "bucketed_movement_only",
+ txlineMovementBucketBps: TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ credentialsRequired: false,
+ walletControlsExposed: false,
+ txlineFixtureIdentifiersExposed: false
+ }
+ })}`);
+}
+
+export function parsePublicDashboardResponse(pathname: PublicDashboardApiPath, value: unknown): unknown {
+ if (pathname === COMMAND_API_PATH) return commandApiResponseSchema.parse(value);
+ if (pathname === CASEBOOK_API_PATH) return casebookApiResponseSchema.parse(value);
+ if (pathname === STUDY_API_PATH) return studyApiResponseSchema.parse(value);
+ return matchroomApiResponseSchema.parse(value);
+}
+
+export function parsePublicDashboardManifest(value: unknown): PublicDashboardManifest {
+ const manifest = publicDashboardManifestSchema.parse(value);
+ for (const [index, definition] of PUBLIC_DASHBOARD_FILES.entries()) {
+ const entry = manifest.files[index];
+ if (
+ !entry ||
+ entry.apiPath !== definition.apiPath ||
+ entry.file !== definition.file ||
+ entry.snapshotId !== definition.snapshotId
+ ) {
+ throw new Error(`Public dashboard manifest entry ${index} does not match the frozen route map`);
+ }
+ }
+ const expectedBundleHash = publicDashboardBundleHash(manifest.generatedAt, manifest.files, manifest.downloads);
+ if (manifest.bundleSha256 !== expectedBundleHash) {
+ throw new Error("Public dashboard manifest bundle hash is invalid");
+ }
+ return manifest;
+}
+
+export type FrozenDashboardResponse = {
+ body: string;
+ manifest: PublicDashboardManifest;
+};
+
+export async function readFrozenDashboardResponse(
+ repoRoot: string,
+ pathname: PublicDashboardApiPath
+): Promise {
+ const bundleRoot = resolve(repoRoot, PUBLIC_DASHBOARD_BUNDLE_RELATIVE_DIR);
+ let manifestValue: unknown;
+ try {
+ manifestValue = JSON.parse(await readFile(resolve(bundleRoot, "manifest.json"), "utf8")) as unknown;
+ } catch (error) {
+ throw new Error(`Frozen dashboard manifest is unavailable: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ const manifest = parsePublicDashboardManifest(manifestValue);
+ const entry = manifest.files.find((candidate) => candidate.apiPath === pathname);
+ if (!entry) throw new Error(`Frozen dashboard manifest has no entry for ${pathname}`);
+ const body = await readFile(resolve(bundleRoot, entry.file), "utf8");
+ if (Buffer.byteLength(body, "utf8") !== entry.bytes) {
+ throw new Error(`Frozen dashboard artifact byte count changed for ${entry.file}`);
+ }
+ if (sha256(body) !== entry.sha256) {
+ throw new Error(`Frozen dashboard artifact hash changed for ${entry.file}`);
+ }
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(body) as unknown;
+ } catch {
+ throw new Error(`Frozen dashboard artifact is invalid JSON: ${entry.file}`);
+ }
+ const response = parsePublicDashboardResponse(pathname, parsed) as { data?: { snapshotId?: unknown } };
+ if (response.data?.snapshotId !== entry.snapshotId) {
+ throw new Error(`Frozen dashboard artifact snapshot identity changed for ${entry.file}`);
+ }
+ return { body, manifest };
+}
diff --git a/src/dash/public-contract.ts b/src/dash/public-contract.ts
new file mode 100644
index 0000000..adad18a
--- /dev/null
+++ b/src/dash/public-contract.ts
@@ -0,0 +1,598 @@
+import {
+ PAPER_STUDY_FROZEN_CONFIG_SHA256,
+ PAPER_STUDY_PROTOCOL_VERSION,
+ PAPER_STUDY_REGISTERED_AT
+} from "../harness/paper-study-ledger.js";
+
+export const SPAIN_BELGIUM_MATCHROOM_ID = "paired-spain-belgium-2026-07-10";
+export const COMMAND_SNAPSHOT_ID = "command-2026-world-cup";
+export const CASEBOOK_SNAPSHOT_ID = "casebook-2026-world-cup";
+export const STUDY_SNAPSHOT_ID = "paper-study-2026-world-cup";
+export const TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS = 25;
+
+// Human-approved protocol identity. These constants intentionally describe the
+// immutable registration event; observation counts remain evidence-derived.
+export const FORWARD_PAPER_PROTOCOL_ID = PAPER_STUDY_PROTOCOL_VERSION;
+export const FORWARD_PAPER_REGISTERED_AT = PAPER_STUDY_REGISTERED_AT;
+export const FORWARD_PAPER_CONFIG_HASH = PAPER_STUDY_FROZEN_CONFIG_SHA256;
+
+export type ReplayStepId = "pre" | "goal" | "post";
+
+export type PublicBookPoint = {
+ offsetMs: number;
+ bestBid: number;
+ bestAsk: number;
+ spread: number;
+ available: true;
+};
+
+export type ReplayState = {
+ id: ReplayStepId;
+ label: string;
+ offsetMs: number;
+ observedAt: string;
+ consensusMoveFromBaselineBps: number;
+ bestBid: number;
+ bestAsk: number;
+ spread: number;
+ conclusionTitle: string;
+ conclusionBody: string;
+ decisionExplanation: string;
+};
+
+export type PublicDataPolicy = {
+ derivedOnly: true;
+ txlineProbabilityDisplay: "bucketed_movement_only";
+ txlineMovementBucketBps: typeof TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS;
+ txlineFixtureIdentifiersExposed: false;
+ credentialsRequired: false;
+ walletControlsExposed: false;
+};
+
+export type DecisionStage = {
+ id: "signal" | "evidence" | "pass" | "execution";
+ label: string;
+ detail: string;
+ status: "complete" | "passed" | "locked";
+ timingLabel: string;
+};
+
+export type EvidenceRow = {
+ replayStateId: ReplayStepId;
+ observedAt: string;
+ offsetLabel: string;
+ source: "Polymarket" | "TXLine" | "Samaritan";
+ observation: string;
+ bestAsk: number;
+ assessment: "Moved first" | "Pass" | "No trade";
+};
+
+export type AvailabilityGap = {
+ startedAt: string;
+ endedAt: string;
+ durationMs: number;
+};
+
+export type MatchroomSnapshot = {
+ schemaVersion: 2;
+ snapshotId: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ caseId: string;
+ casebookCaseCount: number;
+ generatedAt: string;
+ mode: "captured_replay";
+ executionMode: "paper";
+ realMoneyGate: "closed";
+ tradeable: false;
+ match: {
+ fixtureRef: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ eventSlug: string;
+ competition: "World Cup";
+ stage: "Captured fixture";
+ kickoffUtc: string;
+ originalMatchDate: string;
+ home: { name: string; code: string };
+ away: { name: string; code: string };
+ scoreAtCursor: { home: number; away: number };
+ goalOrdinal: number;
+ clockSeconds: number;
+ clockLabel: string;
+ };
+ market: {
+ family: "match_result";
+ outcome: string;
+ label: string;
+ period: "90 minutes plus stoppage time";
+ mappingStatus: "research_only";
+ };
+ replay: {
+ firstSeenAt: string;
+ firstSeenLatencyMs: number;
+ firstMaterialMoveLatencyMs: number;
+ preTriggerMarketMoveBps: number;
+ activeStateId: "goal";
+ states: ReplayState[];
+ chart: PublicBookPoint[];
+ availabilityGaps: AvailabilityGap[];
+ };
+ decision: {
+ disposition: "no_trade";
+ semanticStatus: "disciplined_pass";
+ label: "No trade";
+ primaryReason: "Market moved before signal";
+ explanation: string;
+ capitalMovedMicros: 0;
+ ordersPlaced: 0;
+ walletAccessed: false;
+ stages: DecisionStage[];
+ };
+ evidence: EvidenceRow[];
+ proof: {
+ captureStatus: "verified";
+ identityParity: true;
+ identityHash: string;
+ headHash: string;
+ canonicalEvents: number;
+ replayMode: "capture-order-per-source";
+ feedOutageCount: number;
+ feedDowntimeMs: number;
+ maximumFeedDowntimeMs: number;
+ gateCases: number;
+ movedBeforeTxlineCases: number;
+ noMaterialRepriceCases: number;
+ cleanStaleWindows: 0;
+ corpusCommitment: string;
+ corpusAssurance: "local_file_sha256_not_capture_manifest_membership";
+ };
+ publicDataPolicy: PublicDataPolicy;
+};
+
+export type DashboardApiResponse = {
+ data: MatchroomSnapshot;
+};
+
+export type CommandFixturePhase = "scheduled" | "running" | "complete" | "failed" | "unknown";
+
+export type CommandCaptureTerminalEvidence = {
+ windowStartUtc: string;
+ windowEndUtc: string;
+ synchronizedStartUtc: string;
+ synchronizedEndUtc: string;
+ streamCount: 3;
+};
+
+export type CommandFeedState = {
+ id: "txline" | "polymarket" | "decision_ledger" | "replay_proof";
+ label: string;
+ status: CommandFixturePhase | "initialized" | "verified";
+ statusLabel: string;
+ detail: string;
+};
+
+export type CommandFixture = {
+ captureId: string;
+ home: { name: string; code: string };
+ away: { name: string; code: string };
+ kickoffUtc: string;
+ captureStartUtc: string;
+ captureEndUtc: string;
+ signalCutoffUtc: string;
+ eventSlug: string;
+ phase: CommandFixturePhase;
+ statusLabel: string;
+ statusDetail: string;
+ statusSource: "analysis_manifest" | "supervisor_status" | "reviewed_config" | "none";
+ statusUpdatedAt: string | null;
+ terminalEvidence: CommandCaptureTerminalEvidence | null;
+ identityStatus: "exact_match_confirmed" | "historical_reviewed_config";
+ captureOnly: true;
+ tradeable: false;
+};
+
+export type CommandCase = {
+ caseId: string;
+ matchroomId: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ fixtureRef: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ fixtureLabel: string;
+ home: MatchroomSnapshot["match"]["home"];
+ away: MatchroomSnapshot["match"]["away"];
+ occurredAt: string;
+ marketLabel: string;
+ marketOutcomeLabel: string;
+ candidateLabel: "Live-lane gate readout";
+ disposition: "no_trade";
+ dispositionLabel: string;
+ reason: string;
+ evidenceStatus: "verified_replay";
+ preTriggerMarketMoveBps: number;
+ consensusMoveFromBaselineBps: number;
+ bestAsk: number;
+ canonicalEvents: number;
+ capitalMovedMicros: 0;
+ ordersPlaced: 0;
+ walletAccessed: false;
+};
+
+export type CommandSnapshot = {
+ schemaVersion: 3;
+ snapshotId: typeof COMMAND_SNAPSHOT_ID;
+ generatedAt: string;
+ mode: "offline_artifact";
+ executionMode: "paper";
+ realMoneyGate: "closed";
+ tradeable: false;
+ system: {
+ posture: "standing_by" | "capture_window" | "awaiting_verification";
+ label: string;
+ detail: string;
+ feeds: CommandFeedState[];
+ };
+ featuredCase: CommandCase & {
+ scoreLabel: string;
+ scoreAtCursor: MatchroomSnapshot["match"]["scoreAtCursor"];
+ clockSeconds: number;
+ clockLabel: string;
+ conclusion: string;
+ identityParity: true;
+ chart: PublicBookPoint[];
+ };
+ fixtureSchedule: CommandFixture[];
+ recentCases: CommandCase[];
+ additionalCaseState: {
+ status: "waiting_for_eligible_capture";
+ label: "Registered v2 awaits fresh eligible evidence";
+ detail: string;
+ };
+ study: {
+ protocolVersion: typeof FORWARD_PAPER_PROTOCOL_ID;
+ protocolStatus: "registered";
+ configHash: typeof FORWARD_PAPER_CONFIG_HASH;
+ registeredAt: typeof FORWARD_PAPER_REGISTERED_AT;
+ status: "active_forward_paper";
+ statusLabel: "V2 registered";
+ observationStatus: "awaiting_fresh_evidence" | "collecting_forward_evidence";
+ qualifyingCounts: StudyCounts;
+ requiredFilledMatches: number;
+ requiredFills: number;
+ bountyStatus: "exploratory";
+ stoppingRuleMet: boolean;
+ realMoneyGate: "closed";
+ reason: string;
+ historicalV1: {
+ protocolVersion: string;
+ protocolStatus: "invalidated_suspended";
+ configHash: string;
+ startedAt: string;
+ zeroObservationAudit: true;
+ };
+ };
+ proof: {
+ replayIdentityParity: true;
+ replayIdentityHash: string;
+ canonicalEvents: number;
+ bountyLedgerValid: true;
+ bountyLedgerRows: number;
+ longRunLedgerValid: true;
+ longRunLedgerRows: number;
+ evidenceFixtures: number;
+ pairedBookReplays: number;
+ signalResearchOnly: number;
+ };
+ sourceFreshness: {
+ paperReportGeneratedAt: string;
+ fixtureUniverseGeneratedAt: string;
+ replayGeneratedAt: string;
+ };
+ publicDataPolicy: PublicDataPolicy;
+};
+
+export type CommandApiResponse = {
+ data: CommandSnapshot;
+};
+
+export type CasebookCaseSummary = {
+ caseId: string;
+ matchroomId: typeof SPAIN_BELGIUM_MATCHROOM_ID | null;
+ selectedExemplar: boolean;
+ goalOrdinal: number;
+ goalClockSeconds: number;
+ occurredAt: string;
+ fixtureRef: typeof SPAIN_BELGIUM_MATCHROOM_ID;
+ fixtureLabel: string;
+ homeCode: string;
+ awayCode: string;
+ marketFamily: "Match result" | "Full-time total";
+ marketLabel: string;
+ lineMilli: number | null;
+ classification: "polymarket_moved_before_txline" | "no_material_reprice_in_window";
+ detector: "STALE_QUOTE_FEASIBILITY";
+ disposition: string;
+ executionOutcome: string;
+ evidenceLane: "Research only";
+ source: "Captured replay";
+ verificationStatus: "Internally reconciled";
+ reason: string;
+ preTriggerMarketMoveBps: number;
+};
+
+export type CasebookSnapshot = {
+ schemaVersion: 3;
+ snapshotId: typeof CASEBOOK_SNAPSHOT_ID;
+ generatedAt: string;
+ mode: "offline_artifact";
+ executionMode: "paper";
+ realMoneyGate: "closed";
+ tradeable: false;
+ statistics: {
+ totalCases: number;
+ noTradeCases: number;
+ executedCases: number;
+ reconciledCases: number;
+ capitalMovedMicros: 0;
+ };
+ corpus: {
+ unit: "goal_market_feasibility_observation";
+ coverage: "all_reported_goal_market_cases";
+ captureReplays: 1;
+ fixtureCount: 1;
+ goalEvents: number;
+ marketEventCases: number;
+ movedBeforeTxlineCases: number;
+ noMaterialRepriceCases: number;
+ cleanStaleWindows: 0;
+ commitment: string;
+ assurance: "local_file_sha256_not_capture_manifest_membership";
+ selectedExemplar: {
+ caseId: string;
+ policy: "earliest_pretrigger_match_result_then_largest_pretrigger_ask_move";
+ detail: string;
+ };
+ };
+ filterOptions: {
+ fixtures: string[];
+ marketFamilies: string[];
+ detectors: string[];
+ dispositions: string[];
+ executionOutcomes: string[];
+ evidenceLanes: string[];
+ sources: string[];
+ };
+ cases: CasebookCaseSummary[];
+ selectedCase: {
+ summary: CasebookCaseSummary;
+ match: MatchroomSnapshot["match"];
+ decision: MatchroomSnapshot["decision"];
+ lifecycle: DecisionStage[];
+ evidenceReadout: {
+ consensusMoveFromBaselineBps: number;
+ bestBid: number;
+ bestAsk: number;
+ spread: number;
+ preTriggerMarketMoveBps: number;
+ movementConclusion: "Polymarket moved before TXLine";
+ };
+ analysis: {
+ thesisStatus: "not_requested";
+ thesisReason: string;
+ invalidation: string;
+ costStatus: "not_applicable";
+ costMicros: 0;
+ costReason: string;
+ };
+ evidence: EvidenceRow[];
+ proof: MatchroomSnapshot["proof"];
+ };
+ nextEvidence: {
+ status: "waiting_for_verified_capture";
+ label: string;
+ detail: string;
+ };
+ publicDataPolicy: PublicDataPolicy;
+};
+
+export type CasebookApiResponse = {
+ data: CasebookSnapshot;
+};
+
+export type StudyCounts = {
+ matches: number;
+ signals: number;
+ filledMatches: number;
+ fills: number;
+ settledFills: number;
+};
+
+export type StudyMatchRow = {
+ fixtureRef: string;
+ kickoffUtc: string;
+ selectedLine: number;
+ signals: number;
+ fills: number;
+ fillRate: number;
+ meanHalfSpreadBps: number | null;
+ meanSlippageBps: number | null;
+ grossClvBps: number | null;
+ netClvBps: number | null;
+ settlementPnlMicroUsd: number | null;
+ netReturnBps: number | null;
+};
+
+export type StudyEndpoints = {
+ meanNetClvBps: number;
+ netClvInterval: { iterations: number; seed: number; matches: number; signals: number; low: number; median: number; high: number };
+ meanSettlementPnlMicroUsd: number;
+ settlementPnlInterval: { iterations: number; seed: number; matches: number; signals: number; low: number; median: number; high: number };
+ noTradeBaselineClvBps: 0;
+ randomDirectionControlClvBps: number;
+ fractionSettledMatchesNetPositive: number;
+};
+
+export type StudyGuardrails = {
+ fillRate: number;
+ fillRatePassed: boolean;
+ meanSlippageBps: number | null;
+ slippagePassed: boolean;
+ maxDrawdownMicroUsd: number;
+ drawdownPassed: boolean;
+ selectedDepthComplete: boolean;
+ closeMarksComplete: boolean;
+ settlementComplete: boolean;
+};
+
+export type CorrectedHistoricalCandidate = {
+ schemaVersion: 4;
+ generatedAt: string;
+ protocolId: "historical-gate-causal-economic-v4-2026-07-14";
+ configurationHash: string;
+ status: "historical_signal_candidate_for_forward_paper_review";
+ sourceRegistrationAtGeneration: "engineering_candidate_unregistered";
+ activeStudyAtGeneration: false;
+ detector: "CONSENSUS_MOVE";
+ marketFamily: "Full-time totals";
+ trainingNormalizedCases: 135;
+ heldoutNormalizedCases: 38;
+ heldoutFixtures: 18;
+ costProxyBps: 100;
+ meanNetAfterCostProxyBps: 132.7;
+ matchClustered95Bps: {
+ iterations: 10_000;
+ cluster: "fixture";
+ low: 14.3;
+ high: 243.9;
+ };
+ evidenceClass: "historical_sampled_price_signal_research";
+ executionEvidence: "not_established_no_historical_bid_ask_or_depth";
+ executable: false;
+ claimBoundary: "Historical sampled-price evidence justified review only; the separate v2 registration does not turn it into alpha, fill proof, profitability, or permission for real money.";
+};
+
+export type SyntheticProofReceipt = {
+ label: "Synthetic full-lifecycle proving fixture";
+ path: "/artifacts/dashboard/synthetic-decision-receipt.json";
+ lifecycleStatus: "filled_settled";
+ offlineVerified: true;
+ performanceUse: "excluded_synthetic";
+ externalCalls: 0;
+ solanaAnchorStatus: "not_submitted";
+ explanation: "Closed-world production-component demo; separate from historical evidence and excluded from every performance claim.";
+};
+
+export type StudySnapshot = {
+ schemaVersion: 3;
+ snapshotId: typeof STUDY_SNAPSHOT_ID;
+ generatedAt: string;
+ mode: "offline_artifact";
+ executionMode: "paper";
+ realMoneyGate: "closed";
+ tradeable: false;
+ protocol: {
+ version: typeof FORWARD_PAPER_PROTOCOL_ID;
+ status: "registered";
+ activity: "active_forward_paper";
+ active: true;
+ registeredAt: typeof FORWARD_PAPER_REGISTERED_AT;
+ configHash: typeof FORWARD_PAPER_CONFIG_HASH;
+ realMoneyGate: "closed";
+ observationStatus: "awaiting_fresh_evidence" | "collecting_forward_evidence";
+ evidencePolicy: "fresh_forward_only";
+ qualifyingCounts: StudyCounts;
+ candidate: {
+ detector: "CONSENSUS_MOVE";
+ marketFamily: "Full-time totals only";
+ moveAbsZ: number;
+ cusumThresholdBps: number;
+ minimumGapBps: number;
+ minimumUpdates: number;
+ selector: "Closest to even";
+ minimumCoveragePoints: number;
+ maximumDistanceFromEven: number;
+ };
+ evaluation: {
+ unitOfAnalysis: "match";
+ primaryEndpoint: "Executable CLV net of measured costs";
+ minimumFilledMatches: number;
+ minimumFills: number;
+ targetMatches: 30;
+ bootstrapIterations: number;
+ bootstrapSeed: number;
+ randomDirectionControl: "Seeded matched-cost sign flip";
+ };
+ risk: {
+ bankrollMicroUsd: number;
+ perTradeStakeMicroUsd: number;
+ aggregateExposureMicroUsd: number;
+ drawdownStopMicroUsd: number;
+ };
+ guardrailThresholds: {
+ minimumFillRate: number;
+ maximumMeanSlippageBps: number;
+ maximumDrawdownMicroUsd: number;
+ selectedDepthRequired: true;
+ };
+ };
+ historicalV1: {
+ protocolVersion: string;
+ status: "invalidated_suspended";
+ active: false;
+ invalidatedBeforeObservations: true;
+ configHash: string;
+ startedAt: string;
+ lanes: {
+ bounty: {
+ label: "Preserved invalidated v1 bounty ledger";
+ sourceStatus: "exploratory";
+ statusLabel: "V1 invalidated";
+ reason: string;
+ counts: StudyCounts;
+ chain: { valid: true; rows: number; headHash: string };
+ canSatisfyGate: false;
+ };
+ longRun: {
+ label: "Preserved invalidated v1 long-run ledger";
+ sourceStatus: "sealed";
+ statusLabel: "V1 invalidated";
+ reason: string;
+ counts: StudyCounts;
+ stoppingRuleMet: false;
+ chain: { valid: true; rows: number; headHash: string };
+ canSatisfyGate: false;
+ };
+ };
+ results: {
+ visibility: "sealed";
+ rows: null;
+ endpoints: null;
+ guardrails: null;
+ };
+ };
+ results: {
+ visibility: "sealed";
+ rows: null;
+ endpoints: null;
+ guardrails: null;
+ } | {
+ visibility: "open";
+ rows: StudyMatchRow[];
+ endpoints: StudyEndpoints | null;
+ guardrails: StudyGuardrails;
+ };
+ correctedHistoricalCandidate: CorrectedHistoricalCandidate;
+ syntheticProof: SyntheticProofReceipt;
+ fixtureUniverse: {
+ generatedAt: string;
+ evidenceFixtures: number;
+ pairedBookReplays: number;
+ executableBookReplays: number;
+ signalResearchOnly: number;
+ longRunEligible: number;
+ };
+ decisionRules: {
+ accept: string[];
+ reject: string[];
+ inconclusive: string[];
+ };
+ publicDataPolicy: PublicDataPolicy;
+};
+
+export type StudyApiResponse = {
+ data: StudySnapshot;
+};
diff --git a/src/dash/server.ts b/src/dash/server.ts
new file mode 100644
index 0000000..4723a44
--- /dev/null
+++ b/src/dash/server.ts
@@ -0,0 +1,93 @@
+import { createServer } from "node:http";
+import { readFile, stat } from "node:fs/promises";
+import { extname, resolve, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+import { handleDashboardApi, writeApiResult } from "./api.js";
+
+const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
+const staticRoot = resolve(repoRoot, "apps/dashboard/dist");
+const port = Number(process.env.DASHBOARD_PORT ?? process.env.PORT ?? 4173);
+const host = process.env.DASHBOARD_HOST ?? process.env.HOST ?? "127.0.0.1";
+
+if (!Number.isInteger(port) || port < 1 || port > 65_535) {
+ throw new Error("DASHBOARD_PORT/PORT must be an integer between 1 and 65535");
+}
+if (host.trim().length === 0 || /[\s/]/.test(host)) {
+ throw new Error("DASHBOARD_HOST/HOST is invalid");
+}
+
+const contentTypes: Record = {
+ ".css": "text/css; charset=utf-8",
+ ".html": "text/html; charset=utf-8",
+ ".js": "text/javascript; charset=utf-8",
+ ".json": "application/json; charset=utf-8",
+ ".svg": "image/svg+xml",
+ ".woff2": "font/woff2"
+};
+
+const securityHeaders: Record = {
+ "content-security-policy": "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'",
+ "cross-origin-opener-policy": "same-origin",
+ "permissions-policy": "camera=(), geolocation=(), microphone=(), payment=(), usb=()",
+ "referrer-policy": "no-referrer",
+ "x-content-type-options": "nosniff",
+ "x-frame-options": "DENY"
+};
+
+async function staticFilePath(pathname: string): Promise {
+ const decoded = decodeURIComponent(pathname);
+ const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
+ const candidate = resolve(staticRoot, relative);
+ if (candidate !== staticRoot && !candidate.startsWith(`${staticRoot}${sep}`)) return null;
+ try {
+ if ((await stat(candidate)).isFile()) return candidate;
+ } catch {
+ // SPA routes fall through to index.html.
+ }
+ if (extname(relative) !== "") return null;
+ const index = resolve(staticRoot, "index.html");
+ try {
+ return (await stat(index)).isFile() ? index : null;
+ } catch {
+ return null;
+ }
+}
+
+const server = createServer(async (request, response) => {
+ try {
+ for (const [name, value] of Object.entries(securityHeaders)) response.setHeader(name, value);
+ if (request.method !== "GET" && request.method !== "HEAD") {
+ response.writeHead(405, { allow: "GET, HEAD", "cache-control": "no-store", "content-type": "application/json; charset=utf-8" });
+ response.end(JSON.stringify({ error: "method_not_allowed" }));
+ return;
+ }
+ const url = new URL(request.url ?? "/", "http://localhost");
+ const apiResult = await handleDashboardApi(url.pathname, repoRoot, { method: request.method });
+ if (apiResult) {
+ writeApiResult(response, apiResult, request.method === "HEAD");
+ return;
+ }
+ const path = await staticFilePath(url.pathname);
+ if (!path) {
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
+ response.end("Not found");
+ return;
+ }
+ const body = await readFile(path);
+ response.writeHead(200, {
+ "cache-control": path.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
+ "content-type": contentTypes[extname(path)] ?? "application/octet-stream",
+ "x-content-type-options": "nosniff"
+ });
+ response.end(request.method === "HEAD" ? undefined : body);
+ } catch (error) {
+ console.error(error);
+ response.writeHead(500, { "content-type": "application/json; charset=utf-8" });
+ response.end(JSON.stringify({ error: "internal_error" }));
+ }
+});
+
+server.listen(port, host, () => {
+ const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
+ console.log(`Samaritan dashboard listening at http://${displayHost}:${port} (bound ${host})`);
+});
diff --git a/src/dash/study-projection.ts b/src/dash/study-projection.ts
new file mode 100644
index 0000000..60f3fa9
--- /dev/null
+++ b/src/dash/study-projection.ts
@@ -0,0 +1,470 @@
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { z } from "zod";
+import {
+ PAPER_STUDY_DETECTOR_CONFIG,
+ PAPER_STUDY_TOTAL_SELECTOR_CONFIG
+} from "../config/paper-study.js";
+import { PAPER_STUDY_EVALUATION_CANDIDATE } from "../metrics/paper-study.js";
+import { APPROVED_PAPER_RISK_CONFIG } from "../risk/paper.js";
+import {
+ FORWARD_PAPER_CONFIG_HASH,
+ FORWARD_PAPER_PROTOCOL_ID,
+ FORWARD_PAPER_REGISTERED_AT,
+ STUDY_SNAPSHOT_ID,
+ TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ type CorrectedHistoricalCandidate,
+ type StudyApiResponse,
+ type StudySnapshot
+} from "./public-contract.js";
+
+const countsSchema = z.object({
+ matches: z.number().int().nonnegative(),
+ signals: z.number().int().nonnegative(),
+ filledMatches: z.number().int().nonnegative(),
+ fills: z.number().int().nonnegative(),
+ settledFills: z.number().int().nonnegative()
+});
+
+const intervalSchema = z.object({
+ iterations: z.number().int().positive(),
+ seed: z.number().int(),
+ matches: z.number().int().positive(),
+ signals: z.number().int().positive(),
+ low: z.number().finite(),
+ median: z.number().finite(),
+ high: z.number().finite()
+});
+
+const endpointsSchema = z.object({
+ meanNetClvBps: z.number().finite(),
+ netClvInterval: intervalSchema,
+ meanSettlementPnlMicroUsd: z.number().int(),
+ settlementPnlInterval: intervalSchema,
+ noTradeBaselineClvBps: z.literal(0),
+ randomDirectionControlClvBps: z.number().finite(),
+ fractionSettledMatchesNetPositive: z.number().min(0).max(1)
+});
+
+const guardrailsSchema = z.object({
+ fillRate: z.number().min(0).max(1),
+ fillRatePassed: z.boolean(),
+ meanSlippageBps: z.number().finite().nonnegative().nullable(),
+ slippagePassed: z.boolean(),
+ maxDrawdownMicroUsd: z.number().int().nonnegative(),
+ drawdownPassed: z.boolean(),
+ selectedDepthComplete: z.boolean(),
+ closeMarksComplete: z.boolean(),
+ settlementComplete: z.boolean()
+});
+
+const rowSchema = z.object({
+ fixtureId: z.string().min(1),
+ kickoffTsMs: z.number().int().nonnegative(),
+ selectedLineMilli: z.number().int(),
+ signals: z.number().int().nonnegative(),
+ fills: z.number().int().nonnegative(),
+ fillRate: z.number().min(0).max(1),
+ meanHalfSpreadBps: z.number().finite().nonnegative().nullable(),
+ meanSlippageBps: z.number().finite().nonnegative().nullable(),
+ grossClvBps: z.number().finite().nullable(),
+ netClvBps: z.number().finite().nullable(),
+ settlementPnlMicroUsd: z.number().int().nullable(),
+ netReturnBps: z.number().finite().nullable()
+});
+
+const reportSchema = z.object({
+ lane: z.enum(["bounty", "long_run"]),
+ counts: countsSchema,
+ stoppingRuleMet: z.boolean(),
+ status: z.enum(["sealed", "exploratory", "accept", "reject", "inconclusive"]),
+ reason: z.string().min(1),
+ rows: z.array(rowSchema).nullable(),
+ endpoints: endpointsSchema.nullable(),
+ guardrails: guardrailsSchema.nullable()
+});
+
+const initializationSchema = z.object({
+ configHash: z.string().regex(/^[a-f0-9]{64}$/),
+ protocolVersion: z.string().min(1),
+ protocolStatus: z.literal("registered").optional(),
+ realMoneyGate: z.literal("closed"),
+ startedAtTsMs: z.number().int().nonnegative(),
+ startedAt: z.string().datetime(),
+ registration: z.object({
+ protocolId: z.literal(FORWARD_PAPER_PROTOCOL_ID),
+ status: z.literal("registered"),
+ registeredBy: z.literal("Deborah"),
+ registeredAt: z.literal(FORWARD_PAPER_REGISTERED_AT),
+ scope: z.literal("forward_paper_only"),
+ realMoneyGate: z.literal("closed")
+ }).strict().optional(),
+ frozenConfig: z.object({
+ detector: z.object({
+ consensusCusumThreshold: z.number().finite().positive(),
+ consensusMinimumRawGap: z.number().finite().positive(),
+ consensusMinimumUpdates: z.number().int().positive(),
+ consensusMoveAbsZ: z.number().finite().positive()
+ }),
+ evaluation: z.object({
+ bootstrapIterations: z.number().int().positive(),
+ bootstrapSeed: z.number().int(),
+ maximumDrawdownMicroUsd: z.number().int().positive(),
+ maximumMeanSlippageBps: z.number().finite().positive(),
+ minimumFilledMatches: z.number().int().positive(),
+ minimumFillRate: z.number().min(0).max(1),
+ minimumFills: z.number().int().positive()
+ }),
+ risk: z.object({
+ aggregateExposureMicroUsd: z.number().int().positive(),
+ bankrollMicroUsd: z.number().int().positive(),
+ drawdownStopMicroUsd: z.number().int().positive(),
+ perTradeStakeMicroUsd: z.number().int().positive(),
+ realMoneyGate: z.literal("closed")
+ }),
+ selector: z.object({
+ maximumDistanceFromEven: z.number().min(0).max(0.5),
+ minimumCoveragePoints: z.number().int().positive()
+ })
+ })
+}).passthrough();
+
+const laneSchema = z.object({
+ initialization: initializationSchema,
+ chain: z.object({ valid: z.literal(true), rows: z.number().int().positive(), headHash: z.string().regex(/^[a-f0-9]{64}$/) }),
+ report: reportSchema
+});
+
+const artifactSchema = z.object({
+ generatedAt: z.string().datetime(),
+ protocolVersion: z.string().min(1),
+ configHash: z.string().regex(/^[a-f0-9]{64}$/),
+ realMoneyGate: z.literal("closed"),
+ fixtureUniverseGeneratedAt: z.string().datetime(),
+ lanes: z.object({ bounty: laneSchema, longRun: laneSchema })
+});
+
+const universeSchema = z.object({
+ generatedAt: z.string().datetime(),
+ summary: z.object({
+ fixtures: z.number().int().nonnegative(),
+ pairedBookReplays: z.number().int().nonnegative(),
+ executableBookReplays: z.number().int().nonnegative(),
+ signalResearchOnly: z.number().int().nonnegative(),
+ longRunEligible: z.number().int().nonnegative()
+ })
+});
+
+const correctedHistoricalCandidateSourceSchema = z.object({
+ schemaVersion: z.literal(4),
+ generatedAt: z.string().datetime(),
+ protocolId: z.literal("historical-gate-causal-economic-v4-2026-07-14"),
+ configurationHash: z.string().regex(/^[a-f0-9]{64}$/),
+ status: z.literal("research_evidence_only"),
+ auditStatus: z.literal("causal_selector_and_economic_case_normalization_repaired_pending_human_review"),
+ realMoneyGate: z.literal("closed"),
+ configuration: z.object({
+ costProxyProbability: z.literal(0.01),
+ bootstrap: z.object({
+ iterations: z.literal(10_000),
+ seed: z.literal(20_260_714),
+ cluster: z.literal("fixture")
+ }).passthrough()
+ }).passthrough(),
+ forwardPaperCandidate: z.object({
+ detector: z.literal("CONSENSUS_MOVE"),
+ marketFamily: z.literal("total_goals"),
+ configurationSelection: z.literal("training_only_before_heldout_totals_review"),
+ trainingNormalizedCases: z.literal(135),
+ heldoutNormalizedCases: z.literal(38),
+ heldoutFixtures: z.literal(18),
+ meanNetAfterCostProxy: z.number().finite().positive(),
+ matchClusteredNetClv95: z.object({
+ iterations: z.literal(10_000),
+ seed: z.literal(20_260_714),
+ fixtures: z.literal(18),
+ signals: z.literal(38),
+ low: z.number().finite().positive(),
+ median: z.number().finite().positive(),
+ high: z.number().finite().positive()
+ }).strict(),
+ status: z.literal("historical_signal_candidate_for_forward_paper_review"),
+ executionEvidence: z.literal("not_established_sampled_prices_only"),
+ registration: z.literal("engineering_candidate_unregistered")
+ }).strict()
+}).passthrough();
+
+function validCounts(counts: z.infer): boolean {
+ return counts.filledMatches <= counts.matches && counts.fills <= counts.signals && counts.settledFills <= counts.fills;
+}
+
+function probabilityToOneDecimalBps(value: number): number {
+ return Math.round(value * 100_000) / 10;
+}
+
+function buildCorrectedHistoricalCandidate(
+ source: z.infer
+): CorrectedHistoricalCandidate {
+ const candidate = source.forwardPaperCandidate;
+ const interval = candidate.matchClusteredNetClv95;
+ const meanNetAfterCostProxyBps = probabilityToOneDecimalBps(candidate.meanNetAfterCostProxy);
+ const low = probabilityToOneDecimalBps(interval.low);
+ const high = probabilityToOneDecimalBps(interval.high);
+ if (meanNetAfterCostProxyBps !== 132.7 || low !== 14.3 || high !== 243.9 || interval.low >= interval.high) {
+ throw new Error("Study projection failed closed: corrected v4 aggregate evidence changed");
+ }
+ return {
+ schemaVersion: 4,
+ generatedAt: source.generatedAt,
+ protocolId: source.protocolId,
+ configurationHash: source.configurationHash,
+ status: candidate.status,
+ sourceRegistrationAtGeneration: candidate.registration,
+ activeStudyAtGeneration: false,
+ detector: candidate.detector,
+ marketFamily: "Full-time totals",
+ trainingNormalizedCases: candidate.trainingNormalizedCases,
+ heldoutNormalizedCases: candidate.heldoutNormalizedCases,
+ heldoutFixtures: candidate.heldoutFixtures,
+ costProxyBps: 100,
+ meanNetAfterCostProxyBps,
+ matchClustered95Bps: {
+ iterations: interval.iterations,
+ cluster: "fixture",
+ low,
+ high
+ },
+ evidenceClass: "historical_sampled_price_signal_research",
+ executionEvidence: "not_established_no_historical_bid_ask_or_depth",
+ executable: false,
+ claimBoundary: "Historical sampled-price evidence justified review only; the separate v2 registration does not turn it into alpha, fill proof, profitability, or permission for real money."
+ };
+}
+
+export async function buildStudySnapshot(repoRoot: string): Promise {
+ const [artifactValue, universeValue, registeredArtifactValue, registeredUniverseValue, correctedCandidateValue] = await Promise.all([
+ readFile(resolve(repoRoot, "data/paper/reports/current.json"), "utf8").then(JSON.parse),
+ readFile(resolve(repoRoot, "data/research/paper-fixture-universe.json"), "utf8").then(JSON.parse),
+ readFile(resolve(repoRoot, "data/paper/v2/reports/current.json"), "utf8").then(JSON.parse),
+ readFile(resolve(repoRoot, "data/paper/v2/fixture-universe.json"), "utf8").then(JSON.parse),
+ readFile(resolve(repoRoot, "data/research/historical-gate-study-causal-economic-v4.json"), "utf8").then(JSON.parse)
+ ]);
+ const artifact = artifactSchema.parse(artifactValue);
+ const universe = universeSchema.parse(universeValue);
+ const registeredArtifact = artifactSchema.parse(registeredArtifactValue);
+ const registeredUniverse = universeSchema.parse(registeredUniverseValue);
+ const correctedCandidateSource = correctedHistoricalCandidateSourceSchema.parse(correctedCandidateValue);
+ const bounty = artifact.lanes.bounty;
+ const longRun = artifact.lanes.longRun;
+ if (artifact.protocolVersion !== "paper-study-v1-2026-07-12") {
+ throw new Error("Study projection failed closed: preserved v1 audit identity changed");
+ }
+ if (
+ bounty.report.lane !== "bounty" ||
+ bounty.report.status !== "exploratory" ||
+ longRun.report.lane !== "long_run" ||
+ longRun.report.status === "exploratory" ||
+ artifact.configHash !== bounty.initialization.configHash ||
+ artifact.configHash !== longRun.initialization.configHash ||
+ artifact.protocolVersion !== longRun.initialization.protocolVersion ||
+ artifact.fixtureUniverseGeneratedAt !== universe.generatedAt ||
+ !validCounts(bounty.report.counts) ||
+ !validCounts(longRun.report.counts)
+ ) {
+ throw new Error("Study projection failed closed: paper artifact identity or counts changed");
+ }
+ if (
+ Object.values(bounty.report.counts).some((value) => value !== 0) ||
+ Object.values(longRun.report.counts).some((value) => value !== 0)
+ ) {
+ throw new Error("Study projection failed closed: invalidated v1 ledger contains observations");
+ }
+ if (
+ longRun.report.status !== "sealed" ||
+ longRun.report.stoppingRuleMet ||
+ longRun.report.rows !== null ||
+ longRun.report.endpoints !== null ||
+ longRun.report.guardrails !== null
+ ) {
+ throw new Error("Study projection failed closed: invalidated v1 long-run evidence is not sealed");
+ }
+
+ const registeredBounty = registeredArtifact.lanes.bounty;
+ const registeredLongRun = registeredArtifact.lanes.longRun;
+ const registeredAtTsMs = Date.parse(FORWARD_PAPER_REGISTERED_AT);
+ if (
+ registeredArtifact.protocolVersion !== FORWARD_PAPER_PROTOCOL_ID ||
+ registeredArtifact.configHash !== FORWARD_PAPER_CONFIG_HASH ||
+ registeredArtifact.fixtureUniverseGeneratedAt !== registeredUniverse.generatedAt ||
+ registeredBounty.initialization.protocolVersion !== FORWARD_PAPER_PROTOCOL_ID ||
+ registeredLongRun.initialization.protocolVersion !== FORWARD_PAPER_PROTOCOL_ID ||
+ registeredBounty.initialization.protocolStatus !== "registered" ||
+ registeredLongRun.initialization.protocolStatus !== "registered" ||
+ registeredBounty.initialization.registration?.registeredAt !== FORWARD_PAPER_REGISTERED_AT ||
+ registeredLongRun.initialization.registration?.registeredAt !== FORWARD_PAPER_REGISTERED_AT ||
+ registeredBounty.initialization.configHash !== FORWARD_PAPER_CONFIG_HASH ||
+ registeredLongRun.initialization.configHash !== FORWARD_PAPER_CONFIG_HASH ||
+ registeredBounty.initialization.startedAtTsMs < registeredAtTsMs ||
+ registeredLongRun.initialization.startedAtTsMs < registeredAtTsMs ||
+ registeredBounty.initialization.startedAt !== new Date(registeredBounty.initialization.startedAtTsMs).toISOString() ||
+ registeredLongRun.initialization.startedAt !== new Date(registeredLongRun.initialization.startedAtTsMs).toISOString() ||
+ !validCounts(registeredBounty.report.counts) ||
+ !validCounts(registeredLongRun.report.counts)
+ ) {
+ throw new Error("Study projection failed closed: registered v2 report does not match the frozen forward protocol");
+ }
+ if (
+ registeredLongRun.report.status !== "sealed" ||
+ registeredLongRun.report.stoppingRuleMet ||
+ registeredLongRun.report.rows !== null ||
+ registeredLongRun.report.endpoints !== null ||
+ registeredLongRun.report.guardrails !== null
+ ) {
+ throw new Error("Study projection failed closed: v2 crossed its stopping gate without a refreshed public result contract");
+ }
+
+ const results: StudySnapshot["results"] = {
+ visibility: "sealed",
+ rows: null,
+ endpoints: null,
+ guardrails: null
+ };
+ const qualifyingCounts = registeredLongRun.report.counts;
+ const observationStatus = qualifyingCounts.signals === 0
+ ? "awaiting_fresh_evidence" as const
+ : "collecting_forward_evidence" as const;
+
+ return {
+ schemaVersion: 3,
+ snapshotId: STUDY_SNAPSHOT_ID,
+ generatedAt: registeredArtifact.generatedAt,
+ mode: "offline_artifact",
+ executionMode: "paper",
+ realMoneyGate: "closed",
+ tradeable: false,
+ protocol: {
+ version: FORWARD_PAPER_PROTOCOL_ID,
+ status: "registered",
+ activity: "active_forward_paper",
+ active: true,
+ registeredAt: FORWARD_PAPER_REGISTERED_AT,
+ configHash: FORWARD_PAPER_CONFIG_HASH,
+ realMoneyGate: "closed",
+ observationStatus,
+ evidencePolicy: "fresh_forward_only",
+ qualifyingCounts,
+ candidate: {
+ detector: "CONSENSUS_MOVE",
+ marketFamily: "Full-time totals only",
+ moveAbsZ: PAPER_STUDY_DETECTOR_CONFIG.consensusMoveAbsZ,
+ cusumThresholdBps: PAPER_STUDY_DETECTOR_CONFIG.consensusCusumThreshold * 10_000,
+ minimumGapBps: PAPER_STUDY_DETECTOR_CONFIG.consensusMinimumRawGap * 10_000,
+ minimumUpdates: PAPER_STUDY_DETECTOR_CONFIG.consensusMinimumUpdates,
+ selector: "Closest to even",
+ minimumCoveragePoints: PAPER_STUDY_TOTAL_SELECTOR_CONFIG.minimumCoveragePoints,
+ maximumDistanceFromEven: PAPER_STUDY_TOTAL_SELECTOR_CONFIG.maximumDistanceFromEven
+ },
+ evaluation: {
+ unitOfAnalysis: "match",
+ primaryEndpoint: "Executable CLV net of measured costs",
+ minimumFilledMatches: PAPER_STUDY_EVALUATION_CANDIDATE.minimumFilledMatches,
+ minimumFills: PAPER_STUDY_EVALUATION_CANDIDATE.minimumFills,
+ targetMatches: 30,
+ bootstrapIterations: PAPER_STUDY_EVALUATION_CANDIDATE.bootstrapIterations,
+ bootstrapSeed: PAPER_STUDY_EVALUATION_CANDIDATE.bootstrapSeed,
+ randomDirectionControl: "Seeded matched-cost sign flip"
+ },
+ risk: {
+ bankrollMicroUsd: APPROVED_PAPER_RISK_CONFIG.bankrollMicroUsd,
+ perTradeStakeMicroUsd: APPROVED_PAPER_RISK_CONFIG.perTradeStakeMicroUsd,
+ aggregateExposureMicroUsd: APPROVED_PAPER_RISK_CONFIG.aggregateExposureMicroUsd,
+ drawdownStopMicroUsd: APPROVED_PAPER_RISK_CONFIG.drawdownStopMicroUsd
+ },
+ guardrailThresholds: {
+ minimumFillRate: PAPER_STUDY_EVALUATION_CANDIDATE.minimumFillRate,
+ maximumMeanSlippageBps: PAPER_STUDY_EVALUATION_CANDIDATE.maximumMeanSlippageBps,
+ maximumDrawdownMicroUsd: PAPER_STUDY_EVALUATION_CANDIDATE.maximumDrawdownMicroUsd,
+ selectedDepthRequired: true
+ }
+ },
+ historicalV1: {
+ protocolVersion: artifact.protocolVersion,
+ status: "invalidated_suspended",
+ active: false,
+ invalidatedBeforeObservations: true,
+ configHash: artifact.configHash,
+ startedAt: longRun.initialization.startedAt,
+ lanes: {
+ bounty: {
+ label: "Preserved invalidated v1 bounty ledger",
+ sourceStatus: "exploratory",
+ statusLabel: "V1 invalidated",
+ reason: bounty.report.reason,
+ counts: bounty.report.counts,
+ chain: bounty.chain,
+ canSatisfyGate: false
+ },
+ longRun: {
+ label: "Preserved invalidated v1 long-run ledger",
+ sourceStatus: "sealed",
+ statusLabel: "V1 invalidated",
+ reason: longRun.report.reason,
+ counts: longRun.report.counts,
+ stoppingRuleMet: false,
+ chain: longRun.chain,
+ canSatisfyGate: false
+ }
+ },
+ results: { visibility: "sealed", rows: null, endpoints: null, guardrails: null }
+ },
+ results,
+ correctedHistoricalCandidate: buildCorrectedHistoricalCandidate(correctedCandidateSource),
+ syntheticProof: {
+ label: "Synthetic full-lifecycle proving fixture",
+ path: "/artifacts/dashboard/synthetic-decision-receipt.json",
+ lifecycleStatus: "filled_settled",
+ offlineVerified: true,
+ performanceUse: "excluded_synthetic",
+ externalCalls: 0,
+ solanaAnchorStatus: "not_submitted",
+ explanation: "Closed-world production-component demo; separate from historical evidence and excluded from every performance claim."
+ },
+ fixtureUniverse: {
+ generatedAt: registeredUniverse.generatedAt,
+ evidenceFixtures: registeredUniverse.summary.fixtures,
+ pairedBookReplays: registeredUniverse.summary.pairedBookReplays,
+ executableBookReplays: registeredUniverse.summary.executableBookReplays,
+ signalResearchOnly: registeredUniverse.summary.signalResearchOnly,
+ longRunEligible: registeredUniverse.summary.longRunEligible
+ },
+ decisionRules: {
+ accept: [
+ "Only fresh observations at or after v2 registration may count toward a decision.",
+ "Mean net executable CLV is positive and its match-clustered 95% CI lower bound is above zero.",
+ "Mean settlement P&L is positive and the strategy beats no-trade and random-direction baselines.",
+ "Every registered fill, slippage, drawdown, and depth guardrail holds."
+ ],
+ reject: [
+ "The executable CLV interval touches or falls below zero.",
+ "The strategy fails to beat no-trade or the seeded random-direction control.",
+ "Any registered guardrail fails materially."
+ ],
+ inconclusive: [
+ "The registered v2 study has not yet reached 20 filled matches and 40 fills from fresh evidence.",
+ "Capture-only evidence cannot authorize model spend, paper admission, or real money."
+ ]
+ },
+ publicDataPolicy: {
+ derivedOnly: true,
+ txlineProbabilityDisplay: "bucketed_movement_only",
+ txlineMovementBucketBps: TXLINE_PUBLIC_MOVEMENT_BUCKET_BPS,
+ txlineFixtureIdentifiersExposed: false,
+ credentialsRequired: false,
+ walletControlsExposed: false
+ }
+ };
+}
+
+export async function buildStudyDashboardResponse(repoRoot: string): Promise {
+ return { data: await buildStudySnapshot(repoRoot) };
+}
diff --git a/src/dash/txline-pulse-node.ts b/src/dash/txline-pulse-node.ts
new file mode 100644
index 0000000..9450657
--- /dev/null
+++ b/src/dash/txline-pulse-node.ts
@@ -0,0 +1,61 @@
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import {
+ buildTxlinePulse,
+ type TxlinePulseCredentials,
+ type TxlinePulseOptions,
+ type TxlinePulseResponse
+} from "./txline-pulse.js";
+
+type StoredMainnetToken = {
+ network?: unknown;
+ apiOrigin?: unknown;
+ serviceLevelId?: unknown;
+ jwt?: unknown;
+ apiToken?: unknown;
+};
+
+export type NodeTxlinePulseOptions = Omit & {
+ credentials?: TxlinePulseCredentials | null;
+};
+
+function envCredentials(): TxlinePulseCredentials | null {
+ const jwt = process.env.TXLINE_JWT;
+ const apiToken = process.env.TXLINE_API_TOKEN;
+ if (!jwt || !apiToken || process.env.TXLINE_SERVICE_LEVEL_ID !== "12") return null;
+ return { jwt, apiToken };
+}
+
+async function ignoredLocalCredentials(repoRoot: string): Promise {
+ try {
+ const parsed = JSON.parse(
+ await readFile(resolve(repoRoot, "phase0/.tokens/mainnet.json"), "utf8")
+ ) as StoredMainnetToken;
+ if (
+ parsed.network !== "mainnet" ||
+ parsed.apiOrigin !== "https://txline.txodds.com" ||
+ parsed.serviceLevelId !== 12 ||
+ typeof parsed.jwt !== "string" ||
+ typeof parsed.apiToken !== "string"
+ ) return null;
+ return { jwt: parsed.jwt, apiToken: parsed.apiToken };
+ } catch {
+ return null;
+ }
+}
+
+export async function buildNodeTxlinePulse(
+ repoRoot: string,
+ options: NodeTxlinePulseOptions = {}
+): Promise {
+ const credentials = Object.prototype.hasOwnProperty.call(options, "credentials")
+ ? options.credentials ?? null
+ : envCredentials() ?? await ignoredLocalCredentials(repoRoot);
+ return buildTxlinePulse({
+ credentials,
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
+ ...(options.now === undefined ? {} : { now: options.now }),
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
+ ...(options.maxResponseBytes === undefined ? {} : { maxResponseBytes: options.maxResponseBytes })
+ });
+}
diff --git a/src/dash/txline-pulse.ts b/src/dash/txline-pulse.ts
new file mode 100644
index 0000000..95c8971
--- /dev/null
+++ b/src/dash/txline-pulse.ts
@@ -0,0 +1,291 @@
+export const TXLINE_PULSE_API_PATH = "/api/v1/txline/pulse" as const;
+export const TXLINE_PULSE_ORIGIN = "https://txline.txodds.com" as const;
+export const TXLINE_PULSE_TIMEOUT_MS = 4_000;
+export const TXLINE_PULSE_MAX_RESPONSE_BYTES = 256 * 1_024;
+export const TXLINE_PULSE_CACHE_TTL_MS = 60_000;
+export const TXLINE_PULSE_STALE_RETENTION_MS = 5 * 60_000;
+
+export type TxlinePulseCredentials = {
+ jwt: string;
+ apiToken: string;
+};
+
+export type TxlinePulseResponse = {
+ network: "mainnet";
+ serviceLevel: "SL12";
+ checkedAt: string;
+ status: "connected" | "degraded";
+ latencyMsRounded: number | null;
+ aggregateFixtureCount: number | null;
+ freshnessClass: "current" | "stale" | "unknown";
+};
+
+export type TxlinePulseOptions = {
+ credentials: TxlinePulseCredentials | null;
+ fetchImpl?: typeof fetch;
+ now?: () => number;
+ timeoutMs?: number;
+ maxResponseBytes?: number;
+};
+
+export type TxlinePulseCacheScope = "configured" | "unconfigured";
+
+export type TxlinePulseCache = {
+ get(scope: TxlinePulseCacheScope, load: () => Promise): Promise;
+};
+
+type CachedPulse = {
+ value: TxlinePulseResponse;
+ cachedAtTsMs: number;
+ expiresAtTsMs: number;
+};
+
+type PulseCacheSlot = {
+ served: CachedPulse | null;
+ lastConnected: CachedPulse | null;
+ inFlight: Promise | null;
+};
+
+function validCredential(value: string): boolean {
+ return value.trim().length >= 12 && !/[\r\n]/.test(value);
+}
+
+function roundedLatency(startedAt: number, checkedAt: number): number {
+ const elapsed = Math.max(0, checkedAt - startedAt);
+ return Math.max(25, Math.ceil(elapsed / 25) * 25);
+}
+
+function responseFreshness(response: Response, checkedAt: number): TxlinePulseResponse["freshnessClass"] {
+ const responseDate = response.headers.get("date");
+ if (!responseDate) return "unknown";
+ const responseTs = Date.parse(responseDate);
+ if (!Number.isFinite(responseTs)) return "unknown";
+ return Math.abs(checkedAt - responseTs) <= 2 * 60_000 ? "current" : "stale";
+}
+
+async function cappedResponseText(response: Response, maximumBytes: number): Promise {
+ const declaredLength = Number(response.headers.get("content-length"));
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
+ throw new Error("TXLine pulse response exceeds the public-derivation cap");
+ }
+ if (!response.body) {
+ const text = await response.text();
+ if (new TextEncoder().encode(text).byteLength > maximumBytes) {
+ throw new Error("TXLine pulse response exceeds the public-derivation cap");
+ }
+ return text;
+ }
+ const reader = response.body.getReader();
+ const chunks: Uint8Array[] = [];
+ let totalBytes = 0;
+ while (true) {
+ const next = await reader.read();
+ if (next.done) break;
+ totalBytes += next.value.byteLength;
+ if (totalBytes > maximumBytes) {
+ await reader.cancel();
+ throw new Error("TXLine pulse response exceeds the public-derivation cap");
+ }
+ chunks.push(next.value);
+ }
+ const joined = new Uint8Array(totalBytes);
+ let offset = 0;
+ for (const chunk of chunks) {
+ joined.set(chunk, offset);
+ offset += chunk.byteLength;
+ }
+ return new TextDecoder().decode(joined);
+}
+
+function basePulse(checkedAt: number): TxlinePulseResponse {
+ return {
+ network: "mainnet",
+ serviceLevel: "SL12",
+ checkedAt: new Date(checkedAt).toISOString(),
+ status: "degraded",
+ latencyMsRounded: null,
+ aggregateFixtureCount: null,
+ freshnessClass: "unknown"
+ };
+}
+
+/**
+ * Keeps the public Worker pulse bounded to one upstream request per credential
+ * state and cache window. Concurrent callers share one promise. A failed
+ * refresh may retain only the previous aggregate count for five minutes, and
+ * is explicitly downgraded to stale/degraded before it crosses the boundary.
+ */
+export function createTxlinePulseCache(options: {
+ now?: () => number;
+ ttlMs?: number;
+ staleRetentionMs?: number;
+} = {}): TxlinePulseCache {
+ const now = options.now ?? Date.now;
+ const ttlMs = options.ttlMs ?? TXLINE_PULSE_CACHE_TTL_MS;
+ const staleRetentionMs = options.staleRetentionMs ?? TXLINE_PULSE_STALE_RETENTION_MS;
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1_000 || ttlMs > 5 * 60_000) {
+ throw new Error("TXLine pulse cache TTL must be between 1 and 300 seconds");
+ }
+ if (
+ !Number.isSafeInteger(staleRetentionMs) ||
+ staleRetentionMs < ttlMs ||
+ staleRetentionMs > 15 * 60_000
+ ) {
+ throw new Error("TXLine pulse stale retention must be between the cache TTL and 15 minutes");
+ }
+ const slots: Record = {
+ configured: { served: null, lastConnected: null, inFlight: null },
+ unconfigured: { served: null, lastConnected: null, inFlight: null }
+ };
+
+ return {
+ get(scope, load) {
+ const slot = slots[scope];
+ const requestedAtTsMs = now();
+ if (slot.served && requestedAtTsMs < slot.served.expiresAtTsMs) {
+ return Promise.resolve(slot.served.value);
+ }
+ if (slot.inFlight) return slot.inFlight;
+
+ const request = (async (): Promise => {
+ let loaded: TxlinePulseResponse;
+ try {
+ loaded = await load();
+ } catch {
+ loaded = basePulse(now());
+ }
+ const completedAtTsMs = now();
+ if (loaded.status === "connected") {
+ const connected = {
+ value: loaded,
+ cachedAtTsMs: completedAtTsMs,
+ expiresAtTsMs: completedAtTsMs + ttlMs
+ };
+ slot.lastConnected = connected;
+ slot.served = connected;
+ return loaded;
+ }
+
+ const lastConnected = slot.lastConnected;
+ if (
+ lastConnected &&
+ Math.max(0, completedAtTsMs - lastConnected.cachedAtTsMs) <= staleRetentionMs
+ ) {
+ const stale: TxlinePulseResponse = {
+ ...lastConnected.value,
+ status: "degraded",
+ latencyMsRounded: null,
+ freshnessClass: "stale"
+ };
+ slot.served = {
+ value: stale,
+ cachedAtTsMs: completedAtTsMs,
+ expiresAtTsMs: completedAtTsMs + ttlMs
+ };
+ return stale;
+ }
+
+ slot.lastConnected = null;
+ slot.served = {
+ value: loaded,
+ cachedAtTsMs: completedAtTsMs,
+ expiresAtTsMs: completedAtTsMs + ttlMs
+ };
+ return loaded;
+ })();
+ slot.inFlight = request;
+ const clearInFlight = () => {
+ if (slot.inFlight === request) slot.inFlight = null;
+ };
+ void request.then(clearInFlight, clearInFlight);
+ return request;
+ }
+ };
+}
+
+function validFixtureIdentityRow(value: unknown): boolean {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
+ const row = value as Record;
+ if (Object.keys(row).length === 0) return false;
+ const fixtureId = row.FixtureId;
+ const validFixtureId =
+ (typeof fixtureId === "number" && Number.isSafeInteger(fixtureId) && fixtureId > 0) ||
+ (typeof fixtureId === "string" && /^[1-9][0-9]*$/.test(fixtureId));
+ if (!validFixtureId) return false;
+ const startTime = row.StartTime;
+ if (typeof startTime === "number") return Number.isFinite(startTime) && startTime > 0;
+ if (typeof startTime !== "string" || startTime.trim().length === 0) return false;
+ const numeric = Number(startTime);
+ return (Number.isFinite(numeric) && numeric > 0) || Number.isFinite(Date.parse(startTime));
+}
+
+/**
+ * Calls the official fixture snapshot only, then destroys the payload after
+ * deriving an aggregate count and response-header freshness class. No raw row,
+ * identity, market value, source timestamp, or credential crosses this return
+ * boundary. The function is Worker-portable; credential loading stays outside.
+ */
+export async function buildTxlinePulse(options: TxlinePulseOptions): Promise {
+ const now = options.now ?? Date.now;
+ const fetchImpl = options.fetchImpl ?? fetch;
+ const timeoutMs = options.timeoutMs ?? TXLINE_PULSE_TIMEOUT_MS;
+ const maximumBytes = options.maxResponseBytes ?? TXLINE_PULSE_MAX_RESPONSE_BYTES;
+ const startedAt = now();
+ const degraded = () => basePulse(now());
+ const credentials = options.credentials;
+ if (!credentials || !validCredential(credentials.jwt) || !validCredential(credentials.apiToken)) return degraded();
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 10_000) return degraded();
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1_024 || maximumBytes > 1_048_576) return degraded();
+
+ const epochDay = Math.floor(startedAt / 86_400_000);
+ const url = `${TXLINE_PULSE_ORIGIN}/api/fixtures/snapshot?startEpochDay=${epochDay}`;
+ const controller = new AbortController();
+ let timer: ReturnType | undefined;
+ try {
+ const timeout = new Promise((_resolve, reject) => {
+ timer = setTimeout(() => {
+ controller.abort();
+ reject(new Error("TXLine pulse timed out"));
+ }, timeoutMs);
+ });
+ const response = await Promise.race([
+ fetchImpl(url, {
+ method: "GET",
+ headers: {
+ accept: "application/json",
+ Authorization: `Bearer ${credentials.jwt}`,
+ "X-Api-Token": credentials.apiToken
+ },
+ signal: controller.signal
+ }),
+ timeout
+ ]);
+ if (!response.ok || !(response.headers.get("content-type") ?? "").toLowerCase().includes("application/json")) {
+ return {
+ ...basePulse(now()),
+ latencyMsRounded: roundedLatency(startedAt, now())
+ };
+ }
+ const text = await cappedResponseText(response, maximumBytes);
+ const rows = JSON.parse(text) as unknown;
+ if (!Array.isArray(rows) || !rows.every(validFixtureIdentityRow)) return {
+ ...basePulse(now()),
+ latencyMsRounded: roundedLatency(startedAt, now())
+ };
+ const checkedAt = now();
+ return {
+ network: "mainnet",
+ serviceLevel: "SL12",
+ checkedAt: new Date(checkedAt).toISOString(),
+ status: "connected",
+ latencyMsRounded: roundedLatency(startedAt, checkedAt),
+ aggregateFixtureCount: rows.length,
+ freshnessClass: responseFreshness(response, checkedAt)
+ };
+ } catch {
+ return degraded();
+ } finally {
+ if (timer !== undefined) clearTimeout(timer);
+ controller.abort();
+ }
+}
diff --git a/src/dash/verify-edge-bundle.ts b/src/dash/verify-edge-bundle.ts
new file mode 100644
index 0000000..d4bebe2
--- /dev/null
+++ b/src/dash/verify-edge-bundle.ts
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+import { resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { verifyPublicEdgeBundle } from "./edge-bundle.js";
+
+const canonicalRepoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
+
+async function main(): Promise {
+ try {
+ const result = await verifyPublicEdgeBundle(canonicalRepoRoot);
+ process.stdout.write(
+ `Verified ${result.manifest.routes.length} frozen edge routes ` +
+ `(bundle ${result.manifest.edgeBundleSha256})\n`
+ );
+ } catch (error) {
+ process.stderr.write(`Public edge verification failed: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ await main();
+}
diff --git a/src/dash/verify-sites-build.ts b/src/dash/verify-sites-build.ts
new file mode 100644
index 0000000..804b7a1
--- /dev/null
+++ b/src/dash/verify-sites-build.ts
@@ -0,0 +1,256 @@
+#!/usr/bin/env node
+import { readFile, readdir, stat } from "node:fs/promises";
+import { extname, resolve, sep } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { verifyPublicEdgeBundle } from "./edge-bundle.js";
+
+const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
+const dashboardRoot = resolve(repoRoot, "apps/dashboard");
+const distRoot = resolve(dashboardRoot, "dist");
+const clientRoot = resolve(distRoot, "client");
+const workerPath = resolve(distRoot, "server/index.js");
+
+type WorkerEnv = {
+ ASSETS: { fetch(request: Request): Promise };
+ TXLINE_JWT?: string;
+ TXLINE_API_TOKEN?: string;
+ TXLINE_SERVICE_LEVEL_ID?: string;
+};
+
+type WorkerModule = {
+ default: {
+ fetch(request: Request, env: WorkerEnv): Promise;
+ };
+};
+
+const contentTypes: Record = {
+ ".css": "text/css; charset=utf-8",
+ ".html": "text/html; charset=utf-8",
+ ".js": "text/javascript; charset=utf-8",
+ ".json": "application/json; charset=utf-8",
+ ".woff2": "font/woff2"
+};
+
+function assert(condition: unknown, message: string): asserts condition {
+ if (!condition) throw new Error(message);
+}
+
+class LocalAssetBinding {
+ readonly requests: string[] = [];
+
+ constructor(private readonly corruptPath: string | null = null) {}
+
+ async fetch(request: Request): Promise {
+ const url = new URL(request.url);
+ this.requests.push(`${request.method} ${url.pathname}`);
+ let decoded: string;
+ try {
+ decoded = decodeURIComponent(url.pathname);
+ } catch {
+ return new Response("Not found", { status: 404 });
+ }
+ const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
+ const candidate = resolve(clientRoot, relative);
+ if (candidate !== clientRoot && !candidate.startsWith(`${clientRoot}${sep}`)) {
+ return new Response("Not found", { status: 404 });
+ }
+ try {
+ if (!(await stat(candidate)).isFile()) return new Response("Not found", { status: 404 });
+ const file = await readFile(candidate);
+ const body = decoded === this.corruptPath
+ ? new Uint8Array([...file, 0x20])
+ : new Uint8Array(file);
+ return new Response(request.method === "HEAD" ? null : body, {
+ status: 200,
+ headers: {
+ "content-type": contentTypes[extname(candidate)] ?? "application/octet-stream"
+ }
+ });
+ } catch {
+ return new Response("Not found", { status: 404 });
+ }
+ }
+}
+
+async function verifyLayout(): Promise {
+ for (const path of [
+ workerPath,
+ resolve(clientRoot, "index.html"),
+ resolve(clientRoot, "artifacts/dashboard/manifest.json"),
+ resolve(clientRoot, "artifacts/edge/manifest.json"),
+ resolve(clientRoot, "artifacts/edge/judge-evidence.json")
+ ]) {
+ assert((await stat(path)).isFile(), `Sites build is missing ${path.slice(distRoot.length + 1)}`);
+ }
+ const assets = await readdir(resolve(clientRoot, "assets"));
+ assert(assets.some((name) => name.endsWith(".js")), "Sites build has no browser JavaScript asset");
+ assert(assets.some((name) => name.endsWith(".css")), "Sites build has no browser stylesheet asset");
+ const workerSource = await readFile(workerPath, "utf8");
+ assert(!workerSource.includes("node:"), "Sites Worker bundle contains a Node builtin import");
+ assert(workerSource.includes("ASSETS.fetch"), "Sites Worker bundle does not use the ASSETS binding");
+}
+
+async function verifyRuntime(): Promise {
+ const verified = await verifyPublicEdgeBundle(repoRoot);
+ const workerModule = await import(`${pathToFileURL(workerPath).href}?verify=${Date.now()}`) as WorkerModule;
+ const binding = new LocalAssetBinding();
+ const env: WorkerEnv = { ASSETS: binding };
+
+ for (const route of verified.manifest.routes) {
+ const get = await workerModule.default.fetch(new Request(`https://samaritan.test${route.apiPath}`), env);
+ assert(get.status === 200, `Sites Worker GET failed for ${route.apiPath}`);
+ assert(get.headers.get("cache-control") === "no-store", `Sites Worker cached ${route.apiPath}`);
+ assert(get.headers.get("x-content-type-options") === "nosniff", `Sites Worker lacks nosniff for ${route.apiPath}`);
+ assert(
+ get.headers.get("x-samaritan-edge-bundle") === verified.manifest.edgeBundleSha256,
+ `Sites Worker edge commitment changed for ${route.apiPath}`
+ );
+ const expected = await readFile(resolve(clientRoot, route.assetPath.replace(/^\/+/, "")), "utf8");
+ assert(await get.text() === expected, `Sites Worker body changed for ${route.apiPath}`);
+
+ const head = await workerModule.default.fetch(new Request(`https://samaritan.test${route.apiPath}`, {
+ method: "HEAD"
+ }), env);
+ assert(head.status === 200, `Sites Worker HEAD failed for ${route.apiPath}`);
+ assert(await head.text() === "", `Sites Worker HEAD returned a body for ${route.apiPath}`);
+ }
+
+ const health = await workerModule.default.fetch(new Request("https://samaritan.test/api/v1/health"), env);
+ assert(health.status === 200, "Sites Worker health route failed");
+ assert((await health.json() as { readOnly?: unknown }).readOnly === true, "Sites Worker health route is not read-only");
+
+ const pulse = await workerModule.default.fetch(new Request("https://samaritan.test/api/v1/txline/pulse"), env);
+ assert(pulse.status === 200, "Sites Worker TXLine pulse did not fail degraded without credentials");
+ const pulseValue = await pulse.json() as Record;
+ assert(pulseValue.status === "degraded", "Sites Worker TXLine pulse claimed connectivity without credentials");
+ assert(pulseValue.aggregateFixtureCount === null, "Sites Worker TXLine pulse exposed fixture data while degraded");
+ assert(
+ Object.keys(pulseValue).sort().join(",") === [
+ "aggregateFixtureCount",
+ "checkedAt",
+ "freshnessClass",
+ "latencyMsRounded",
+ "network",
+ "serviceLevel",
+ "status"
+ ].sort().join(","),
+ "Sites Worker TXLine pulse response crossed its derived-metadata allowlist"
+ );
+ assert(pulse.headers.get("cache-control") === "no-store", "Sites Worker cached the TXLine pulse");
+ const pulseHead = await workerModule.default.fetch(new Request("https://samaritan.test/api/v1/txline/pulse", {
+ method: "HEAD"
+ }), env);
+ assert(pulseHead.status === 200 && await pulseHead.text() === "", "Sites Worker TXLine pulse HEAD semantics changed");
+
+ const originalFetch = globalThis.fetch;
+ let upstreamPulseCalls = 0;
+ globalThis.fetch = async (input: string | URL | globalThis.Request, init?: RequestInit): Promise => {
+ upstreamPulseCalls += 1;
+ const upstream = new Request(input, init);
+ assert(
+ upstream.url.startsWith("https://txline.txodds.com/api/fixtures/snapshot?startEpochDay="),
+ "Sites Worker pulse called a non-official origin"
+ );
+ assert(upstream.headers.get("authorization") === "Bearer test-jwt-value", "Sites Worker pulse omitted the JWT");
+ assert(upstream.headers.get("x-api-token") === "test-api-token", "Sites Worker pulse omitted the API token");
+ return new Response(JSON.stringify([
+ { Participant1: "must-not-cross-boundary", FixtureId: 1, StartTime: "2026-07-18T21:00:00Z" },
+ { Participant2: "must-not-cross-boundary", FixtureId: 2, StartTime: "2026-07-19T19:00:00Z" }
+ ]), {
+ status: 200,
+ headers: {
+ "content-type": "application/json",
+ date: new Date().toUTCString()
+ }
+ });
+ };
+ try {
+ const configuredEnv = {
+ ASSETS: binding,
+ TXLINE_JWT: "test-jwt-value",
+ TXLINE_API_TOKEN: "test-api-token",
+ TXLINE_SERVICE_LEVEL_ID: "12"
+ };
+ const [connectedPulse, connectedHead, connectedGet] = await Promise.all([
+ workerModule.default.fetch(new Request("https://samaritan.test/api/v1/txline/pulse"), configuredEnv),
+ workerModule.default.fetch(new Request("https://samaritan.test/api/v1/txline/pulse", {
+ method: "HEAD"
+ }), configuredEnv),
+ workerModule.default.fetch(new Request("https://samaritan.test/api/v1/txline/pulse"), configuredEnv)
+ ]);
+ const connectedValue = await connectedPulse.json() as Record;
+ assert(connectedHead.status === 200 && await connectedHead.text() === "", "Cached Worker pulse HEAD semantics changed");
+ assert(
+ JSON.stringify(await connectedGet.json()) === JSON.stringify(connectedValue),
+ "Concurrent Worker pulse callers did not share one cached result"
+ );
+ assert(upstreamPulseCalls === 1, "Concurrent Worker pulse callers fanned out authenticated requests");
+ const cachedPulse = await workerModule.default.fetch(
+ new Request("https://samaritan.test/api/v1/txline/pulse"),
+ configuredEnv
+ );
+ assert(cachedPulse.status === 200, "Cached Worker pulse failed");
+ assert(upstreamPulseCalls === 1, "Worker pulse did not retain its bounded one-minute cache");
+ assert(connectedValue.status === "connected", "Sites Worker did not use hosted TXLine credentials");
+ assert(connectedValue.aggregateFixtureCount === 2, "Sites Worker pulse did not derive the aggregate fixture count");
+ assert(
+ !JSON.stringify(connectedValue).includes("must-not-cross-boundary"),
+ "Sites Worker pulse exposed a raw TXLine fixture row"
+ );
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+
+ const wrongTierPulse = await workerModule.default.fetch(
+ new Request("https://samaritan.test/api/v1/txline/pulse"),
+ {
+ ASSETS: binding,
+ TXLINE_JWT: "test-jwt-value",
+ TXLINE_API_TOKEN: "test-api-token",
+ TXLINE_SERVICE_LEVEL_ID: "1"
+ }
+ );
+ assert(
+ (await wrongTierPulse.json() as { status?: unknown }).status === "degraded",
+ "Sites Worker pulse accepted a non-SL12 credential context"
+ );
+
+ const unknownApi = await workerModule.default.fetch(new Request("https://samaritan.test/api/v1/private-state"), env);
+ assert(unknownApi.status === 404, "Sites Worker exposed an unknown API route");
+ const arbitraryFile = await workerModule.default.fetch(new Request("https://samaritan.test/package.json"), env);
+ assert(arbitraryFile.status === 404, "Sites Worker exposed a repository file outside dist");
+
+ const post = await workerModule.default.fetch(new Request("https://samaritan.test/api/v1/command", {
+ method: "POST"
+ }), env);
+ assert(post.status === 405 && post.headers.get("allow") === "GET, HEAD", "Sites Worker accepted a mutating method");
+
+ for (const pathname of ["/command", "/study", "/proof", "/casebook", "/matchroom"]) {
+ const deepRoute = await workerModule.default.fetch(new Request(`https://samaritan.test${pathname}`), env);
+ assert(deepRoute.status === 200, `Sites Worker SPA fallback failed for ${pathname}`);
+ assert((await deepRoute.text()).includes("
"), `Sites Worker did not return the SPA for ${pathname}`);
+ assert(deepRoute.headers.has("content-security-policy"), `Sites Worker omitted CSP for ${pathname}`);
+ }
+
+ const corruptRoute = verified.manifest.routes[0];
+ assert(corruptRoute, "Sites edge manifest has no frozen route");
+ const corruptBinding = new LocalAssetBinding(corruptRoute.assetPath);
+ const corrupt = await workerModule.default.fetch(new Request(`https://samaritan.test${corruptRoute.apiPath}`), {
+ ASSETS: corruptBinding
+ });
+ assert(corrupt.status === 503, "Sites Worker served a frozen API artifact after integrity failure");
+ assert((await corrupt.json() as { error?: unknown }).error === "evidence_unavailable", "Integrity failure disclosed internals");
+}
+
+async function main(): Promise {
+ try {
+ await verifyLayout();
+ await verifyRuntime();
+ process.stdout.write("Verified Sites Worker layout, frozen APIs, SPA fallback, and fail-closed integrity\n");
+ } catch (error) {
+ process.stderr.write(`Sites build verification failed: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
+
+await main();
diff --git a/src/demo/run-synthetic-judge-case.ts b/src/demo/run-synthetic-judge-case.ts
new file mode 100644
index 0000000..98d5061
--- /dev/null
+++ b/src/demo/run-synthetic-judge-case.ts
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+import { runSyntheticJudgeCase } from "./synthetic-judge-case.js";
+
+try {
+ const report = await runSyntheticJudgeCase();
+ const { receipt, ...summary } = report;
+ process.stdout.write(`${JSON.stringify({
+ ...summary,
+ receiptCommitment: {
+ receiptId: receipt.receiptId,
+ receiptHash: receipt.integrity.receiptHash,
+ committedLedgerHead: receipt.ledger.finalHeadHash,
+ lifecycleStatus: receipt.lifecycle.finalStatus,
+ disclosure: receipt.disclosure,
+ solanaAnchor: receipt.solanaAnchor
+ }
+ }, null, 2)}\n`);
+} catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`SYNTHETIC JUDGE CASE FAILED: ${message}\n`);
+ process.exitCode = 1;
+}
diff --git a/src/demo/synthetic-judge-case.ts b/src/demo/synthetic-judge-case.ts
new file mode 100644
index 0000000..1716e0f
--- /dev/null
+++ b/src/demo/synthetic-judge-case.ts
@@ -0,0 +1,576 @@
+import { CLAUDE_MODEL, CLAUDE_PRICING } from "../agents/claude-pricing.js";
+import type { AnalystAgent, TriageAgent } from "../agents/contracts.js";
+import {
+ CANONICAL_SCHEMA_VERSION,
+ type CanonicalEvent,
+ type CanonicalMarket,
+ type OddsQuoteEvent,
+ type PolymarketBookEvent,
+ type PolymarketResolutionEvent
+} from "../bus/events.js";
+import { PAPER_STUDY_TOTAL_SELECTOR_CONFIG } from "../config/paper-study.js";
+import { probability } from "../domain/probability.js";
+import type { PolymarketFeeParameters } from "../exec/paper.js";
+import { createPersistentPaperLaneRuntime } from "../harness/paper-lane-runtime.js";
+import type { PaperFixtureUniverse } from "../harness/paper-fixture-universe.js";
+import type { PaperRuntimeBatch } from "../harness/paper-runtime.js";
+import { runPaperSession } from "../harness/paper-session.js";
+import {
+ PAPER_STUDY_PROTOCOL_STATUS,
+ initializePaperStudyLedger
+} from "../harness/paper-study-ledger.js";
+import {
+ generateDecisionReceipt,
+ hashJsonEvidence,
+ sourceEvidenceCommitment
+} from "../proof/decision-receipt.js";
+import {
+ receiptReferenceHash,
+ sha256,
+ verifyDecisionReceipt,
+ type DecisionReceipt,
+ type DecisionReceiptVerification,
+ type ReceiptAgentRun
+} from "../proof/decision-receipt-schema.js";
+
+// The proving fixture is synthetic, but its registered-v2 clock must still obey
+// the same prospective registration boundary as every persistent study lane.
+const BASE_TS_MS = Date.UTC(2026, 6, 18, 8, 0, 0);
+const FIXTURE_ID = "synthetic-judge-fixture-v1";
+const CONDITION_ID = "synthetic-judge-condition-v1";
+const OVER_ASSET_ID = "synthetic-judge-over-asset-v1";
+const UNDER_ASSET_ID = "synthetic-judge-under-asset-v1";
+const LINE_MILLI = 2_500;
+const DECISION_LATENCY_MS = 300;
+const MAXIMUM_PENDING_MS = 60_000;
+const KICKOFF_TS_MS = BASE_TS_MS + 4 * 60 * 60_000;
+const DEMO_PROCESSING_TS_MS = Date.UTC(2026, 6, 18, 15, 0, 0);
+const DEMO_RECEIPT_GENERATED_AT_TS_MS = DEMO_PROCESSING_TS_MS + 60_000;
+
+const MARKET: CanonicalMarket = Object.freeze({
+ family: "total_goals",
+ period: "full_time",
+ lineMilli: LINE_MILLI,
+ key: `${FIXTURE_ID}:total_goals:full_time:${LINE_MILLI}`
+});
+
+const LABEL =
+ "SYNTHETIC JUDGE CASE — no real match, licensed feed record, external model call, venue order, wallet, network request, or performance evidence";
+
+type SyntheticBoundaryAudit = {
+ triageStubCalls: number;
+ analystStubCalls: number;
+ anthropicCalls: 0;
+ txlineApiCalls: 0;
+ polymarketApiCalls: 0;
+ walletCalls: 0;
+ solanaRpcCalls: 0;
+ realOrders: 0;
+};
+
+export type SyntheticJudgeCaseReport = {
+ schemaVersion: 1;
+ label: typeof LABEL;
+ synthetic: true;
+ performanceUse: "excluded_synthetic";
+ protocolStatus: typeof PAPER_STUDY_PROTOCOL_STATUS;
+ realMoneyGate: "closed";
+ boundaries: SyntheticBoundaryAudit;
+ runtime: {
+ canonicalEventsPublished: number;
+ rawSignals: number;
+ normalizedSignals: number;
+ routedSignals: number;
+ paperExecutionStatus: "filled" | "partial";
+ closeMarks: number;
+ settlements: number;
+ finalLifecycleStatus: "filled_settled" | "partial_settled";
+ ledgerRows: number;
+ ledgerHashSchemaVersions: number[];
+ ledgerHeadSha256: string;
+ };
+ receiptVerification: DecisionReceiptVerification;
+ receipt: DecisionReceipt;
+};
+
+function oddsQuote(sequence: number, tsMs: number, overProbability: number): OddsQuoteEvent {
+ const underProbability = 1 - overProbability;
+ return {
+ schemaVersion: CANONICAL_SCHEMA_VERSION,
+ kind: "odds.quote",
+ eventId: `synthetic-txline-quote-${sequence}`,
+ source: "txline",
+ sourceTsMs: tsMs,
+ observedTsMs: tsMs + 10,
+ fixtureId: FIXTURE_ID,
+ sourceMessageId: `synthetic-message-${sequence}`,
+ bookmaker: "SYNTHETIC_TXLINE_STABLE_PRICE",
+ bookmakerId: 10021,
+ inRunning: false,
+ gameState: null,
+ market: MARKET,
+ outcomes: [
+ {
+ outcome: "over",
+ oddsX1000: Math.round(1_000 / overProbability),
+ fairProbability: probability(overProbability)
+ },
+ {
+ outcome: "under",
+ oddsX1000: Math.round(1_000 / underProbability),
+ fairProbability: probability(underProbability)
+ }
+ ]
+ };
+}
+
+function book(input: {
+ id: string;
+ sourceTsMs: number;
+ observedTsMs: number;
+ bid: number;
+ ask: number;
+ secondAsk?: number;
+}): PolymarketBookEvent {
+ return {
+ schemaVersion: CANONICAL_SCHEMA_VERSION,
+ kind: "polymarket.book",
+ eventId: input.id,
+ source: "polymarket",
+ sourceTsMs: input.sourceTsMs,
+ observedTsMs: input.observedTsMs,
+ fixtureId: FIXTURE_ID,
+ market: MARKET,
+ mappingStatus: "verified",
+ conditionId: CONDITION_ID,
+ assetId: OVER_ASSET_ID,
+ outcome: "over",
+ tokenRole: "canonical",
+ bids: [{ price: probability(input.bid), size: "100" }],
+ asks: input.secondAsk === undefined
+ ? [{ price: probability(input.ask), size: "100" }]
+ : [
+ { price: probability(input.ask), size: "2" },
+ { price: probability(input.secondAsk), size: "100" }
+ ],
+ lastTradePrice: probability((input.bid + input.ask) / 2),
+ tickSize: "0.01"
+ };
+}
+
+function resolution(): PolymarketResolutionEvent {
+ const sourceTsMs = KICKOFF_TS_MS + 2 * 60 * 60_000;
+ return {
+ schemaVersion: CANONICAL_SCHEMA_VERSION,
+ kind: "polymarket.resolution",
+ eventId: "synthetic-polymarket-resolution",
+ source: "polymarket",
+ sourceTsMs,
+ observedTsMs: sourceTsMs + 10,
+ fixtureId: FIXTURE_ID,
+ market: MARKET,
+ mappingStatus: "verified",
+ conditionId: CONDITION_ID,
+ assetIds: [OVER_ASSET_ID, UNDER_ASSET_ID],
+ winningAssetId: OVER_ASSET_ID,
+ winningOutcomeLabel: "Over"
+ };
+}
+
+function syntheticAgentRun(input: {
+ stage: "triage" | "analyst";
+ response: unknown;
+}): ReceiptAgentRun {
+ const model = input.stage === "triage" ? CLAUDE_MODEL.triage : CLAUDE_MODEL.analyst;
+ const pricing = CLAUDE_PRICING[model];
+ return {
+ stage: input.stage,
+ invocationClass: "synthetic_stub",
+ model,
+ promptVersion: `synthetic-judge-${input.stage}-v1`,
+ promptSha256: sha256(`synthetic-judge:${input.stage}:fixed-prompt-v1`),
+ responseSha256: hashJsonEvidence(input.response),
+ billingEvidenceRefSha256: receiptReferenceHash("synthetic_billing", `judge:${input.stage}:zero`),
+ status: "success",
+ usage: {
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheCreationInputTokens: 0,
+ cacheReadInputTokens: 0
+ },
+ pricing: {
+ pricingVersion: "anthropic-public-2026-07-12",
+ currency: "nano_usd",
+ inputNanoUsdPerToken: pricing.inputNanoUsdPerToken,
+ outputNanoUsdPerToken: pricing.outputNanoUsdPerToken,
+ cacheWriteNanoUsdPerToken: pricing.cacheWriteNanoUsdPerToken,
+ cacheReadNanoUsdPerToken: pricing.cacheReadNanoUsdPerToken
+ },
+ actualCostNanoUsd: 0
+ };
+}
+
+function requiredBatch(value: T | undefined, message: string): T {
+ if (value === undefined) throw new Error(message);
+ return value;
+}
+
+function syntheticUniverse(laneStartTsMs: number): PaperFixtureUniverse {
+ return {
+ generatedAt: new Date(laneStartTsMs).toISOString(),
+ laneStartTsMs,
+ selectorConfig: structuredClone(PAPER_STUDY_TOTAL_SELECTOR_CONFIG),
+ fixtures: [{
+ fixtureId: FIXTURE_ID,
+ home: "Synthetic Home",
+ away: "Synthetic Away",
+ kickoffTsMs: KICKOFF_TS_MS,
+ eventSlugs: ["synthetic-judge-event-v1"],
+ mappingStatus: "verified",
+ selectedTotal: {
+ marketId: "synthetic-judge-market-v1",
+ marketKey: MARKET.key,
+ conditionId: CONDITION_ID,
+ lineMilli: LINE_MILLI,
+ preKickoffOverProbability: 0.5,
+ preKickoffPointTsMs: BASE_TS_MS,
+ coveragePoints: 1_000,
+ assetIds: [OVER_ASSET_ID, UNDER_ASSET_ID]
+ },
+ evidenceGrade: "paired_order_books",
+ capabilities: {
+ signalResearchReplay: true,
+ executablePaperReplay: true,
+ kickoffCloseReplay: true,
+ publicResolutionReplay: true
+ },
+ bountyLane: {
+ mode: "executable_book_replay",
+ exploratory: true,
+ reason: "Synthetic conductor proof only; permanently excluded from performance."
+ },
+ longRunLane: {
+ eligible: false,
+ reason: "predates_long_run_lane_start"
+ },
+ pairedCapture: {
+ runId: "synthetic-judge-capture-v1",
+ status: "verified",
+ fixtureId: FIXTURE_ID,
+ eventSlug: "synthetic-judge-event-v1",
+ logComplete: true,
+ mappingConfirmed: true,
+ identityParity: true,
+ replayMode: "capture-order-per-source",
+ rows: 20,
+ firstPolymarketObservedTsMs: BASE_TS_MS + 110,
+ lastPolymarketObservedTsMs: KICKOFF_TS_MS + 2 * 60 * 60_000 + 10,
+ firstTxlineOddsObservedTsMs: BASE_TS_MS + 10,
+ lastTxlineOddsObservedTsMs: BASE_TS_MS + 8 * 60_000 + 10,
+ firstTxlineScoresObservedTsMs: BASE_TS_MS + 210,
+ lastTxlineScoresObservedTsMs: KICKOFF_TS_MS + 2 * 60 * 60_000 + 10,
+ selectedTotal: {
+ eventSlug: "synthetic-judge-event-v1",
+ marketId: "synthetic-judge-market-v1",
+ conditionId: CONDITION_ID,
+ lineMilli: LINE_MILLI,
+ assetIds: [OVER_ASSET_ID, UNDER_ASSET_ID]
+ },
+ selectedBookDepthComplete: true,
+ exactFixtureTxlineOddsAvailable: true,
+ exactFixtureTxlineScoresAvailable: true,
+ exactFixtureScoreCompleted: true,
+ proofCommitment: "0".repeat(64),
+ kickoffCloseAvailable: true,
+ publicResolutionAvailable: true,
+ publicMarketResolvedNormalized: true
+ }
+ }],
+ summary: {
+ fixtures: 1,
+ pairedBookReplays: 1,
+ executableBookReplays: 1,
+ bookLifecycleReplays: 0,
+ signalResearchOnly: 0,
+ unavailable: 0,
+ longRunEligible: 0
+ }
+ };
+}
+
+async function* canonicalSequence(events: CanonicalEvent[]): AsyncGenerator {
+ for (const event of events) yield event;
+}
+
+/**
+ * Runs a closed-world proving case through the production paper components.
+ * All canonical records are invented inside this module and carry synthetic
+ * identities. Nothing reads a file, environment secret, API, RPC, wallet, or
+ * external model, and the resulting receipt is permanently performance-excluded.
+ */
+export async function runSyntheticJudgeCase(): Promise {
+ let durableInsertTsMs = DEMO_PROCESSING_TS_MS;
+ const boundaryAudit: SyntheticBoundaryAudit = {
+ triageStubCalls: 0,
+ analystStubCalls: 0,
+ anthropicCalls: 0,
+ txlineApiCalls: 0,
+ polymarketApiCalls: 0,
+ walletCalls: 0,
+ solanaRpcCalls: 0,
+ realOrders: 0
+ };
+ const handle = initializePaperStudyLedger({
+ path: ":memory:",
+ lane: "bounty",
+ startedAtTsMs: BASE_TS_MS - 60_000,
+ insertedAtNow: () => durableInsertTsMs++
+ });
+
+ try {
+ const triageAgent: TriageAgent = {
+ triage: async () => {
+ boundaryAudit.triageStubCalls += 1;
+ return {
+ decision: "escalate",
+ priority: "normal",
+ rationale: "Synthetic proving signal meets the fixed escalation path; performance use is forbidden."
+ };
+ }
+ };
+ const analystAgent: AnalystAgent = {
+ investigate: async ({ signal, asOfTsMs }) => {
+ boundaryAudit.analystStubCalls += 1;
+ return {
+ schemaVersion: 1,
+ signalId: signal.signalId,
+ fixtureId: signal.fixtureId,
+ marketKey: signal.market.key,
+ outcome: signal.outcome,
+ direction: signal.direction,
+ recommendation: "paper_trade",
+ fairProbability: signal.evidence.consensusProbability,
+ thesisSummary: "Synthetic consensus movement remains ahead of the synthetic paper book.",
+ evidenceFor: ["The synthetic executable gap clears the frozen deterministic paper threshold."],
+ steelmanAgainst: "The invented movement could reverse before the invented kickoff.",
+ invalidationConditions: ["The synthetic ask reaches the deterministic private boundary."],
+ submittedAtTsMs: asOfTsMs,
+ expiresAtTsMs: asOfTsMs + 15 * 60_000,
+ analystModel: CLAUDE_MODEL.analyst
+ };
+ }
+ };
+ const persistentRuntime = createPersistentPaperLaneRuntime({
+ lane: "bounty",
+ initialization: handle.initialization,
+ universe: syntheticUniverse(handle.initialization.startedAtTsMs),
+ ledger: handle.ledger,
+ triageAgent,
+ analystAgent,
+ executionLatencyMs: DECISION_LATENCY_MS,
+ maximumPendingMs: MAXIMUM_PENDING_MS,
+ feeResolver: async (executionBook, asOfTsMs): Promise => ({
+ source: "polymarket_clob_market_info",
+ conditionId: executionBook.conditionId,
+ feesEnabled: true,
+ takerFeeRate: 0.05,
+ feeCurveExponent: 1,
+ takerOnly: true,
+ minimumOrderSize: 5,
+ minimumTickSize: 0.01,
+ fetchedAtTsMs: asOfTsMs
+ })
+ });
+ const batches: Array<{ event: CanonicalEvent; batch: PaperRuntimeBatch }> = [];
+ const events: CanonicalEvent[] = [];
+ const baselineProbabilities = [0.5, 0.5002, 0.4999, 0.5001, 0.5, 0.5002, 0.4999, 0.5001];
+ for (const [index, fair] of baselineProbabilities.entries()) {
+ const tsMs = BASE_TS_MS + index * 60_000;
+ events.push(oddsQuote(index, tsMs, fair));
+ events.push(book({
+ id: `synthetic-baseline-book-${index}`,
+ sourceTsMs: tsMs + 100,
+ observedTsMs: tsMs + 110,
+ bid: 0.5,
+ ask: 0.52
+ }));
+ }
+
+ const triggerQuote = oddsQuote(8, BASE_TS_MS + 8 * 60_000, 0.55);
+ events.push(triggerQuote);
+
+ const executionBook = book({
+ id: "synthetic-execution-book",
+ sourceTsMs: triggerQuote.sourceTsMs + 2_000,
+ observedTsMs: triggerQuote.observedTsMs + 2_000,
+ bid: 0.5,
+ ask: 0.52,
+ secondAsk: 0.53
+ });
+ events.push(executionBook);
+ const closeBook = book({
+ id: "synthetic-closing-book",
+ sourceTsMs: KICKOFF_TS_MS - 1_000,
+ observedTsMs: KICKOFF_TS_MS - 990,
+ bid: 0.57,
+ ask: 0.59
+ });
+ events.push(closeBook);
+ const settlementEvent = resolution();
+ events.push(settlementEvent);
+
+ const session = await runPaperSession({
+ source: canonicalSequence(events),
+ runtime: persistentRuntime,
+ onBatch: ({ event, batch }) => { batches.push({ event, batch }); }
+ });
+ requiredBatch(
+ batches.find(({ batch }) => batch.routedSignalIds.length === 1),
+ "Frozen synthetic detector path did not route exactly one signal"
+ );
+ const signalBook = requiredBatch(
+ batches.find(({ event }) => event.eventId === "synthetic-baseline-book-7")?.event,
+ "Synthetic signal-side paper book is missing"
+ );
+ if (signalBook.kind !== "polymarket.book") {
+ throw new Error("Synthetic signal-side evidence is not a canonical Polymarket book");
+ }
+ const executionBatch = requiredBatch(
+ batches.find(({ batch }) => batch.caseResults.length === 1),
+ "Synthetic scheduler did not produce one paper execution result"
+ );
+ const executionResult = requiredBatch(
+ executionBatch.batch.caseResults[0],
+ "Synthetic paper execution result is missing"
+ );
+ if (executionResult.status !== "filled" && executionResult.status !== "partial") {
+ throw new Error(`Synthetic paper execution failed closed with status ${executionResult.status}`);
+ }
+ const lifecycleBatch = requiredBatch(
+ batches.find(({ batch }) => batch.settlementResults.length === 1),
+ "Synthetic lifecycle did not settle exactly one paper position"
+ );
+ if (lifecycleBatch.batch.closeResults.length !== 1) {
+ throw new Error("Synthetic lifecycle did not create exactly one kickoff close mark");
+ }
+
+ const caseEntries = handle.ledger.entries(executionResult.caseId);
+ const triageResponse = requiredBatch(
+ caseEntries.find((entry) => entry.kind === "triage_decision")?.payload,
+ "Synthetic triage ledger commitment is missing"
+ );
+ const analystResponse = requiredBatch(
+ caseEntries.find((entry) => entry.kind === "thesis_submitted")?.payload,
+ "Synthetic thesis ledger commitment is missing"
+ );
+ const sourceEvidence = [
+ sourceEvidenceCommitment({
+ source: "txline",
+ role: "signal",
+ sourceTsMs: triggerQuote.sourceTsMs,
+ observedAtTsMs: triggerQuote.observedTsMs,
+ payload: JSON.parse(JSON.stringify(triggerQuote))
+ }),
+ sourceEvidenceCommitment({
+ source: "polymarket",
+ role: "signal",
+ sourceTsMs: signalBook.sourceTsMs,
+ observedAtTsMs: signalBook.observedTsMs,
+ payload: JSON.parse(JSON.stringify(signalBook))
+ }),
+ sourceEvidenceCommitment({
+ source: "polymarket",
+ role: "execution",
+ sourceTsMs: executionBook.sourceTsMs,
+ observedAtTsMs: executionBook.observedTsMs,
+ payload: JSON.parse(JSON.stringify(executionBook))
+ }),
+ sourceEvidenceCommitment({
+ source: "polymarket",
+ role: "close",
+ sourceTsMs: closeBook.sourceTsMs,
+ observedAtTsMs: closeBook.observedTsMs,
+ payload: JSON.parse(JSON.stringify(closeBook))
+ }),
+ sourceEvidenceCommitment({
+ source: "polymarket",
+ role: "settlement",
+ sourceTsMs: settlementEvent.sourceTsMs,
+ observedAtTsMs: settlementEvent.observedTsMs,
+ payload: JSON.parse(JSON.stringify(settlementEvent))
+ })
+ ];
+ const receipt = generateDecisionReceipt({
+ ledger: handle.ledger,
+ caseId: executionResult.caseId,
+ context: {
+ generatedAtTsMs: DEMO_RECEIPT_GENERATED_AT_TS_MS,
+ provenance: {
+ evidenceClass: "synthetic_proving_fixture",
+ synthetic: true,
+ performanceUse: "excluded_synthetic",
+ label: LABEL
+ },
+ build: {
+ codeVersion: "synthetic-judge-case-v1",
+ codeSha256: sha256("samaritan:synthetic-judge-case:v1"),
+ configSha256: handle.initialization.configHash
+ },
+ sourceEvidence,
+ agentRuns: [
+ syntheticAgentRun({ stage: "triage", response: triageResponse }),
+ syntheticAgentRun({ stage: "analyst", response: analystResponse })
+ ],
+ executionBookEvidenceRefSha256: sourceEvidence[2]!.evidenceRefSha256,
+ closeBookEvidenceRefSha256: sourceEvidence[3]!.evidenceRefSha256,
+ settlementEvidenceRefSha256: sourceEvidence[4]!.evidenceRefSha256,
+ solanaAnchor: null
+ }
+ });
+ const receiptVerification = verifyDecisionReceipt(receipt);
+ const ledgerVerification = handle.ledger.verifyChain();
+ const summary = persistentRuntime.portfolio.summary();
+ if (
+ boundaryAudit.triageStubCalls !== 1 ||
+ boundaryAudit.analystStubCalls !== 1 ||
+ summary.positions !== 1 ||
+ summary.openPositions !== 0 ||
+ summary.settledPositions !== 1
+ ) {
+ throw new Error("Synthetic proving case did not preserve its bounded one-case invariants");
+ }
+ if (
+ receiptVerification.lifecycleStatus !== "filled_settled" &&
+ receiptVerification.lifecycleStatus !== "partial_settled"
+ ) {
+ throw new Error(`Unexpected synthetic receipt lifecycle ${receiptVerification.lifecycleStatus}`);
+ }
+
+ return {
+ schemaVersion: 1,
+ label: LABEL,
+ synthetic: true,
+ performanceUse: "excluded_synthetic",
+ protocolStatus: handle.initialization.protocolStatus,
+ realMoneyGate: handle.initialization.realMoneyGate,
+ boundaries: boundaryAudit,
+ runtime: {
+ canonicalEventsPublished: session.events,
+ rawSignals: batches.reduce((sum, item) => sum + item.batch.rawSignals.length, 0),
+ normalizedSignals: batches.reduce((sum, item) => sum + item.batch.signals.length, 0),
+ routedSignals: batches.reduce((sum, item) => sum + item.batch.routedSignalIds.length, 0),
+ paperExecutionStatus: executionResult.status,
+ closeMarks: batches.reduce((sum, item) => sum + item.batch.closeResults.length, 0),
+ settlements: batches.reduce((sum, item) => sum + item.batch.settlementResults.length, 0),
+ finalLifecycleStatus: receiptVerification.lifecycleStatus,
+ ledgerRows: ledgerVerification.rows,
+ ledgerHashSchemaVersions: ledgerVerification.hashSchemaVersions,
+ ledgerHeadSha256: ledgerVerification.headHash
+ },
+ receiptVerification,
+ receipt
+ };
+ } finally {
+ handle.ledger.close();
+ }
+}
diff --git a/src/detectors/bank.ts b/src/detectors/bank.ts
index 272d98a..7a20d23 100644
--- a/src/detectors/bank.ts
+++ b/src/detectors/bank.ts
@@ -228,6 +228,7 @@ export class DetectorBank {
signalId: signalId(identity),
kind,
detectedAtTsMs: snapshot.asOfTsMs,
+ observedAtTsMs: snapshot.observedAtTsMs,
fixtureId: snapshot.fixtureId,
market: snapshot.market,
outcome: snapshot.outcome,
diff --git a/src/detectors/economic-cases.ts b/src/detectors/economic-cases.ts
new file mode 100644
index 0000000..1fce2cb
--- /dev/null
+++ b/src/detectors/economic-cases.ts
@@ -0,0 +1,181 @@
+import { stableJson } from "../domain/json.js";
+import type { CanonicalOutcome } from "../bus/events.js";
+import type { DetectorSignal } from "./types.js";
+
+export type EconomicCaseDispositionReason =
+ | "executable_buy_retained"
+ | "duplicate_executable_buy_collapsed"
+ | "complementary_sell_collapsed_into_executable_buy"
+ | "sell_only_unproven_executable_ask"
+ | "unsupported_total_goals_outcome"
+ | "non_binary_market_passthrough";
+
+export type EconomicCaseDisposition = {
+ signalId: string;
+ disposition: "retained" | "collapsed" | "dropped";
+ reason: EconomicCaseDispositionReason;
+ economicOutcome: "over" | "under" | null;
+};
+
+export type EconomicCaseNormalizationSummary = {
+ rawEmissions: number;
+ normalizedCases: number;
+ executableTotalGoalsCases: number;
+ nonBinaryMarketSignalsPassedThrough: number;
+ duplicateExecutableBuysCollapsed: number;
+ complementarySellsCollapsed: number;
+ sellOnlySignalsDropped: number;
+ unsupportedTotalGoalsSignalsDropped: number;
+};
+
+export type EconomicCaseNormalizationResult = {
+ /**
+ * Signals safe to pass to the next stage. Total-goals entries in this list are
+ * always actual BUY signals; non-binary markets are deliberately untouched.
+ */
+ signals: DetectorSignal[];
+ dispositions: EconomicCaseDisposition[];
+ summary: EconomicCaseNormalizationSummary;
+};
+
+type IndexedSignal = { signal: DetectorSignal; index: number; economicOutcome: "over" | "under" };
+
+function isBinaryTotalOutcome(outcome: CanonicalOutcome): outcome is "over" | "under" {
+ return outcome === "over" || outcome === "under";
+}
+
+function complement(outcome: "over" | "under"): "over" | "under" {
+ return outcome === "over" ? "under" : "over";
+}
+
+function economicOutcome(signal: DetectorSignal): "over" | "under" {
+ const outcome = signal.outcome as "over" | "under";
+ return signal.direction === "buy" ? outcome : complement(outcome);
+}
+
+function economicCaseKey(item: IndexedSignal): string {
+ const { signal } = item;
+ return stableJson({
+ detector: signal.kind,
+ fixtureId: signal.fixtureId,
+ marketKey: signal.market.key,
+ detectedAtTsMs: signal.detectedAtTsMs,
+ observedAtTsMs: signal.observedAtTsMs,
+ economicOutcome: item.economicOutcome
+ });
+}
+
+/**
+ * Convert raw binary Total Goals detector emissions into executable economic
+ * cases without ever synthesizing an order. A Total Goals sell is only an
+ * economic description of buying the complementary token; DetectorSignal does
+ * not carry proof of that token's executable ask, so a sell-only case fails
+ * closed. When the same batch contains the actual complementary BUY, that BUY
+ * is retained and the sell expression is collapsed into it.
+ *
+ * Match Result is three-way and therefore has no one-token complement. Those
+ * signals pass through byte-for-byte and are not deduplicated by this module.
+ */
+export function normalizeExecutableEconomicCases(
+ rawSignals: readonly DetectorSignal[]
+): EconomicCaseNormalizationResult {
+ const dispositions: Array = new Array(rawSignals.length);
+ const retainedIndexes = new Set();
+ const groups = new Map();
+
+ for (const [index, signal] of rawSignals.entries()) {
+ if (signal.market.family !== "total_goals") {
+ retainedIndexes.add(index);
+ dispositions[index] = {
+ signalId: signal.signalId,
+ disposition: "retained",
+ reason: "non_binary_market_passthrough",
+ economicOutcome: null
+ };
+ continue;
+ }
+ if (!isBinaryTotalOutcome(signal.outcome)) {
+ dispositions[index] = {
+ signalId: signal.signalId,
+ disposition: "dropped",
+ reason: "unsupported_total_goals_outcome",
+ economicOutcome: null
+ };
+ continue;
+ }
+ const item: IndexedSignal = { signal, index, economicOutcome: economicOutcome(signal) };
+ const key = economicCaseKey(item);
+ const group = groups.get(key) ?? [];
+ group.push(item);
+ groups.set(key, group);
+ }
+
+ for (const group of groups.values()) {
+ const executableBuy = group.find(({ signal, economicOutcome: outcome }) =>
+ signal.direction === "buy" && signal.outcome === outcome
+ );
+ if (!executableBuy) {
+ for (const item of group) {
+ dispositions[item.index] = {
+ signalId: item.signal.signalId,
+ disposition: "dropped",
+ reason: "sell_only_unproven_executable_ask",
+ economicOutcome: item.economicOutcome
+ };
+ }
+ continue;
+ }
+
+ retainedIndexes.add(executableBuy.index);
+ dispositions[executableBuy.index] = {
+ signalId: executableBuy.signal.signalId,
+ disposition: "retained",
+ reason: "executable_buy_retained",
+ economicOutcome: executableBuy.economicOutcome
+ };
+ for (const item of group) {
+ if (item.index === executableBuy.index) continue;
+ const isDuplicateBuy = item.signal.direction === "buy";
+ dispositions[item.index] = {
+ signalId: item.signal.signalId,
+ disposition: "collapsed",
+ reason: isDuplicateBuy
+ ? "duplicate_executable_buy_collapsed"
+ : "complementary_sell_collapsed_into_executable_buy",
+ economicOutcome: item.economicOutcome
+ };
+ }
+ }
+
+ const completeDispositions = dispositions.map((item, index) => {
+ if (!item) throw new Error(`Missing economic-case disposition for signal index ${index}`);
+ return item;
+ });
+ const signals = rawSignals.filter((_, index) => retainedIndexes.has(index));
+ return {
+ signals,
+ dispositions: completeDispositions,
+ summary: {
+ rawEmissions: rawSignals.length,
+ normalizedCases: signals.length,
+ executableTotalGoalsCases: completeDispositions.filter(
+ (item) => item.disposition === "retained" && item.reason === "executable_buy_retained"
+ ).length,
+ nonBinaryMarketSignalsPassedThrough: completeDispositions.filter(
+ (item) => item.reason === "non_binary_market_passthrough"
+ ).length,
+ duplicateExecutableBuysCollapsed: completeDispositions.filter(
+ (item) => item.reason === "duplicate_executable_buy_collapsed"
+ ).length,
+ complementarySellsCollapsed: completeDispositions.filter(
+ (item) => item.reason === "complementary_sell_collapsed_into_executable_buy"
+ ).length,
+ sellOnlySignalsDropped: completeDispositions.filter(
+ (item) => item.reason === "sell_only_unproven_executable_ask"
+ ).length,
+ unsupportedTotalGoalsSignalsDropped: completeDispositions.filter(
+ (item) => item.reason === "unsupported_total_goals_outcome"
+ ).length
+ }
+ };
+}
diff --git a/src/detectors/types.ts b/src/detectors/types.ts
index 1ca5626..b1cf8fe 100644
--- a/src/detectors/types.ts
+++ b/src/detectors/types.ts
@@ -6,7 +6,10 @@ export type SignalDirection = "buy" | "sell";
export type DetectorSignal = {
signalId: string;
kind: SignalKind;
+ /** Market/source time of the triggering evidence; retained for provenance and ordering. */
detectedAtTsMs: number;
+ /** Knowledge time when Samaritan actually observed and could process the signal. */
+ observedAtTsMs: number;
fixtureId: string;
market: CanonicalMarket;
outcome: CanonicalOutcome;
diff --git a/src/domain/bounded-async-queue.ts b/src/domain/bounded-async-queue.ts
new file mode 100644
index 0000000..fb320b8
--- /dev/null
+++ b/src/domain/bounded-async-queue.ts
@@ -0,0 +1,139 @@
+export type BoundedAsyncQueueState = "open" | "ended" | "failed";
+
+export type BoundedAsyncQueueSnapshot = {
+ label: string;
+ capacity: number;
+ depth: number;
+ highWaterMark: number;
+ overflowCount: number;
+ state: BoundedAsyncQueueState;
+};
+
+export class BoundedAsyncQueueOverflowError extends Error {
+ readonly code = "BOUNDED_ASYNC_QUEUE_OVERFLOW" as const;
+ readonly label: string;
+ readonly capacity: number;
+ readonly highWaterMark: number;
+ readonly overflowCount: number;
+
+ constructor(snapshot: BoundedAsyncQueueSnapshot) {
+ super(
+ `${snapshot.label} overflowed at capacity ${snapshot.capacity} ` +
+ `(high-water ${snapshot.highWaterMark}); processing halted without dropping silently`
+ );
+ this.name = "BoundedAsyncQueueOverflowError";
+ this.label = snapshot.label;
+ this.capacity = snapshot.capacity;
+ this.highWaterMark = snapshot.highWaterMark;
+ this.overflowCount = snapshot.overflowCount;
+ }
+}
+
+type QueueWaiter = {
+ resolve: (result: IteratorResult) => void;
+ reject: (error: unknown) => void;
+};
+
+/**
+ * Single-consumer, non-blocking producer queue. Producers never wait for
+ * capacity: reaching the bound is a fatal, observable condition and queued
+ * values are discarded so a consumer cannot act on a partial stream.
+ */
+export class BoundedAsyncQueue {
+ readonly #items: T[] = [];
+ readonly #label: string;
+ readonly #capacity: number;
+ #waiter: QueueWaiter | null = null;
+ #state: BoundedAsyncQueueState = "open";
+ #failure: unknown = null;
+ #highWaterMark = 0;
+ #overflowCount = 0;
+
+ constructor(options: { label: string; capacity: number }) {
+ if (options.label.trim().length === 0) throw new Error("Bounded queue label cannot be empty");
+ if (!Number.isSafeInteger(options.capacity) || options.capacity <= 0) {
+ throw new RangeError("Bounded queue capacity must be a positive safe integer");
+ }
+ this.#label = options.label;
+ this.#capacity = options.capacity;
+ }
+
+ push(value: T): BoundedAsyncQueueSnapshot {
+ if (this.#state !== "open") {
+ throw new Error(`${this.#label} cannot accept values after it is ${this.#state}`);
+ }
+ if (this.#waiter) {
+ const waiter = this.#waiter;
+ this.#waiter = null;
+ waiter.resolve({ done: false, value });
+ return this.snapshot();
+ }
+ if (this.#items.length >= this.#capacity) {
+ this.#overflowCount += 1;
+ const error = new BoundedAsyncQueueOverflowError({
+ ...this.snapshot(),
+ overflowCount: this.#overflowCount,
+ state: "failed"
+ });
+ this.fail(error);
+ throw error;
+ }
+ this.#items.push(value);
+ this.#highWaterMark = Math.max(this.#highWaterMark, this.#items.length);
+ return this.snapshot();
+ }
+
+ end(): BoundedAsyncQueueSnapshot {
+ if (this.#state !== "open") return this.snapshot();
+ this.#state = "ended";
+ if (this.#waiter) {
+ const waiter = this.#waiter;
+ this.#waiter = null;
+ waiter.resolve({ done: true, value: undefined });
+ }
+ return this.snapshot();
+ }
+
+ /** Stop cleanly and discard backlog, used for explicit cancellation. */
+ stop(): BoundedAsyncQueueSnapshot {
+ if (this.#state === "failed") return this.snapshot();
+ this.#items.splice(0);
+ if (this.#state === "ended") return this.snapshot();
+ return this.end();
+ }
+
+ /** Fail closed: queued values become unusable and the consumer is woken. */
+ fail(error: unknown): BoundedAsyncQueueSnapshot {
+ if (this.#state === "failed") return this.snapshot();
+ this.#state = "failed";
+ this.#failure = error;
+ this.#items.splice(0);
+ if (this.#waiter) {
+ const waiter = this.#waiter;
+ this.#waiter = null;
+ waiter.reject(error);
+ }
+ return this.snapshot();
+ }
+
+ async take(): Promise> {
+ if (this.#items.length > 0) return { done: false, value: this.#items.shift()! };
+ if (this.#state === "failed") throw this.#failure;
+ if (this.#state === "ended") return { done: true, value: undefined };
+ if (this.#waiter) throw new Error(`${this.#label} supports exactly one consumer`);
+ return new Promise>((resolve, reject) => {
+ this.#waiter = { resolve, reject };
+ });
+ }
+
+ snapshot(): BoundedAsyncQueueSnapshot {
+ return {
+ label: this.#label,
+ capacity: this.#capacity,
+ depth: this.#items.length,
+ highWaterMark: this.#highWaterMark,
+ overflowCount: this.#overflowCount,
+ state: this.#state
+ };
+ }
+}
diff --git a/src/domain/filesystem-paths.ts b/src/domain/filesystem-paths.ts
new file mode 100644
index 0000000..72717b3
--- /dev/null
+++ b/src/domain/filesystem-paths.ts
@@ -0,0 +1,97 @@
+import { lstat, realpath, stat } from "node:fs/promises";
+import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
+
+export type FilesystemPathIdentity = {
+ requestedPath: string;
+ canonicalPath: string;
+ comparisonKey: string;
+ inodeKey: string | null;
+};
+
+/**
+ * Resolve every existing symlink component without requiring the final path or
+ * its parent to exist. Broken symlink components fail closed.
+ */
+export async function canonicalFilesystemPath(path: string): Promise {
+ let cursor = resolve(path);
+ const missingSegments: string[] = [];
+ while (true) {
+ try {
+ return resolve(await realpath(cursor), ...missingSegments);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
+ try {
+ if ((await lstat(cursor)).isSymbolicLink()) {
+ throw new Error(`Refusing broken symlink path: ${path}`);
+ }
+ } catch (linkError) {
+ if ((linkError as NodeJS.ErrnoException).code !== "ENOENT") throw linkError;
+ }
+ const parent = dirname(cursor);
+ if (parent === cursor) throw new Error(`Cannot canonicalize path: ${path}`);
+ missingSegments.unshift(basename(cursor));
+ cursor = parent;
+ }
+ }
+}
+
+async function inodeKey(path: string): Promise {
+ try {
+ const metadata = await stat(path, { bigint: true });
+ return `${metadata.dev}:${metadata.ino}`;
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
+ throw error;
+ }
+}
+
+export async function filesystemPathIdentity(path: string): Promise {
+ const canonicalPath = await canonicalFilesystemPath(path);
+ return {
+ requestedPath: path,
+ canonicalPath,
+ // Conservative on case-sensitive filesystems, correct on default APFS and
+ // Windows, and deterministic for two not-yet-created output paths.
+ comparisonKey: canonicalPath.normalize("NFC").toLocaleLowerCase("en-US"),
+ inodeKey: await inodeKey(canonicalPath)
+ };
+}
+
+/** Reject string aliases, symlink aliases, case-fold aliases, and hard links. */
+export async function assertDistinctFilesystemPaths(
+ paths: readonly string[],
+ label: string
+): Promise {
+ const identities = await Promise.all(paths.map(filesystemPathIdentity));
+ const comparisonKeys = new Set();
+ const inodeKeys = new Set();
+ for (const identity of identities) {
+ if (comparisonKeys.has(identity.comparisonKey)) {
+ throw new Error(`${label} paths must be distinct`);
+ }
+ comparisonKeys.add(identity.comparisonKey);
+ if (identity.inodeKey !== null) {
+ if (inodeKeys.has(identity.inodeKey)) {
+ throw new Error(`${label} paths must not be hard links to the same filesystem object`);
+ }
+ inodeKeys.add(identity.inodeKey);
+ }
+ }
+ return identities.map((identity) => identity.canonicalPath);
+}
+
+/** Require the canonical target to remain inside the canonical root. */
+export async function assertFilesystemPathWithin(
+ path: string,
+ root: string,
+ label: string
+): Promise {
+ const [canonicalPath, canonicalRoot] = await Promise.all([
+ canonicalFilesystemPath(path),
+ canonicalFilesystemPath(root)
+ ]);
+ const suffix = relative(canonicalRoot, canonicalPath);
+ if (suffix === "" || suffix.startsWith("..") || isAbsolute(suffix)) {
+ throw new Error(`${label} must be a descendant of ${root}`);
+ }
+}
diff --git a/src/domain/money.ts b/src/domain/money.ts
new file mode 100644
index 0000000..f257918
--- /dev/null
+++ b/src/domain/money.ts
@@ -0,0 +1,16 @@
+export const USD_MICRO_UNITS = 1_000_000;
+
+export type MicroUsd = number & { readonly __brand: "MicroUsd" };
+
+export function microUsd(value: number): MicroUsd {
+ if (!Number.isSafeInteger(value)) {
+ throw new RangeError(`Money must be represented as safe integer USD micro-units; received ${value}`);
+ }
+ return value as MicroUsd;
+}
+
+export function nonnegativeMicroUsd(value: number): MicroUsd {
+ const amount = microUsd(value);
+ if (amount < 0) throw new RangeError(`Money amount must be non-negative; received ${value}`);
+ return amount;
+}
diff --git a/src/exec/paper.ts b/src/exec/paper.ts
new file mode 100644
index 0000000..60dbb6e
--- /dev/null
+++ b/src/exec/paper.ts
@@ -0,0 +1,310 @@
+import type { PolymarketBookEvent } from "../bus/events.js";
+import type { SignalDirection } from "../detectors/types.js";
+import { microUsd, type MicroUsd, USD_MICRO_UNITS } from "../domain/money.js";
+import type { Probability } from "../domain/probability.js";
+
+/** Current Polymarket delay before marketable sports orders are eligible to match. */
+export const POLYMARKET_SPORTS_MARKETABLE_ORDER_DELAY_MS = 1_000 as const;
+
+export type PolymarketFeeParameters = {
+ source: "polymarket_clob_market_info";
+ conditionId: string;
+ feesEnabled: boolean;
+ takerFeeRate: number;
+ feeCurveExponent: number;
+ takerOnly: boolean;
+ minimumOrderSize: number;
+ minimumTickSize: number;
+ fetchedAtTsMs: number;
+};
+
+export type PaperOrderIntent = {
+ lane: "bounty" | "long_run";
+ caseId: string;
+ signalId: string;
+ fixtureId: string;
+ marketKey: string;
+ outcome: string;
+ direction: SignalDirection;
+ stakeMicroUsd: MicroUsd;
+ limitProbability: Probability;
+ availableShares: number;
+};
+
+export type PaperFill = {
+ adapter: "paper";
+ status: "filled" | "partial" | "no_fill";
+ reason: string | null;
+ assetId: string;
+ conditionId: string;
+ direction: SignalDirection;
+ requestedStakeMicroUsd: MicroUsd;
+ grossMicroUsd: MicroUsd;
+ feeMicroUsd: MicroUsd;
+ netConsiderationMicroUsd: MicroUsd;
+ filledShares: number;
+ averagePrice: number | null;
+ bestPrice: number | null;
+ halfSpreadBps: number | null;
+ executableDepthUsd: number;
+ slippageProbabilityBps: number | null;
+ bookObservedTsMs: number;
+ feeParameters: PolymarketFeeParameters;
+};
+
+export type PaperExecutor = {
+ execute(
+ intent: PaperOrderIntent,
+ book: PolymarketBookEvent,
+ fees: PolymarketFeeParameters
+ ): Promise;
+};
+
+type FillAccumulator = {
+ shares: number;
+ grossMicro: number;
+ feeMicro: number;
+};
+
+type BookEvidence = {
+ halfSpreadBps: number | null;
+ executableDepthUsd: number;
+};
+
+function parseSize(value: string): number {
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new RangeError(`Invalid order-book size: ${value}`);
+ return parsed;
+}
+
+export function takerFeePerShare(price: number, fees: PolymarketFeeParameters): number {
+ return fees.feesEnabled ? fees.takerFeeRate * price * (1 - price) : 0;
+}
+
+function floorShares(value: number): number {
+ return Math.floor(value * 1_000_000) / 1_000_000;
+}
+
+function roundedFeeMicro(value: number): number {
+ return Math.ceil(value / 10) * 10;
+}
+
+function tickAligned(price: number, tickSize: number): boolean {
+ return Math.abs(price / tickSize - Math.round(price / tickSize)) < 1e-8;
+}
+
+function fillStatus(requestedMicro: number, usedMicro: number): "filled" | "partial" {
+ return requestedMicro - usedMicro <= Math.max(10, Math.ceil(requestedMicro / 1_000_000))
+ ? "filled"
+ : "partial";
+}
+
+function bookEvidence(intent: PaperOrderIntent, book: PolymarketBookEvent): BookEvidence {
+ const bid = book.bids.length === 0 ? null : Math.max(...book.bids.map((level) => level.price));
+ const ask = book.asks.length === 0 ? null : Math.min(...book.asks.map((level) => level.price));
+ const levels = intent.direction === "buy"
+ ? book.asks.filter((level) => level.price <= intent.limitProbability)
+ : book.bids.filter((level) => level.price >= intent.limitProbability);
+ return {
+ halfSpreadBps: bid === null || ask === null || bid > ask ? null : (ask - bid) / 2 * 10_000,
+ executableDepthUsd: levels.reduce((sum, level) => sum + parseSize(level.size) * level.price, 0)
+ };
+}
+
+export class OrderBookPaperExecutor implements PaperExecutor {
+ async execute(
+ intent: PaperOrderIntent,
+ book: PolymarketBookEvent,
+ fees: PolymarketFeeParameters
+ ): Promise {
+ if (!Number.isSafeInteger(intent.stakeMicroUsd) || intent.stakeMicroUsd <= 0) {
+ throw new RangeError("Paper stake must be a positive integer USD micro-unit amount");
+ }
+ if (!Number.isFinite(intent.availableShares) || intent.availableShares < 0) {
+ throw new RangeError("Available shares must be finite and non-negative");
+ }
+ if (!Number.isFinite(intent.limitProbability) || intent.limitProbability < 0 || intent.limitProbability > 1) {
+ throw new RangeError("Paper limit probability must be between zero and one");
+ }
+ if (
+ book.fixtureId !== intent.fixtureId ||
+ book.market.key !== intent.marketKey ||
+ book.outcome !== intent.outcome ||
+ book.tokenRole !== "canonical"
+ ) {
+ throw new Error("Paper order intent does not match the canonical order book");
+ }
+ if (
+ !Number.isSafeInteger(book.sourceTsMs) ||
+ !Number.isSafeInteger(book.observedTsMs) ||
+ book.sourceTsMs < 0 ||
+ book.observedTsMs < 0
+ ) {
+ throw new Error("Canonical order book has impossible timestamps");
+ }
+ if (book.mappingStatus === "rejected") throw new Error("Rejected mappings cannot be paper executed");
+ if (fees.conditionId !== book.conditionId) throw new Error("Fee metadata does not match the order book");
+ if (!Number.isSafeInteger(fees.fetchedAtTsMs) || fees.fetchedAtTsMs < 0) {
+ throw new RangeError("Fee metadata timestamp must be a non-negative integer");
+ }
+ if (!fees.feesEnabled && fees.takerFeeRate !== 0) {
+ throw new Error("Fee-disabled market supplied a non-zero taker rate");
+ }
+ if (fees.feesEnabled && (!(fees.takerFeeRate > 0) || !Number.isFinite(fees.takerFeeRate))) {
+ throw new Error("Fee-enabled market is missing a valid taker rate");
+ }
+ if (fees.feeCurveExponent !== 1) {
+ throw new Error(`Unsupported fee-curve exponent: ${fees.feeCurveExponent}`);
+ }
+ if (!fees.takerOnly) throw new Error("Paper executor currently supports taker-only fee metadata");
+ if (!(fees.minimumOrderSize > 0) || !Number.isFinite(fees.minimumOrderSize)) {
+ throw new Error("Market is missing a valid minimum order size");
+ }
+ if (!(fees.minimumTickSize > 0) || !Number.isFinite(fees.minimumTickSize)) {
+ throw new Error("Market is missing a valid minimum tick size");
+ }
+ if (book.tickSize !== null && Number(book.tickSize) !== fees.minimumTickSize) {
+ throw new Error("Canonical book tick size disagrees with CLOB market info");
+ }
+ if ([...book.bids, ...book.asks].some((level) => !tickAligned(level.price, fees.minimumTickSize))) {
+ throw new Error("Canonical book contains a price outside the market tick grid");
+ }
+ if (intent.direction === "sell" && intent.availableShares <= 0) {
+ return this.#empty(intent, book, fees, "sell_requires_owned_inventory");
+ }
+ return intent.direction === "buy"
+ ? this.#buy(intent, book, fees)
+ : this.#sell(intent, book, fees);
+ }
+
+ #buy(intent: PaperOrderIntent, book: PolymarketBookEvent, fees: PolymarketFeeParameters): PaperFill {
+ const evidence = bookEvidence(intent, book);
+ const levels = [...book.asks]
+ .sort((left, right) => left.price - right.price)
+ .filter((level) => level.price <= intent.limitProbability);
+ if (levels.length === 0) return this.#empty(intent, book, fees, "no_ask_within_limit");
+ const bestPrice = levels[0]!.price;
+ const filled: FillAccumulator = { shares: 0, grossMicro: 0, feeMicro: 0 };
+ for (const level of levels) {
+ const remainingMicro = intent.stakeMicroUsd - filled.grossMicro - filled.feeMicro;
+ if (remainingMicro <= 11) break;
+ const grossPerShareMicro = level.price * USD_MICRO_UNITS;
+ const feePerShareMicro = takerFeePerShare(level.price, fees) * USD_MICRO_UNITS;
+ const shares = floorShares(Math.min(
+ parseSize(level.size),
+ (remainingMicro - 11) / (grossPerShareMicro + feePerShareMicro)
+ ));
+ if (shares <= 0) continue;
+ const grossMicro = Math.ceil(shares * grossPerShareMicro);
+ const feeMicro = roundedFeeMicro(shares * feePerShareMicro);
+ if (grossMicro + feeMicro > remainingMicro) continue;
+ filled.shares += shares;
+ filled.grossMicro += grossMicro;
+ filled.feeMicro += feeMicro;
+ }
+ if (filled.shares === 0) return this.#empty(intent, book, fees, "insufficient_ask_depth");
+ if (filled.shares < fees.minimumOrderSize) {
+ return this.#empty(intent, book, fees, "below_minimum_order_size");
+ }
+ const averagePrice = filled.grossMicro / USD_MICRO_UNITS / filled.shares;
+ const consideration = filled.grossMicro + filled.feeMicro;
+ return {
+ adapter: "paper",
+ status: fillStatus(intent.stakeMicroUsd, consideration),
+ reason: null,
+ assetId: book.assetId,
+ conditionId: book.conditionId,
+ direction: intent.direction,
+ requestedStakeMicroUsd: intent.stakeMicroUsd,
+ grossMicroUsd: microUsd(filled.grossMicro),
+ feeMicroUsd: microUsd(filled.feeMicro),
+ netConsiderationMicroUsd: microUsd(consideration),
+ filledShares: filled.shares,
+ averagePrice,
+ bestPrice,
+ ...evidence,
+ slippageProbabilityBps: (averagePrice - bestPrice) * 10_000,
+ bookObservedTsMs: book.observedTsMs,
+ feeParameters: fees
+ };
+ }
+
+ #sell(intent: PaperOrderIntent, book: PolymarketBookEvent, fees: PolymarketFeeParameters): PaperFill {
+ const evidence = bookEvidence(intent, book);
+ const levels = [...book.bids]
+ .sort((left, right) => right.price - left.price)
+ .filter((level) => level.price >= intent.limitProbability);
+ if (levels.length === 0) return this.#empty(intent, book, fees, "no_bid_within_limit");
+ const bestPrice = levels[0]!.price;
+ const filled: FillAccumulator = { shares: 0, grossMicro: 0, feeMicro: 0 };
+ let remainingShares = intent.availableShares;
+ for (const level of levels) {
+ const remainingGrossTarget = intent.stakeMicroUsd - filled.grossMicro;
+ if (remainingGrossTarget <= 0 || remainingShares <= 0) break;
+ const shares = floorShares(Math.min(
+ parseSize(level.size),
+ remainingShares,
+ remainingGrossTarget / (level.price * USD_MICRO_UNITS)
+ ));
+ if (shares <= 0) continue;
+ const grossMicro = Math.floor(shares * level.price * USD_MICRO_UNITS);
+ const feeMicro = roundedFeeMicro(shares * takerFeePerShare(level.price, fees) * USD_MICRO_UNITS);
+ filled.shares += shares;
+ filled.grossMicro += grossMicro;
+ filled.feeMicro += feeMicro;
+ remainingShares -= shares;
+ }
+ if (filled.shares === 0) return this.#empty(intent, book, fees, "insufficient_bid_depth");
+ if (filled.shares < fees.minimumOrderSize) {
+ return this.#empty(intent, book, fees, "below_minimum_order_size");
+ }
+ const averagePrice = filled.grossMicro / USD_MICRO_UNITS / filled.shares;
+ return {
+ adapter: "paper",
+ status: fillStatus(intent.stakeMicroUsd, filled.grossMicro),
+ reason: null,
+ assetId: book.assetId,
+ conditionId: book.conditionId,
+ direction: intent.direction,
+ requestedStakeMicroUsd: intent.stakeMicroUsd,
+ grossMicroUsd: microUsd(filled.grossMicro),
+ feeMicroUsd: microUsd(filled.feeMicro),
+ netConsiderationMicroUsd: microUsd(filled.grossMicro - filled.feeMicro),
+ filledShares: filled.shares,
+ averagePrice,
+ bestPrice,
+ ...evidence,
+ slippageProbabilityBps: (bestPrice - averagePrice) * 10_000,
+ bookObservedTsMs: book.observedTsMs,
+ feeParameters: fees
+ };
+ }
+
+ #empty(
+ intent: PaperOrderIntent,
+ book: PolymarketBookEvent,
+ fees: PolymarketFeeParameters,
+ reason: string
+ ): PaperFill {
+ const evidence = bookEvidence(intent, book);
+ return {
+ adapter: "paper",
+ status: "no_fill",
+ reason,
+ assetId: book.assetId,
+ conditionId: book.conditionId,
+ direction: intent.direction,
+ requestedStakeMicroUsd: intent.stakeMicroUsd,
+ grossMicroUsd: microUsd(0),
+ feeMicroUsd: microUsd(0),
+ netConsiderationMicroUsd: microUsd(0),
+ filledShares: 0,
+ averagePrice: null,
+ bestPrice: null,
+ ...evidence,
+ slippageProbabilityBps: null,
+ bookObservedTsMs: book.observedTsMs,
+ feeParameters: fees
+ };
+ }
+}
diff --git a/src/features/engine.ts b/src/features/engine.ts
index 8c645eb..cc988f9 100644
--- a/src/features/engine.ts
+++ b/src/features/engine.ts
@@ -16,6 +16,22 @@ export type FeatureEngineConfig = {
freshnessMaxAgeMs: number;
};
+export type FeatureEngineDiagnostics = {
+ rejectedSourceRegressions: {
+ txline: number;
+ polymarket: number;
+ };
+ rejectedObservedRegressions: {
+ txline: number;
+ polymarket: number;
+ };
+ rejectedInvalidTimestamps: number;
+ sourceObservationDeltaMs: {
+ txline: { minimum: number | null; maximum: number | null };
+ polymarket: { minimum: number | null; maximum: number | null };
+ };
+};
+
export type VelocityFeature = EwmaObservation & {
windowMs: number;
velocity: number | null;
@@ -25,7 +41,10 @@ export type VelocityFeature = EwmaObservation & {
export type FeatureSnapshot = {
triggerEventId: string;
triggerSource: "txline" | "polymarket";
+ /** Market/source clock for the event that caused this snapshot. */
asOfTsMs: number;
+ /** Knowledge clock: when Samaritan actually observed the triggering event. */
+ observedAtTsMs: number;
fixtureId: string;
market: CanonicalMarket;
outcome: CanonicalOutcome;
@@ -110,7 +129,17 @@ function bestAsk(levels: Array<{ price: Probability }>): Probability | null {
export class FeatureEngine {
readonly #states = new Map();
readonly #scores = new Map();
+ readonly #lastObservedTsMs = new Map<"txline" | "polymarket", number>();
readonly #retentionMs: number;
+ readonly #diagnostics: FeatureEngineDiagnostics = {
+ rejectedSourceRegressions: { txline: 0, polymarket: 0 },
+ rejectedObservedRegressions: { txline: 0, polymarket: 0 },
+ rejectedInvalidTimestamps: 0,
+ sourceObservationDeltaMs: {
+ txline: { minimum: null, maximum: null },
+ polymarket: { minimum: null, maximum: null }
+ }
+ };
constructor(readonly config: FeatureEngineConfig) {
if (config.velocityWindowsMs.length === 0 || config.velocityWindowsMs.some((window) => window <= 0)) {
@@ -123,6 +152,7 @@ export class FeatureEngine {
}
ingest(event: CanonicalEvent): FeatureSnapshot[] {
+ if (!this.#acceptEventTimestamp(event)) return [];
if (event.kind === "score.update") {
const scores = this.#scores.get(event.fixtureId) ?? [];
scores.push(event);
@@ -138,10 +168,12 @@ export class FeatureEngine {
return event.outcomes.flatMap((outcome, index) => {
if (outcome.fairProbability === null) return [];
const state = this.#state(event.fixtureId, event.market, outcome.outcome);
+ if (!this.#updateSource(state.consensus, outcome.fairProbability, event.sourceTsMs, "txline")) {
+ return [];
+ }
const crossCheck = rawSum > 0 ? rawImplied[index]! / rawSum : null;
state.devigCrossCheckProbability = crossCheck;
state.devigDiscrepancy = crossCheck === null ? null : Math.abs(outcome.fairProbability - crossCheck);
- this.#updateSource(state.consensus, outcome.fairProbability, event.sourceTsMs);
const previous = state.consensus.series.latest;
if (previous && previous.tsMs === event.sourceTsMs && state.consensus.updateCount > 1) {
const deltaPoint = state.consensus.series.valueAtOrBefore(event.sourceTsMs - 1);
@@ -151,43 +183,84 @@ export class FeatureEngine {
state.cusumDown = cusum.down;
}
}
- return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs)];
+ return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs, event.observedTsMs)];
});
}
if (event.kind === "polymarket.price") {
if (event.tokenRole !== "canonical") return [];
const state = this.#state(event.fixtureId, event.market, event.outcome);
- state.mappingStatus = event.mappingStatus;
- state.polymarketObservation = event.observation;
- if (event.bestBid !== null) state.bestBid = event.bestBid;
- if (event.bestAsk !== null) state.bestAsk = event.bestAsk;
+ const proposedBid = event.bestBid ?? state.bestBid;
+ const proposedAsk = event.bestAsk ?? state.bestAsk;
const value =
- state.bestBid !== null && state.bestAsk !== null
- ? ((state.bestBid + state.bestAsk) / 2 as Probability)
+ proposedBid !== null && proposedAsk !== null
+ ? ((proposedBid + proposedAsk) / 2 as Probability)
: event.price;
- if (value === null) return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs)];
- this.#updateSource(state.polymarket, value, event.sourceTsMs);
- return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs)];
+ if (value === null) {
+ if (this.#isSourceRegression(state.polymarket, event.sourceTsMs, "polymarket")) return [];
+ } else if (!this.#updateSource(state.polymarket, value, event.sourceTsMs, "polymarket")) {
+ return [];
+ }
+ state.mappingStatus = event.mappingStatus;
+ state.polymarketObservation = event.observation;
+ state.bestBid = proposedBid;
+ state.bestAsk = proposedAsk;
+ return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs, event.observedTsMs)];
}
if (event.kind === "polymarket.book") {
if (event.tokenRole !== "canonical") return [];
const state = this.#state(event.fixtureId, event.market, event.outcome);
+ const proposedBid = bestBid(event.bids);
+ const proposedAsk = bestAsk(event.asks);
+ if (proposedBid !== null && proposedAsk !== null) {
+ if (
+ !this.#updateSource(
+ state.polymarket,
+ ((proposedBid + proposedAsk) / 2) as Probability,
+ event.sourceTsMs,
+ "polymarket"
+ )
+ ) {
+ return [];
+ }
+ } else if (this.#isSourceRegression(state.polymarket, event.sourceTsMs, "polymarket")) {
+ return [];
+ }
state.mappingStatus = event.mappingStatus;
state.polymarketObservation = "book";
- state.bestBid = bestBid(event.bids);
- state.bestAsk = bestAsk(event.asks);
- if (state.bestBid !== null && state.bestAsk !== null) {
- this.#updateSource(
- state.polymarket,
- ((state.bestBid + state.bestAsk) / 2) as Probability,
- event.sourceTsMs
- );
- }
- return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs)];
+ state.bestBid = proposedBid;
+ state.bestAsk = proposedAsk;
+ return [this.#snapshot(state, event.eventId, event.source, event.sourceTsMs, event.observedTsMs)];
}
return [];
}
+ diagnostics(): FeatureEngineDiagnostics {
+ return structuredClone(this.#diagnostics);
+ }
+
+ #acceptEventTimestamp(event: CanonicalEvent): boolean {
+ if (
+ !Number.isSafeInteger(event.sourceTsMs) ||
+ !Number.isSafeInteger(event.observedTsMs) ||
+ event.sourceTsMs < 0 ||
+ event.observedTsMs < 0
+ ) {
+ this.#diagnostics.rejectedInvalidTimestamps += 1;
+ return false;
+ }
+ const previous = this.#lastObservedTsMs.get(event.source);
+ if (previous !== undefined && event.observedTsMs < previous) {
+ this.#diagnostics.rejectedObservedRegressions[event.source] += 1;
+ return false;
+ }
+ this.#lastObservedTsMs.set(event.source, event.observedTsMs);
+ const delta = event.observedTsMs - event.sourceTsMs;
+ const deltas = this.#diagnostics.sourceObservationDeltaMs[event.source];
+ deltas.minimum = deltas.minimum === null ? delta : Math.min(deltas.minimum, delta);
+ deltas.maximum = deltas.maximum === null ? delta : Math.max(deltas.maximum, delta);
+ return true;
+ }
+
#sourceState(): SourceState {
return {
series: new RollingSeries(this.#retentionMs),
@@ -237,10 +310,18 @@ export class FeatureEngine {
return state;
}
- #updateSource(source: SourceState, value: Probability, tsMs: number): void {
+ #updateSource(
+ source: SourceState,
+ value: Probability,
+ tsMs: number,
+ sourceName: "txline" | "polymarket"
+ ): boolean {
const previous = source.series.latest;
const added = source.series.add({ tsMs, value });
- if (!added.accepted) return;
+ if (!added.accepted) {
+ this.#diagnostics.rejectedSourceRegressions[sourceName] += 1;
+ return false;
+ }
source.probability = value;
source.sourceTsMs = tsMs;
source.updateCount += 1;
@@ -270,13 +351,25 @@ export class FeatureEngine {
...source.moments.get(windowMs)!.observe(velocity, tsMs)
};
});
+ return true;
+ }
+
+ #isSourceRegression(
+ source: SourceState,
+ tsMs: number,
+ sourceName: "txline" | "polymarket"
+ ): boolean {
+ if (source.sourceTsMs === null || tsMs >= source.sourceTsMs) return false;
+ this.#diagnostics.rejectedSourceRegressions[sourceName] += 1;
+ return true;
}
#snapshot(
state: OutcomeState,
triggerEventId: string,
triggerSource: "txline" | "polymarket",
- asOfTsMs: number
+ asOfTsMs: number,
+ observedAtTsMs: number
): FeatureSnapshot {
const txAge = state.consensus.sourceTsMs === null ? null : asOfTsMs - state.consensus.sourceTsMs;
const pmAge = state.polymarket.sourceTsMs === null ? null : asOfTsMs - state.polymarket.sourceTsMs;
@@ -301,6 +394,7 @@ export class FeatureEngine {
triggerEventId,
triggerSource,
asOfTsMs,
+ observedAtTsMs,
fixtureId: state.fixtureId,
market: state.market,
outcome: state.outcome,
diff --git a/src/harness/build-capture-readiness.ts b/src/harness/build-capture-readiness.ts
new file mode 100644
index 0000000..f3b393e
--- /dev/null
+++ b/src/harness/build-capture-readiness.ts
@@ -0,0 +1,32 @@
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { PAPER_STUDY_MINIMUM_SIGNAL_TO_KICKOFF_MS } from "../config/paper-study.js";
+import { buildCaptureReadiness, renderCaptureReadinessMarkdown } from "./capture-readiness.js";
+
+function argument(name: string, fallback: string): string {
+ const index = process.argv.indexOf(`--${name}`);
+ return index >= 0 && process.argv[index + 1] ? process.argv[index + 1]! : fallback;
+}
+
+const gammaPath = resolve(argument("gamma", "data/live/gamma-discovery/open-world-cup-events.json"));
+const txlinePath = resolve(argument("txline", "samples/fixtures/mainnet-world-cup-fixtures.json"));
+const outputPath = resolve(argument("output", "data/research/capture-readiness.json"));
+const reportPath = resolve(argument("report", "docs/research/capture-readiness.md"));
+const [gammaEvents, txlineFixtures] = await Promise.all([
+ readFile(gammaPath, "utf8").then((value) => JSON.parse(value) as Array>),
+ readFile(txlinePath, "utf8").then((value) => JSON.parse(value) as Array>)
+]);
+const readiness = buildCaptureReadiness({
+ generatedAt: new Date().toISOString(),
+ nowTsMs: Date.now(),
+ requiredLeadMs: 3 * 60 * 60_000,
+ signalCutoffMs: PAPER_STUDY_MINIMUM_SIGNAL_TO_KICKOFF_MS,
+ gammaEvents,
+ txlineFixtures
+});
+await Promise.all([mkdir(dirname(outputPath), { recursive: true }), mkdir(dirname(reportPath), { recursive: true })]);
+await Promise.all([
+ writeFile(outputPath, `${JSON.stringify(readiness, null, 2)}\n`),
+ writeFile(reportPath, renderCaptureReadinessMarkdown(readiness))
+]);
+console.log(JSON.stringify({ outputPath, reportPath, fixtures: readiness.fixtures }, null, 2));
diff --git a/src/harness/build-paired-capture-analysis.ts b/src/harness/build-paired-capture-analysis.ts
new file mode 100644
index 0000000..9a4628e
--- /dev/null
+++ b/src/harness/build-paired-capture-analysis.ts
@@ -0,0 +1,1410 @@
+import { createHash, randomUUID } from "node:crypto";
+import { createReadStream, readFileSync } from "node:fs";
+import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
+import { dirname, relative, resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+import { z } from "zod";
+import { capturedFrameToEnvelope } from "../ingest/sse.js";
+import { decimalLineToMilli } from "../domain/probability.js";
+import { stableJson } from "../domain/json.js";
+import {
+ assertDistinctFilesystemPaths,
+ assertFilesystemPathWithin
+} from "../domain/filesystem-paths.js";
+import { MappingRegistry, type MappingRecord } from "../mapping/registry.js";
+import { readNdjson } from "../replay/files.js";
+import {
+ PAPER_STUDY_MINIMUM_SIGNAL_TO_KICKOFF_MS,
+ PAPER_STUDY_REPLAY_WINDOW_BEFORE_KICKOFF_MS,
+ PAPER_STUDY_TOTAL_SELECTOR_CONFIG
+} from "../config/paper-study.js";
+import {
+ selectMainTotalLine,
+ type TotalLineEvidence
+} from "../research/main-total-selector.js";
+import {
+ validateCaptureConfig,
+ type CaptureConfig
+} from "./capture-config.js";
+import {
+ verifyTerminalCaptureEvidence,
+ type SynchronizedCaptureEvidence
+} from "./run-scheduled-capture.js";
+import {
+ verifiedPairedAnalysisManifestSchema,
+ type SelectedTotalBinding,
+ type VerifiedPairedAnalysisManifest
+} from "./paired-capture-manifest.js";
+import { capturedPaperReplaySource } from "./paper-event-source.js";
+import {
+ capturedPaperIngressCapacity,
+ profileCapturedPaperIngress
+} from "./captured-paper-admission.js";
+
+const HASH_PATTERN = /^[a-f0-9]{64}$/;
+const TXLINE_DEVIG_SUM_MIN = 99.5;
+const TXLINE_DEVIG_SUM_MAX = 100.5;
+
+type JsonRecord = Record;
+
+type Subscription = {
+ assetId: string;
+ outcome: "over" | "under";
+ eventSlug: string;
+ marketId: string;
+ conditionId: string;
+ lineMilli: number;
+};
+
+type MarketGroup = {
+ eventSlug: string;
+ marketId: string;
+ conditionId: string;
+ lineMilli: number;
+ assets: Subscription[];
+};
+
+type BookAssetAccumulator = {
+ assetId: string;
+ outcome: "over" | "under";
+ bookEvents: number;
+ usableDepthSnapshots: number;
+ firstUsableObservedTsMs: number | null;
+ lastUsableObservedTsMs: number | null;
+ latestPreKickoffObservedTsMs: number | null;
+};
+
+type ResolutionEvidence = {
+ conditionId: string;
+ assetIds: string[];
+ winningAssetId: string;
+ winningOutcome: "over" | "under";
+ observedTsMs: number;
+};
+
+type MarketAccumulator = {
+ group: MarketGroup;
+ assets: Map;
+ resolution: ResolutionEvidence | null;
+};
+
+type PolymarketScan = {
+ messages: number;
+ parsedItems: number;
+ books: number;
+ prices: number;
+ resolutions: number;
+ firstObservedTsMs: number;
+ lastObservedTsMs: number;
+ markets: Map;
+};
+
+type TxlineLineEvidence = {
+ lineMilli: number;
+ usableFrames: number;
+ firstObservedTsMs: number | null;
+ lastObservedTsMs: number | null;
+ latestPreKickoffObservedTsMs: number | null;
+};
+
+type TxlineScan = {
+ oddsFrames: number;
+ exactFixtureOddsFrames: number;
+ usableOddsFrames: number;
+ scoreFrames: number;
+ exactFixtureScoreFrames: number;
+ completedExactFixtureScoreFrames: number;
+ finalScore: { homeGoals: number; awayGoals: number };
+ firstOddsObservedTsMs: number;
+ lastOddsObservedTsMs: number;
+ firstScoresObservedTsMs: number;
+ lastScoresObservedTsMs: number;
+ totalsByLine: Map;
+};
+
+type SelectionResult = {
+ selectedTotal: SelectedTotalBinding | null;
+ mappingConfirmed: boolean;
+ mappingRecords: number;
+ missingGates: string[];
+};
+
+export type PairedCaptureAnalysisStatus = "verified" | "verified_capture" | "failed_closed";
+
+export type PairedCaptureAnalysisManifest = VerifiedPairedAnalysisManifest | {
+ schemaVersion: 2;
+ runId: string;
+ checkedAt: string;
+ status: Exclude;
+ fixtureId: string;
+ eventSlug: string;
+ totalsEventSlug: string;
+ captureVerification: {
+ status: "verified" | "failed_closed";
+ terminalEvidenceAvailable: boolean;
+ };
+ selectedTotal: SelectedTotalBinding | null;
+ marketEvidence: unknown[];
+ proof: {
+ algorithm: "sha256";
+ inputCommitment: string;
+ analysisCommitment: string;
+ inputHashes: Record;
+ };
+ admission: {
+ status: "failed_closed";
+ missingGates: string[];
+ };
+ failures: Array<{ code: string; detail: string }>;
+ notes: string[];
+};
+
+export type BuildPairedCaptureAnalysisOptions = {
+ repoRoot: string;
+ captureConfigPath: string;
+ mappingsPath: string;
+ totalEvidencePath: string;
+ outputPath: string;
+ checkedAt?: string;
+};
+
+class CaptureAnalysisError extends Error {
+ constructor(readonly code: string, message: string) {
+ super(message);
+ this.name = "CaptureAnalysisError";
+ }
+}
+
+const subscriptionSchema = z.object({
+ assetId: z.string().min(1),
+ outcome: z.string().min(1),
+ eventSlug: z.string().min(1),
+ teams: z.array(z.string().min(1)).length(2),
+ kickoffMs: z.number().int().positive().safe(),
+ marketId: z.string().min(1),
+ conditionId: z.string().min(1),
+ sportsMarketType: z.string().min(1),
+ line: z.union([z.number(), z.string(), z.null()])
+}).passthrough();
+
+const capturedMessageSchema = z.object({
+ receivedAt: z.string().datetime(),
+ rawPayload: z.string(),
+ parseError: z.string().nullable().optional()
+}).passthrough();
+
+const capturedFrameSchema = z.object({
+ receivedAt: z.string().datetime(),
+ stream: z.enum(["odds", "scores"]),
+ rawFrame: z.string()
+}).passthrough();
+
+function sha256(value: string): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+async function sha256File(path: string): Promise {
+ const hash = createHash("sha256");
+ for await (const chunk of createReadStream(path)) hash.update(chunk as Buffer);
+ return hash.digest("hex");
+}
+
+async function optionalFileHash(path: string): Promise {
+ try {
+ const metadata = await stat(path);
+ if (!metadata.isFile()) return null;
+ return await sha256File(path);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
+ throw error;
+ }
+}
+
+function relativePath(repoRoot: string, path: string): string {
+ return relative(repoRoot, path) || ".";
+}
+
+function verifiedInputHashes(
+ hashes: Record
+): VerifiedPairedAnalysisManifest["proof"]["inputHashes"] {
+ const required = (name: string): string => {
+ const hash = hashes[name];
+ if (hash === null || hash === undefined || !HASH_PATTERN.test(hash)) {
+ throw new Error(`Verified input hash missing: ${name}`);
+ }
+ return hash;
+ };
+ return {
+ captureConfig: required("captureConfig"),
+ txlineFixtureSnapshot: required("txlineFixtureSnapshot"),
+ polymarketEventSnapshot: required("polymarketEventSnapshot"),
+ polymarketTerminalManifest: required("polymarketTerminalManifest"),
+ txlineTerminalManifest: required("txlineTerminalManifest"),
+ subscriptions: required("subscriptions"),
+ polymarketMessages: required("polymarketMessages"),
+ txlineOdds: required("txlineOdds"),
+ txlineScores: required("txlineScores"),
+ mappings: required("mappings"),
+ causalTotalEvidence: required("causalTotalEvidence")
+ };
+}
+
+function asRecord(value: unknown): JsonRecord | null {
+ return value !== null && typeof value === "object" && !Array.isArray(value)
+ ? value as JsonRecord
+ : null;
+}
+
+function finitePositive(value: unknown): boolean {
+ const parsed = typeof value === "string" && value.trim() === "" ? Number.NaN : Number(value);
+ return Number.isFinite(parsed) && parsed > 0;
+}
+
+function usableDepth(value: unknown): boolean {
+ if (!Array.isArray(value) || value.length === 0) return false;
+ return value.some((level) => {
+ const record = asRecord(level);
+ if (!record || !finitePositive(record.size)) return false;
+ const price = Number(record.price);
+ return Number.isFinite(price) && price > 0 && price < 1;
+ });
+}
+
+function isUsableTxlineOdds(record: JsonRecord): boolean {
+ if (
+ record.SuperOddsType !== "1X2_PARTICIPANT_RESULT" &&
+ record.SuperOddsType !== "OVERUNDER_PARTICIPANT_GOALS"
+ ) return false;
+ const expectedNames = record.SuperOddsType === "1X2_PARTICIPANT_RESULT"
+ ? ["part1", "draw", "part2"]
+ : ["over", "under"];
+ if (
+ !Array.isArray(record.PriceNames) || !Array.isArray(record.Prices) || !Array.isArray(record.Pct) ||
+ record.PriceNames.length === 0 || record.Prices.length !== record.PriceNames.length ||
+ record.Pct.length !== record.PriceNames.length
+ ) return false;
+ if (stableJson(record.PriceNames) !== stableJson(expectedNames)) return false;
+ if (!record.Prices.every((value) => typeof value === "number" && Number.isInteger(value) && value > 0)) return false;
+ if (!record.Pct.every((value) => {
+ if (typeof value !== "string" || value.trim() === "") return false;
+ const parsed = Number(value);
+ return Number.isFinite(parsed) && parsed >= 0 && parsed <= 100;
+ })) return false;
+ const sum = record.Pct.reduce((total, value) => total + Number(value), 0);
+ return sum >= TXLINE_DEVIG_SUM_MIN && sum <= TXLINE_DEVIG_SUM_MAX;
+}
+
+function fullTimeTotalLine(record: JsonRecord): number | null {
+ if (record.SuperOddsType !== "OVERUNDER_PARTICIPANT_GOALS") return null;
+ if (record.MarketPeriod !== null && String(record.MarketPeriod ?? "").trim() !== "") return null;
+ const match = String(record.MarketParameters ?? "").match(/(?:^|;)line=(-?\d+(?:\.\d{1,3})?)(?:;|$)/);
+ return match?.[1] ? decimalLineToMilli(match[1]) : null;
+}
+
+function parseFinalScore(record: JsonRecord): { homeGoals: number; awayGoals: number } | null {
+ if (record.Action !== "game_finalised" || record.StatusId !== 100) return null;
+ const score = asRecord(record.Score);
+ const homeTotal = asRecord(asRecord(score?.Participant1)?.Total);
+ const awayTotal = asRecord(asRecord(score?.Participant2)?.Total);
+ const homeGoals = homeTotal?.Goals;
+ const awayGoals = awayTotal?.Goals;
+ if (
+ typeof homeGoals !== "number" || !Number.isInteger(homeGoals) || homeGoals < 0 ||
+ typeof awayGoals !== "number" || !Number.isInteger(awayGoals) || awayGoals < 0
+ ) {
+ throw new CaptureAnalysisError(
+ "invalid_final_score",
+ "Exact-fixture game_finalised frame lacks non-negative integer total goals"
+ );
+ }
+ return { homeGoals, awayGoals };
+}
+
+function expectedOutcome(finalScore: { homeGoals: number; awayGoals: number }, lineMilli: number): "over" | "under" {
+ const totalMilli = (finalScore.homeGoals + finalScore.awayGoals) * 1_000;
+ if (totalMilli === lineMilli) {
+ throw new CaptureAnalysisError("unsupported_total_push", "Selected binary total resolves exactly on its line");
+ }
+ return totalMilli > lineMilli ? "over" : "under";
+}
+
+const captureEvidenceEnvelopeSchema = z.object({
+ evidence: z.object({
+ txlineFixtures: z.string().min(1),
+ polymarketEvents: z.string().min(1)
+ }).passthrough()
+}).passthrough();
+
+function validateStableCaptureConfig(
+ options: BuildPairedCaptureAnalysisOptions,
+ raw: unknown,
+ fixturesText: string,
+ eventsText: string
+): CaptureConfig {
+ const fixtures = JSON.parse(fixturesText) as unknown[];
+ const events = JSON.parse(eventsText) as unknown[];
+ return validateCaptureConfig({
+ repoRoot: options.repoRoot,
+ config: raw,
+ txlineFixtures: fixtures as Parameters[0]["txlineFixtures"],
+ polymarketEvents: events as Parameters[0]["polymarketEvents"],
+ nowTsMs: Number.MAX_SAFE_INTEGER
+ }).config;
+}
+
+function subscriptionGroups(value: unknown, config: CaptureConfig): {
+ groups: MarketGroup[];
+ allAssetCount: number;
+} {
+ const records = z.array(subscriptionSchema).min(1).parse(value);
+ const kickoffTsMs = Date.parse(config.txline.kickoffUtc);
+ const allowedSlugs = new Set([config.polymarket.eventSlug, config.polymarket.totalsEventSlug]);
+ const seenAssets = new Set();
+ const groups = new Map();
+ let mainEventAssets = 0;
+ for (const record of records) {
+ if (!allowedSlugs.has(record.eventSlug)) {
+ throw new CaptureAnalysisError("subscription_slug_mismatch", `Unexpected captured event slug ${record.eventSlug}`);
+ }
+ if (record.teams.join("|") !== `${config.polymarket.home}|${config.polymarket.away}` || record.kickoffMs !== kickoffTsMs) {
+ throw new CaptureAnalysisError("subscription_fixture_mismatch", `Subscription ${record.assetId} changed teams or kickoff`);
+ }
+ if (seenAssets.has(record.assetId)) {
+ throw new CaptureAnalysisError("duplicate_subscription_asset", `Duplicate subscribed asset ${record.assetId}`);
+ }
+ seenAssets.add(record.assetId);
+ if (record.eventSlug === config.polymarket.eventSlug) mainEventAssets += 1;
+ if (record.eventSlug !== config.polymarket.totalsEventSlug || record.sportsMarketType !== "totals") continue;
+ if (record.line === null) {
+ throw new CaptureAnalysisError("total_line_missing", `Total subscription ${record.assetId} has no line`);
+ }
+ const normalizedOutcome = record.outcome.toLocaleLowerCase();
+ if (normalizedOutcome !== "over" && normalizedOutcome !== "under") {
+ throw new CaptureAnalysisError("total_outcome_invalid", `Total subscription ${record.assetId} has outcome ${record.outcome}`);
+ }
+ const lineMilli = decimalLineToMilli(String(record.line));
+ const key = `${record.marketId}|${record.conditionId}|${lineMilli}`;
+ const group = groups.get(key) ?? {
+ eventSlug: record.eventSlug,
+ marketId: record.marketId,
+ conditionId: record.conditionId,
+ lineMilli,
+ assets: []
+ };
+ group.assets.push({
+ assetId: record.assetId,
+ outcome: normalizedOutcome,
+ eventSlug: record.eventSlug,
+ marketId: record.marketId,
+ conditionId: record.conditionId,
+ lineMilli
+ });
+ groups.set(key, group);
+ }
+ if (mainEventAssets <= 0) throw new CaptureAnalysisError("match_result_subscriptions_missing", "No exact match-result assets were subscribed");
+ const totals = [...groups.values()].sort((left, right) =>
+ left.lineMilli - right.lineMilli || left.marketId.localeCompare(right.marketId)
+ );
+ if (totals.length === 0) throw new CaptureAnalysisError("total_subscriptions_missing", "No exact full-time total was subscribed");
+ for (const group of totals) {
+ group.assets.sort((left, right) => left.assetId.localeCompare(right.assetId));
+ const outcomes = group.assets.map((asset) => asset.outcome).sort();
+ if (group.assets.length !== 2 || stableJson(outcomes) !== stableJson(["over", "under"])) {
+ throw new CaptureAnalysisError(
+ "total_outcome_pair_incomplete",
+ `Total ${group.marketId}/${group.conditionId} requires exactly Over and Under assets`
+ );
+ }
+ }
+ return { groups: totals, allAssetCount: seenAssets.size };
+}
+
+function payloadItems(rawPayload: string): JsonRecord[] {
+ if (["ping", "pong"].includes(rawPayload.trim().toLocaleLowerCase())) return [];
+ const parsed = JSON.parse(rawPayload) as unknown;
+ const items = Array.isArray(parsed) ? parsed : [parsed];
+ return items.map((item) => {
+ const record = asRecord(item);
+ if (!record) throw new CaptureAnalysisError("polymarket_payload_invalid", "Captured Polymarket item is not an object");
+ return record;
+ });
+}
+
+function itemAssetIds(item: JsonRecord): string[] {
+ const values = new Set();
+ if (item.asset_id !== undefined) values.add(String(item.asset_id));
+ if (Array.isArray(item.assets_ids)) for (const value of item.assets_ids) values.add(String(value));
+ if (Array.isArray(item.price_changes)) {
+ for (const change of item.price_changes) {
+ const record = asRecord(change);
+ if (record?.asset_id !== undefined) values.add(String(record.asset_id));
+ }
+ }
+ return [...values];
+}
+
+async function scanPolymarket(
+ path: string,
+ groups: readonly MarketGroup[],
+ kickoffTsMs: number
+): Promise {
+ const byAsset = new Map(groups.flatMap((group) => group.assets.map((asset) => [asset.assetId, group] as const)));
+ const markets = new Map();
+ for (const group of groups) {
+ const assets = new Map