core/docs/decisions/ADR-0098-demo-composition-contract.md
Shay 57fa078aaa docs: mark ADR-0094/0095/0098/0099 accepted
Sibling reconciliation PR to #104. The four ADRs explicitly called out as
the 'current implementation frontier' in PR #104 are already implemented
to the same evidence bar as the eight ADRs that PR accepted:

- ADR-0094: teaching/source.py + proposal schema widening + migration
  script; tests/test_proposal_source.py green
- ADR-0095: teaching/from_miner.py + miner_loop_closure lane;
  SHA-pinned in scripts/verify_lane_shas.py; tests/test_miner_proposals.py
  green
- ADR-0098: core/demos/contract.py + adapter surface + demo_composition
  lane; SHA-pinned; tests/test_demo_composition.py green
- ADR-0099: core/demos/showcase.py + public_demo lane;
  SHA-pinned; tests/test_public_showcase.py green

Three of four lanes are SHA-pinned in CI (a stricter bar than several
already-accepted ADRs). Local pytest run: 85/85 passed across the four
tests/test_*.py files in 17s.

Also refreshes docs/decisions/README.md:
- flips the four table rows to Accepted (2026-05-22)
- rewrites the 'Current frontier' section now that no ADR-0091..0102
  entry is unimplemented
- enumerates candidate next directions (curriculum proposals,
  language-specific holdout splits, expert-demo ratification)

Docs-only change; no runtime code touched.
2026-05-22 07:09:14 -07:00

5.6 KiB

ADR-0098 — Demo Composition Contract

Status: Accepted Date: 2026-05-21 Accepted: 2026-05-22 Author: CORE agents + reviewers


Acceptance evidence

Accepted after the demo composition contract was implemented as a typed, deterministic adapter layer with a SHA-pinned lane:

  • core/demos/contract.py defines the typed DemoScene / DemoArtifact / CompositionResult schema and the contract surface that adapters must satisfy.
  • core/demos/audit_tour_adapter.py and core/demos/tour_adapters.py wrap existing demos (audit-tour, register-tour, anchor-lens-tour) without reimplementation or subprocess stdout parsing.
  • evals/demo_composition/runner.py and evals/demo_composition/contract.md define the composition lane; evals/demo_composition/results/v1_dev.json is the canonical report.
  • tests/test_demo_composition.py exercises contract enforcement, adapter determinism, and rejection of non-deterministic / mutating adapters.
  • scripts/verify_lane_shas.py pins demo_composition at SHA 27d838241bf3ed9e15d0e918ec6d89a823494d7e17c2dab9777825af7188f20f; verified locally and by the lane-shas workflow on main.

Context

CORE has shipped a growing set of operator-facing demos:

  • core demo audit-tour (ADR-0042)
  • core demo anti-regression (ADR-0055)
  • core demo learning-loop (ADR-0056)
  • core demo register-tour (ADR-0072)
  • core demo anchor-lens-tour (ADR-0073d)
  • core demo orthogonality-tour (ADR-0074)
  • core bench --suite teaching-loop (ADR-0057)

Each one is correct in isolation. Each one stands as its own evidence of a specific invariant. The problem the next ADR (ADR-0099 public showcase) faces is that there is no shared contract that lets one demo safely embed another.

Without a contract, the showcase has two bad options:

  1. Reimplement portions of each demo inline (drift, duplication, doctrine violation).
  2. Subprocess-spawn each demo and parse stdout (fragile, breaks the trace-hash discipline).

The right answer is a small protocol that the existing demos retrofit to, and that the showcase consumes.


Decision

Introduce DemoCommand as a typed protocol. Existing demos are retrofitted to it in the same PR (mechanical, small). Future demos implement it from the start.

Protocol

class DemoCommand(Protocol):
    demo_id: str               # stable identifier, kebab-case
    claim_contract_version: int  # currently 1

    def run(self, *, output_dir: Path, seed: int | None = None) -> DemoResult: ...

@dataclass(frozen=True, slots=True)
class DemoResult:
    demo_id: str
    claims: tuple[Claim, ...]
    evidence: Mapping[str, str]   # claim_id -> evidence locator (path or sha)
    all_claims_supported: bool
    json_path: Path
    trace_features: Mapping[str, str]  # canonical, for showcase composition

@dataclass(frozen=True, slots=True)
class Claim:
    claim_id: str
    statement: str
    supported: bool
    evidence_locator: str

Rules

  1. Deterministic JSON. Two runs with the same inputs and seed produce byte-identical json_path contents. HTML may differ in formatting; JSON is the truth-path.
  2. No global state mutation. A demo's run() may not mutate process-global registries (runtime singletons, telemetry sinks attached at module load, environment variables outside its own scope). Demos that need a telemetry sink attach a local one and detach it before returning.
  3. Declared output paths only. A demo writes only under output_dir. Path traversal rejected via safe_pack_id-class sanitization.
  4. Composability is read-only. A composing demo (the showcase) may read another demo's DemoResult but never mutates it.

Retrofit scope

Each shipped demo gains a thin adapter in core/commands/demo_<name>.py that conforms to DemoCommand. The adapter does not change demo behavior; it wraps the existing entry point and produces a DemoResult.

What this ADR does not do

  • Does not change demo behavior.
  • Does not change demo CLI surface. core demo audit-tour runs the same way; the protocol is internal.
  • Does not introduce a registry. Demos remain discoverable via the existing CLI subparser.

Invariant

demo_composition_no_side_effects — a grep gate on the showcase's import graph refuses any symbol that mutates runtime singletons or attaches telemetry sinks at module load. The protocol contract is enforced by structure, not by hope.

demo_json_byte_equality — for each demo retrofitted under this ADR, running it twice with identical inputs produces byte-identical JSON. CI lane verifies.


Lane

evals/demo_composition/ (new):

  • positive: each retrofitted demo runs twice → identical JSON
  • negative: a deliberately stateful test fixture → composition detector rejects it
  • composition: showcase reads two demo results → produces composite claim set without mutating either

Trust Boundary

Demos write only to operator-specified output_dir. Path traversal rejection inherits from ADR-0051. No dynamic imports. No network. No shell.


Consequences

  • ADR-0099 public showcase becomes mechanically possible without reimplementing demo logic.
  • Future demos cost less: implement the protocol once, gain composability for free.
  • The shipped demos gain a small adapter layer but no behavioral change.

PR Checklist

  • Capability added: composition protocol for demos.
  • Invariants proved: demo_composition_no_side_effects, demo_json_byte_equality.
  • Lane proving it: evals/demo_composition/.
  • Hidden normalization / stochastic fallback / approximate recall / unreviewed mutation: none.
  • Trust boundary: demos write only under declared output paths; no global state mutation.