diff --git a/docs/workbench/UI-UX-GUIDE.md b/docs/workbench/UI-UX-GUIDE.md index d0092a61..2295d4f5 100644 --- a/docs/workbench/UI-UX-GUIDE.md +++ b/docs/workbench/UI-UX-GUIDE.md @@ -59,13 +59,14 @@ successful proof. ## 4. Current Route Map The route registry in `workbench-ui/src/app/routes.ts` is the source of truth. -Current route count: 13. +Current route count: 14. | Section | Route | Path | Shortcut | Purpose | |---|---|---|---|---| | Converse | Chat | `/chat` | `⌘1` | Execute a normal CORE turn and journal evidence. | | Cognition | Trace | `/trace` | `⌘2` | Inspect turn surfaces, grounding, verdicts, and trace hashes. | | Cognition | Contemplation | `/contemplation` | Palette | Inspect persisted contemplation process traces. | +| Determinism | Tour | `/tour` | Palette | Guided determinism tour: a curated narrative over the real demos with "what this proves / does not prove" honesty cards. | | Determinism | Replay | `/replay` | `⌘3` | Re-execute journaled turns in a sealed fresh runtime and compare envelopes. | | Determinism | Demos | `/demos` | Palette | Run registered determinism demos. | | Evidence | Proposals | `/proposals` | `⌘4` | Review cognition and math proposal evidence. | diff --git a/docs/workbench/data-shapes-v1.md b/docs/workbench/data-shapes-v1.md index 69109a31..6f0e79d3 100644 --- a/docs/workbench/data-shapes-v1.md +++ b/docs/workbench/data-shapes-v1.md @@ -226,6 +226,38 @@ entry, no engine execution. Endpoint: `GET /trace/{turn_id}/bundle`. The Trace route's **Bundle** tab shows the citable digest, a "what this proves / does not prove" honesty note, the reproducer, and a deterministic JSON download. +## DeterminismTour (D1/D2 guided tour) + +```ts +export type TourStepKind = "intro" | "demo" | "payoff"; +export type TourStep = { + step_id: string; + order: number; + kind: TourStepKind; + headline: string; // authored narrative + narrative: string; // authored framing + demo_id: string | null; // a real registered demo + demo_title: string | null; // pulled from the demo spec + what_this_proves: string | null; // pulled from the demo spec + what_this_does_not_prove: string | null;// pulled from the demo spec + route_hint: string | null; // where to go deeper +}; +export type DeterminismTour = { + schema_version: "determinism_tour_v1"; + title: string; + thesis: string; // the provider-agnostic pitch + steps: TourStep[]; +}; +``` + +The first-run, provider-agnostic narrative. Authored headlines/narrative frame +the story, but every demo step is **bound to a real demo**: the honesty cards +(`what_this_proves` / `what_this_does_not_prove`) and `demo_title` are pulled +from the demo spec, never re-authored, and a step referencing a missing demo +fails closed. Read-only. Endpoint: `GET /tour`. Surfaced as the `/tour` route +(Determinism section) — thesis hero + ordered step cards with the honesty cards +and links to the real demos / replay. + --- # Proposal diff --git a/docs/workbench/wave-M-worthiness.md b/docs/workbench/wave-M-worthiness.md index f50f1814..98de2867 100644 --- a/docs/workbench/wave-M-worthiness.md +++ b/docs/workbench/wave-M-worthiness.md @@ -210,6 +210,19 @@ Detailed brief pack: `docs/handoff/wave-M-phaseB-calibration-briefs-2026-06-13.m your own model's claim; watch the deterministic engine decide, refuse, and replay it." The Tool-Authority / Hybrid-Verification demos already embody this; make it the tour's spine. + **D1+D2 implementation note (2026-06-13):** BUILT as the first-class `/tour` + route. `workbench/tour.py::determinism_tour()` is a curated, ordered narrative + bound to the **real** demo registry: intro (the provider-agnostic thesis) → + three demo steps (deductive entailment decides; epistemic truth-state refuses + a wrong proposer; proof-carrying promotion ignores proposer authority) → + payoff (replay-to-the-same-hash + the citable evidence bundle). **Honesty by + construction:** each demo step's `what_this_proves` / `what_this_does_not_prove` + cards are pulled from the real demo spec (never re-authored), and a step that + references a missing demo **fails closed** (`KeyError`) rather than becoming a + dead link — a test asserts both. The spine is the three substrate-capability + demos that exist today (the named Tool-Authority/Hybrid demos are not in the + registry); the thesis carries the bring-your-model framing. Read endpoint + `GET /tour`; route registered in the registry (Determinism section, 14 routes). - **Shareable evidence bundles** — deterministic export of a turn + its trace + replay + calibration verdict as a single citable artifact. Reproducibility *as a deliverable*. diff --git a/tests/test_workbench_tour.py b/tests/test_workbench_tour.py new file mode 100644 index 00000000..bebc8a83 --- /dev/null +++ b/tests/test_workbench_tour.py @@ -0,0 +1,56 @@ +"""D1/D2 guided determinism tour — honesty + drift guards. + +The tour is authored narrative, so the load-bearing guarantee is that it cannot +drift from the real demos: every demo step binds to a registered demo, the +honesty cards come from the demo spec (never re-authored), and a phantom demo id +fails closed rather than becoming a dead link. +""" + +from __future__ import annotations + +import pytest + +from workbench.readers import DEMO_SPECS +from workbench.tour import _build_tour, determinism_tour + + +def test_every_demo_step_binds_to_a_registered_demo() -> None: + tour = determinism_tour() + demo_ids = [step.demo_id for step in tour.steps if step.demo_id is not None] + assert demo_ids, "tour should reference at least one real demo" + for demo_id in demo_ids: + assert demo_id in DEMO_SPECS + + +def test_honesty_cards_come_from_the_spec_not_re_authored() -> None: + # If a tour step re-worded what a demo proves, this fails — the tour can + # never claim more (or less) than the demo it points at. + for step in determinism_tour().steps: + if step.demo_id is None: + continue + spec = DEMO_SPECS[step.demo_id] + assert step.what_this_proves == spec.what_this_proves + assert step.what_this_does_not_prove == spec.what_this_does_not_prove + assert step.demo_title == spec.title + + +def test_non_demo_steps_carry_no_demo_claims() -> None: + for step in determinism_tour().steps: + if step.kind == "demo": + continue + assert step.demo_id is None + assert step.what_this_proves is None + assert step.what_this_does_not_prove is None + + +def test_phantom_demo_fails_closed() -> None: + with pytest.raises(KeyError, match="unknown demo"): + _build_tour((("x", "demo", "h", "n", "no_such_demo", "/x"),)) + + +def test_steps_are_ordered_and_thesis_is_present() -> None: + tour = determinism_tour() + assert tour.schema_version == "determinism_tour_v1" + assert [step.order for step in tour.steps] == list(range(len(tour.steps))) + # D2 provider-agnostic framing is present and explicit. + assert "proposing model" in tour.thesis.lower() or "any model" in tour.thesis.lower() diff --git a/workbench-ui/schema-snapshot.json b/workbench-ui/schema-snapshot.json index 9c901b54..edb71401 100644 --- a/workbench-ui/schema-snapshot.json +++ b/workbench-ui/schema-snapshot.json @@ -166,6 +166,12 @@ "read_only", "scenarios" ], + "DeterminismTour": [ + "schema_version", + "title", + "thesis", + "steps" + ], "EvalLaneSummary": [ "lane", "versions", @@ -347,6 +353,18 @@ "source_path", "source_digest" ], + "TourStep": [ + "step_id", + "order", + "kind", + "headline", + "narrative", + "demo_id", + "demo_title", + "what_this_proves", + "what_this_does_not_prove", + "route_hint" + ], "TurnJournalEntrySchema": [ "turn_id", "timestamp", diff --git a/workbench-ui/src/api/client.ts b/workbench-ui/src/api/client.ts index 1119b7ae..e9fc2530 100644 --- a/workbench-ui/src/api/client.ts +++ b/workbench-ui/src/api/client.ts @@ -24,6 +24,7 @@ import type { CognitivePipelineRecord, FieldEvidence, EvidenceBundle, + DeterminismTour, TurnJournalEntry, TurnJournalSummary, ContemplationRunDetail, @@ -200,6 +201,10 @@ export async function fetchTraceBundle(turnId: number): Promise ); } +export async function fetchTour(): Promise { + return apiFetch("/tour"); +} + export async function fetchAuditEvents( limit?: number, offset?: number, diff --git a/workbench-ui/src/api/queries.ts b/workbench-ui/src/api/queries.ts index 499fdd2c..59dd620e 100644 --- a/workbench-ui/src/api/queries.ts +++ b/workbench-ui/src/api/queries.ts @@ -28,6 +28,7 @@ import { fetchTracePipeline, fetchTraceField, fetchTraceBundle, + fetchTour, fetchTraceTurns, fetchContemplationRun, fetchContemplationRuns, @@ -59,6 +60,7 @@ import type { CognitivePipelineRecord, FieldEvidence, EvidenceBundle, + DeterminismTour, TurnJournalEntry, TurnJournalSummary, EvalLaneSummary, @@ -234,6 +236,15 @@ export function useTraceBundle(turnId?: number | null) { }); } +export function useTour() { + return useQuery({ + queryKey: ["api", "tour"], + queryFn: fetchTour, + staleTime: 60_000, + refetchOnWindowFocus: false, + }); +} + export function useAuditEvents(limit?: number, offset?: number) { return useQuery<{ items: AuditEvent[] }, WorkbenchApiError>({ queryKey: ["api", "audit", "events", limit ?? null, offset ?? null], diff --git a/workbench-ui/src/app/App.tsx b/workbench-ui/src/app/App.tsx index 73db6ea7..b71f4b91 100644 --- a/workbench-ui/src/app/App.tsx +++ b/workbench-ui/src/app/App.tsx @@ -21,6 +21,9 @@ const ContemplationRoute = lazy(() => const ReplayRoute = lazy(() => import("./replay/ReplayRoute").then((module) => ({ default: module.ReplayRoute })), ); +const TourRoute = lazy(() => + import("./tour/TourRoute").then((module) => ({ default: module.TourRoute })), +); const DemoTheaterRoute = lazy(() => import("./demos/DemoTheaterRoute").then((module) => ({ default: module.DemoTheaterRoute, @@ -76,6 +79,7 @@ export const ROUTE_ELEMENTS: RouteElementMap = { chat: lazyRoute(), trace: lazyRoute(), contemplation: lazyRoute(), + tour: lazyRoute(), replay: lazyRoute(), demos: lazyRoute(), proposals: lazyRoute(), diff --git a/workbench-ui/src/app/Shell.test.tsx b/workbench-ui/src/app/Shell.test.tsx index 0ca17541..89b0a324 100644 --- a/workbench-ui/src/app/Shell.test.tsx +++ b/workbench-ui/src/app/Shell.test.tsx @@ -85,11 +85,11 @@ describe("Shell", () => { expect(document.querySelector('[data-density="compact"]')).toBeInTheDocument(); }); - it("LeftNav has exactly 13 items in section-grouped order", () => { + it("LeftNav has exactly 14 items in section-grouped order", () => { renderShell(); const nav = document.querySelector('[data-region="leftnav"]')!; const links = nav.querySelectorAll("a"); - expect(links).toHaveLength(13); + expect(links).toHaveLength(14); const labels = Array.from(links).map((l) => l.textContent); // Grouped by section (Converse → Cognition → Determinism → Evidence → // Discipline → Substrate → Settings), derived from the route registry. @@ -97,6 +97,7 @@ describe("Shell", () => { "Chat", "Trace", "Contemplation", + "Tour", "Replay", "Demos", "Proposals", diff --git a/workbench-ui/src/app/routes.ts b/workbench-ui/src/app/routes.ts index f5172ee5..e4e261a0 100644 --- a/workbench-ui/src/app/routes.ts +++ b/workbench-ui/src/app/routes.ts @@ -105,6 +105,19 @@ export const WORKBENCH_ROUTES: readonly WorkbenchRoute[] = [ keyboardDigit: null, routeConformanceRequired: true, }, + { + id: "tour", + path: "/tour", + routePattern: "tour", + label: "Tour", + description: "Guided determinism tour over the real demos.", + section: "Determinism", + leftNavVisible: true, + commandPaletteVisible: true, + landingRouteAllowed: true, + keyboardDigit: null, + routeConformanceRequired: true, + }, { id: "replay", path: "/replay", diff --git a/workbench-ui/src/app/tour/TourRoute.test.tsx b/workbench-ui/src/app/tour/TourRoute.test.tsx new file mode 100644 index 00000000..242da614 --- /dev/null +++ b/workbench-ui/src/app/tour/TourRoute.test.tsx @@ -0,0 +1,104 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createTestQueryClient } from "../../test/createTestQueryClient"; +import type { DeterminismTour } from "../../types/api"; +import { TourRoute } from "./TourRoute"; + +const TOUR: DeterminismTour = { + schema_version: "determinism_tour_v1", + title: "The Determinism Tour", + thesis: "Bring a claim from any model and watch the engine decide, refuse, and replay it.", + steps: [ + { + step_id: "intro", + order: 0, + kind: "intro", + headline: "Determinism you can check", + narrative: "Each step runs a real demo over pinned fixtures.", + demo_id: null, + demo_title: null, + what_this_proves: null, + what_this_does_not_prove: null, + route_hint: "/demos", + }, + { + step_id: "decide", + order: 1, + kind: "demo", + headline: "Bring a claim — the engine decides", + narrative: "Served only when the pinned engine and an oracle agree.", + demo_id: "deductive_entailment_authority", + demo_title: "Deductive Entailment Authority", + what_this_proves: "CORE serves decisions only when engine and oracle agree.", + what_this_does_not_prove: "It does not claim open-domain theorem proving.", + route_hint: "/demos/deductive_entailment_authority", + }, + { + step_id: "payoff", + order: 2, + kind: "payoff", + headline: "Replay to the same hash", + narrative: "Every decision replays to the same trace hash.", + demo_id: null, + demo_title: null, + what_this_proves: null, + what_this_does_not_prove: null, + route_hint: "/replay", + }, + ], +}; + +function stubFetch(tour: DeterminismTour = TOUR) { + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ ok: true, generated_at: "now", data: tour }), + }), + ), + ); +} + +function renderRoute() { + return render( + + + + + , + ); +} + +describe("TourRoute", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("renders the provider-agnostic thesis and the ordered narrative", async () => { + stubFetch(); + renderRoute(); + + expect(await screen.findByText(/Bring a claim from any model/)).toBeInTheDocument(); + expect(screen.getByText("Determinism you can check")).toBeInTheDocument(); + expect(screen.getByText("Bring a claim — the engine decides")).toBeInTheDocument(); + expect(screen.getByText("Replay to the same hash")).toBeInTheDocument(); + }); + + it("shows honest what-proves / what-does-not-prove cards on demo steps", async () => { + stubFetch(); + renderRoute(); + + expect(await screen.findByText("what this proves")).toBeInTheDocument(); + expect(screen.getByText(/engine and oracle agree/)).toBeInTheDocument(); + expect(screen.getByText("what this does not prove")).toBeInTheDocument(); + expect(screen.getByText(/does not claim open-domain/)).toBeInTheDocument(); + // The demo step links to the real demo route. + expect(screen.getByTestId("tour-link-decide")).toHaveAttribute( + "href", + "/demos/deductive_entailment_authority", + ); + }); +}); diff --git a/workbench-ui/src/app/tour/TourRoute.tsx b/workbench-ui/src/app/tour/TourRoute.tsx new file mode 100644 index 00000000..5bdc7d18 --- /dev/null +++ b/workbench-ui/src/app/tour/TourRoute.tsx @@ -0,0 +1,124 @@ +import { Link } from "react-router-dom"; +import { WorkbenchApiError } from "../../api/client"; +import { useTour } from "../../api/queries"; +import { Panel } from "../../design/components/Panel/Panel"; +import { ErrorState } from "../../design/components/states/ErrorState"; +import { LoadingState } from "../../design/components/states/LoadingState"; +import type { TourStep } from "../../types/api"; + +function errorMessage(error: unknown): string { + return error instanceof WorkbenchApiError ? error.message : "Tour request failed."; +} + +const KIND_LABEL: Record = { + intro: "Start here", + demo: "Live demo", + payoff: "The payoff", +}; + +function StepCard({ step }: { step: TourStep }) { + return ( +
  • +
    + + {step.order + 1} + +
    +
    +
    + + {step.headline} + + + {KIND_LABEL[step.kind]} + +
    +

    + {step.narrative} +

    + + {step.demo_id && step.demo_title ? ( +

    + demo: {step.demo_title} +

    + ) : null} + + {step.what_this_proves ? ( +
    +
    + what this proves +
    +

    + {step.what_this_proves} +

    +
    + ) : null} + + {step.what_this_does_not_prove ? ( +
    +
    + what this does not prove +
    +

    + {step.what_this_does_not_prove} +

    +
    + ) : null} + + {step.route_hint ? ( + + {step.kind === "demo" ? "Run this demo" : "Go there"} → + + ) : null} +
    +
  • + ); +} + +export function TourRoute() { + const tourQuery = useTour(); + + if (tourQuery.isLoading) { + return ; + } + if (tourQuery.isError) { + return ( + + ); + } + const tour = tourQuery.data; + if (!tour) { + return ; + } + + return ( +
    + +
    +
    +

    + the pitch +

    +

    + {tour.thesis} +

    +
    +
      + {tour.steps.map((step) => ( + + ))} +
    +
    +
    +
    + ); +} diff --git a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx index d6b0fdbc..0a84a182 100644 --- a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx +++ b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx @@ -52,11 +52,11 @@ describe("CommandPalette keyboard contract", () => { expect(screen.getByRole("button", { name: "Open Trace" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Open Replay" })).toBeInTheDocument(); - // One Navigate command per palette-visible route (13), derived from the - // route registry — Demos, Calibration, and Contemplation are included (the - // prior hand-maintained list of 10 dropped them). + // One Navigate command per palette-visible route (14), derived from the + // route registry — Demos, Calibration, Contemplation, and Tour are included + // (the prior hand-maintained list of 10 dropped them). const items = dialog.querySelectorAll('[role="option"]'); - expect(items.length).toBe(13); + expect(items.length).toBe(14); const lastIndex = items.length - 1; // Initially first item (index 0) is focused — check aria-selected diff --git a/workbench-ui/src/types/api.ts b/workbench-ui/src/types/api.ts index 6d8a2537..7614f4a9 100644 --- a/workbench-ui/src/types/api.ts +++ b/workbench-ui/src/types/api.ts @@ -140,6 +140,33 @@ export interface EvidenceBundle { bundle_digest: string; } +export type TourStepKind = "intro" | "demo" | "payoff"; + +/** + * D1/D2 guided determinism tour — a curated narrative over the real demos. + * `headline`/`narrative` are authored framing; for demo steps the honesty cards + * are pulled from the real demo spec, never re-authored. + */ +export interface TourStep { + step_id: string; + order: number; + kind: TourStepKind; + headline: string; + narrative: string; + demo_id: string | null; + demo_title: string | null; + what_this_proves: string | null; + what_this_does_not_prove: string | null; + route_hint: string | null; +} + +export interface DeterminismTour { + schema_version: "determinism_tour_v1"; + title: string; + thesis: string; + steps: TourStep[]; +} + export type LeewayLicense = "PROPOSE" | "SERVE" | "blocked" | "unknown"; export type ClaimDisclosure = "approximate" | "verified" | "proposal_only" | "none"; diff --git a/workbench/api.py b/workbench/api.py index f97bd5b0..4e67601e 100644 --- a/workbench/api.py +++ b/workbench/api.py @@ -21,6 +21,7 @@ from core.epistemic_state import ( from workbench import calibration, readers from workbench.journal import DEFAULT_JOURNAL_DIR, TurnJournal, TurnJournalEntry from workbench.evidence_bundle import build_evidence_bundle +from workbench.tour import determinism_tour from workbench.field_evidence import ( field_evidence_from_journal_entry, field_evidence_from_result, @@ -284,6 +285,8 @@ class WorkbenchApi: ) if method == "GET" and path == "/demos": return ApiResponse(200, ok({"items": readers.list_demos()})) + if method == "GET" and path == "/tour": + return ApiResponse(200, ok(determinism_tour())) if method == "POST" and path.endswith("/run") and path.startswith("/demos/"): demo_id = unquote(path.removeprefix("/demos/").removesuffix("/run")) try: diff --git a/workbench/schemas.py b/workbench/schemas.py index 8006555b..22f09da7 100644 --- a/workbench/schemas.py +++ b/workbench/schemas.py @@ -203,6 +203,48 @@ class EvidenceBundle: bundle_digest: str +TourStepKind = Literal["intro", "demo", "payoff"] + + +@dataclass(frozen=True, slots=True) +class TourStep: + """One step of the guided determinism tour. + + ``headline`` / ``narrative`` are the authored, provider-agnostic framing. + For ``kind == "demo"`` steps the honesty cards (``what_this_proves`` / + ``what_this_does_not_prove``) and ``demo_title`` are pulled from the REAL + demo spec at build time — never re-authored — so the tour cannot claim more + than the demo it points at. ``route_hint`` is where to go deeper. + """ + + step_id: str + order: int + kind: TourStepKind + headline: str + narrative: str + demo_id: str | None + demo_title: str | None + what_this_proves: str | None + what_this_does_not_prove: str | None + route_hint: str | None + + +@dataclass(frozen=True, slots=True) +class DeterminismTour: + """D1/D2 guided determinism tour — a curated narrative over real demos. + + The ``thesis`` is the provider-agnostic pitch: bring a claim from any model + and watch the deterministic engine decide, refuse, and replay it — proposer + authority ignored. ``steps`` are ordered and each demo step is bound to a + real entry in the demo registry (fail-closed if a demo id is missing). + """ + + schema_version: Literal["determinism_tour_v1"] + title: str + thesis: str + steps: list[TourStep] = field(default_factory=list) + + @dataclass(frozen=True, slots=True) class ChatTurnResult: prompt: str diff --git a/workbench/tour.py b/workbench/tour.py new file mode 100644 index 00000000..40ef2995 --- /dev/null +++ b/workbench/tour.py @@ -0,0 +1,119 @@ +"""D1/D2 guided determinism tour — a curated narrative over the real demos. + +The tour is the first-run, provider-agnostic story: bring a claim from any +model and watch the deterministic engine decide it, refuse it, or replay it — +proposer authority ignored. Each demo step is BOUND to a real entry in the +demo registry: the honesty cards (``what_this_proves`` / +``what_this_does_not_prove``) and the demo title are pulled from the spec, never +re-authored, so the tour can never claim more than the demo it points at. A +step that references a missing demo is a fail-closed error, not a dead link. +""" + +from __future__ import annotations + +from workbench.readers import DEMO_SPECS +from workbench.schemas import DeterminismTour, TourStep, TourStepKind + +_THESIS = ( + "CORE is the deterministic engine the opaque transformer defines itself " + "against. Bring a claim — from any model, including your own — and watch the " + "engine decide it, refuse it, or replay it to the same hash. The proposing " + "model's authority is ignored; only verified evidence promotes." +) + +# (step_id, kind, headline, narrative, demo_id, route_hint) +_TourPlanRow = tuple[str, TourStepKind, str, str, str | None, str | None] + +_TOUR_PLAN: tuple[_TourPlanRow, ...] = ( + ( + "intro", + "intro", + "Determinism you can check", + "These are not screenshots or animations. Each step runs a real demo " + "over pinned fixtures, end to end. Watch the engine apply discipline you " + "can re-run and verify yourself.", + None, + "/demos", + ), + ( + "decide", + "demo", + "Bring a claim — the engine decides", + "A formal claim is served as entailed, refuted, or unknown only when the " + "pinned ROBDD engine and an independent oracle agree. Disagree, and it " + "refuses. The proposer never gets to assert the answer.", + "deductive_entailment_authority", + "/demos/deductive_entailment_authority", + ), + ( + "refuse", + "demo", + "A wrong proposer gets refused", + "When a proposer tries to smuggle an unsupported truth-state, the engine " + "rejects it. You are watching a wrong answer get refused rather than " + "served — the discipline that makes wrong=0 real.", + "epistemic_truth_state", + "/demos/epistemic_truth_state", + ), + ( + "promote", + "demo", + "Only a verified certificate promotes", + "Promotion into the vault happens only through an owner-applied verified " + "certificate. Proposer status is ignored entirely — authority lives in " + "the evidence, not the model.", + "proof_carrying_promotion", + "/demos/proof_carrying_promotion", + ), + ( + "payoff", + "payoff", + "Replay to the same hash, export the proof", + "Every decision above replays to the same trace hash, and any turn " + "exports as a content-addressed evidence bundle you can cite and " + "reproduce. Determinism isn't a claim here; it's a deliverable.", + None, + "/replay", + ), +) + + +def _build_tour(plan: tuple[_TourPlanRow, ...]) -> DeterminismTour: + steps: list[TourStep] = [] + for order, (step_id, kind, headline, narrative, demo_id, route_hint) in enumerate(plan): + demo_title = proves = not_proves = None + if demo_id is not None: + spec = DEMO_SPECS.get(demo_id) + if spec is None: + raise KeyError( + f"determinism tour references unknown demo: {demo_id}" + ) + demo_title = spec.title + proves = spec.what_this_proves + not_proves = spec.what_this_does_not_prove + steps.append( + TourStep( + step_id=step_id, + order=order, + kind=kind, + headline=headline, + narrative=narrative, + demo_id=demo_id, + demo_title=demo_title, + what_this_proves=proves, + what_this_does_not_prove=not_proves, + route_hint=route_hint, + ) + ) + return DeterminismTour( + schema_version="determinism_tour_v1", + title="The Determinism Tour", + thesis=_THESIS, + steps=steps, + ) + + +def determinism_tour() -> DeterminismTour: + """Build the curated determinism tour, bound to the live demo registry.""" + + return _build_tour(_TOUR_PLAN)