# Widget Contract Reconciliation **Status:** living document · updated alongside each spec / engine / UI release **Companion to:** `STORYTANGL_WIDGET_VOCAB.md` v1.5 **Companion to:** `bundles//EXTENSIONS.md` (Tier P3 genre layers) This document tracks **implementation status** across the three layers named in spec §0.7: | Layer | Document | Role | |---|---|---| | **L1 — UI Vocabulary** | `STORYTANGL_WIDGET_VOCAB.md` + `bundles//EXTENSIONS.md` | Target-truth. What data shapes the player-facing client needs. The spec evolves here; everything else chases. | | **L2 — API Transport** | `API_SPEC.md` (forthcoming; derived from this doc) | REST endpoints (and other wire transport) routing L1 needs to L3 capabilities. Optional layer — CLI ports skip it. | | **L3 — Engine Capabilities** | `ENGINE_CAPABILITIES.md` (forthcoming; derived from this doc) | Python callables that produce the data L1 wants. The current engine's actual surface. | **The spec is target-truth.** This document is the honest reality check. The two are intentionally allowed to disagree during a settling phase — that's how the inversion (UI-led design, backend chases) works. --- ## How to read this doc Each surface in the spec gets a row in one of the §-numbered tables below. The four status columns per row: | Column | What it tracks | |---|---| | **Spec tier (L1)** | `Tier S` / `Tier P1` / `Tier P2` / `Tier P3` per the vocab spec. The contract commitment. | | **Reference clients** | What `apps/web/` and the reference CLI/Tk ports ship today. Values: `done`, `partial`, `not_started`, `n/a`. | | **Engine backend (L3)** | What `engine/` ships today. Values: same as L2, plus `untyped` (works but with `dict[str, Any]` rather than typed Pydantic). | | **Plan** | One-line forward direction. Empty when the row is settled across columns. | A row is **settled** when all three implementation columns match the spec's tier commitment. A row with mismatched columns is in **negotiation** — that's fine; the doc just makes the gap visible. **Sequence of work, per the inversion strategy.** UI vocabulary leads (commit the target shape in the spec), the reference UI catches up next (against fixtures, even with dummy data), then the engine + API expose capabilities that match. The CLI port skips L2 entirely and consumes L3 directly via an in-process shim. --- ## §A — Tier S surfaces (core contract) These are the surfaces v1.5 marks Tier S — committed as stable target contract. Most are already shipping in the reference UI and engine; the negotiation is mostly about typed-shape graduations. | Surface | Spec tier | Reference UI | Engine backend | Plan | |---|---|---|---|---| | `ContentFragment` shape | S | done | done | — | | `AttributedFragment` shape | S | done | done | — | | `MediaFragment` shape | S | done | done | — | | `GroupFragment` shape (`scene`, `dialog`, `overlay`, `status_sidecar`) | S | done | done | — | | `KvFragment` shape | S | done (`KvRow[]`) | done (`list[KvRow]`) | tuple-row fixtures migrated in typed accepts / KvRow PR | | `ChoiceFragment` shape (frame only) | S | done | done | — | | `ControlFragment` (`update` / `delete`) | S | done | done | — | | `UserEventFragment` | S | done | done | — | | `ProjectedState.sections` with five `value_type`s | S | done | done | — | | `metadata.info_affordances: list[InfoAffordance]` | S | done (web + CLI `?`/slash floor) | done (typed; gathered through service-info dispatch) | sandbox and credentials fixtures cover advertised channels | | `InfoAffordance.query` optional dict (opaque descriptor) | S | done | done | bundles choose descriptor contents; clients hand it back | | `metadata.info_state: InfoState` typed | S | done (nested type + dirty-kind cache hints) | partial (typed; v1 conservatively marks advertised kinds dirty) | add finer dirty tracking only when a client needs caching | | `/story/info` accepts `kind`/`kinds` + `query` params | S | done client-side | done (same endpoint; singular/plural kind filters + query descriptor routing) | add bundle-specific providers as worlds need them | | §1.6 Info channels | S | done (info pills + CLI floor) | done | keep rich renderers optional; `ProjectedState` fallback remains required | | `PresentationHints` (style_name, style_tags, style_dict, icon) | S | partial (basic style/icon fields) | done | audit aliases and long-tail hints before treating complete | | Bundle customization / presentation profiles | S target | partial (docs + older world `ui_config`) | not_started | keep advisory; requires world-info catalog before conformance | | §5.1 Decision Legibility Contract | S | partial (JSON harness covers fixtures/sequences) | n/a (contract; not a capability) | expand field coverage as new decision surfaces promote | | §5.2 Time Parity Rule (visual ritual skip, media advance) | S | partial (JSON harness covers readable fallbacks) | n/a | browser timing/skip E2E remains later | | §5.3 Input Parity Rule (drag fallback, hotkey numbers) | S | partial (JSON harness covers fixture submit floors) | n/a | extend toward renderer-specific drag/keyboard fallbacks | | §0.2 CLI Floor Rule | S | n/a | n/a (gating rule on PRs) | wire into CI for Tier S graduations | --- ## §B — Tier P1 surfaces (target for next engine epoch) These are committed target contract but require additive engine work. Each row should land as a single PR-shaped change unless the surface is small enough to settle with its immediate neighbors. | Surface | Spec tier | Reference UI | Engine backend | Plan | |---|---|---|---|---| | Typed `PiecesAccepts` (was `tokens`) | P1 | done (kind `pieces` + typed TS shape) | done (`Accepts` union) | expand conformance cases only as new widgets land | | Typed `PickAccepts`, `TextAccepts`, `QuantityAccepts`, `RawCommandAccepts` | P1 | done | done (`Accepts` union) | keep legacy web `payload_type` path as local UI compatibility until fixtures stop using it | | Typed `PlaceAccepts` (including optional `edge_ref`) | P1 | partial (`edge_ref` not rendered) | done (`Accepts` union) | add `edge_ref` fixture when route/network MVP lands | | Typed `ComposeAccepts` | P1 | partial (web nested renderer + CLI/Tk inspection fixture) | done (`Accepts` union) | harden layout and add broader part combinations as worlds emit them | | Typed `UIHints` | P1 | partial (documented + ad-hoc keys) | done (`UIHints`, extra-allow) | tighten named fields when more worlds use them | | Typed `Blocker` (replaces dict blockers) | P1 | done (typed + rendered) | untyped | engine PR | | Typed `InterpretationFragment` | P1 | done (renders `result` / `text` / `message`) | untyped | engine PR | | `cost_previews: list[CostPreview]` (plural) | P1 | done (choice display) | not_started | engine PR | | `metadata.grammar` typed sub-key | P1 | done | partial (string-keyed dict) | engine PR for `GrammarHint` Pydantic model | | HTTP body field `edge_id` | P1 | done | done | — | | §1.5 Cursors and journal channels (per-channel envelopes) | P1 | n/a (single cursor) | n/a (single cursor) | wait for MVP author needing multi-cursor (Discord-bot bundle, Lost Worlds gamebook) | **Block on Tier P1 graduation to Tier S:** the CLI reference port (`engine/contrib/conformance/cli_reference_port.py`) must implement each surface before its row can promote. Until then they stay P1. --- ## §C — Tier P2 surfaces (interactive surface vocabulary) These are committed target contract but larger, and several depend on the §7.4 predicate-registration protocol (an explicit open question in the spec). | Surface | Spec tier | Reference UI | Engine backend | Plan | |---|---|---|---|---| | `PieceFragment` (core shape) | P2 | partial (basic web widget + carwars wireframes) | partial (untyped equivalent) | engine PR: typed `PieceFragment` with required `hints.label_text` | | `PieceFragment.realized` + `cost` (offers) | P2 | done in carwars catalog fixtures | not_started | engine PR for typed lifecycle; bundle MVP needed for shop semantics | | `PieceFragment.owner` | P2 (proposal fixture) | not_started | not_started | wait for multi-cursor MVP | | `PieceFragment.position` | P2 (proposal fixture) | not_started | not_started | wait for grid/hex bundle MVP (Patchwork, Carcassonne) | | `PieceFragment.available` + `unavailable_reason` | P2 | done | not_started | engine PR with `PieceFragment` graduation | | `group_type="zone"` with `ZoneConstraints` | P2 | partial (basic web widget + carwars wireframes) | not_started | engine PR: typed `GroupFragment.constraints`; bundle MVP for capacity | | `ZoneLayoutHints` (orientation, fan, grid, hex, graph, floorplan) | P2 | partial (orientation/grid/fan) | not_started | engine PR; render-port catches up as needed | | `GraphLayout.edges` (first-class adjacencies with UIDs) | P2 (proposal fixture) | not_started | not_started | wait for network/route bundle MVP (Ticket-to-Ride-shaped) | | `PlaceAccepts.edge_ref` fixture coverage | P2 pressure fixture | not_started | not_started | P1 typed field exists in the target; graph-route fixture remains gated on a route/network MVP | | `RollFragment` (typed) | P2 | partial (basic rendering in carwars) | not_started | engine PR; CLI reference port renders outcome word + narrative | | `RitualHints` (skip_label, auto_skip_after_seen, allow_replay, duration_ms) | P2 | partial | not_started | engine PR with `RollFragment` graduation | | `predicate_ref` registration protocol | §7.4 OPEN | n/a | n/a | **highest-leverage open question**; awaiting MVP author | | `visibility="hidden" \| "owner_only" \| "public"` | P2 | partial | not_started | engine PR — single-cursor today, audience-list extension when multi-cursor lands | | `visibility: list[ParticipantId]` (audience lists) | P2 (proposal fixture) | not_started | not_started | wait for team-game MVP | --- ## §D — Tier P3 genre extension surfaces These are advisory genre conventions layered on top of v1.5. They do not create new core fragment types, accepts kinds, or value types. A generic client remains conforming when it renders the underlying v1.5 surfaces and ignores these enrichments. | Surface | Spec tier | Reference UI | Engine backend | Plan | |---|---|---|---|---| | `bundles/carwars/EXTENSIONS.md` | P3 | docs + v1.5 wireframe | n/a | keep drag optional; no garage-specific core widgets | | `bundles/credentials/EXTENSIONS.md` | P3 | docs + v1.5 wireframe | n/a | packet / finding / disposition treatments remain genre enrichments | | `bundles/training/EXTENSIONS.md` | P3 | docs + v1.5 wireframe | n/a | study previews, stat checks, and inventory unlocks remain genre enrichments | | `bundles/elefant_hunt/EXTENSIONS.md` | P3 | docs + v1.5 wireframe | n/a | journal-as-story transcript proof; no new surface beyond graph zones / rolls | | `ui_hints.stat_check` | P3 | docs + v1.5 wireframe | bundle-specific | optional badge or CLI suffix; backend remains authoritative | | `ui_hints.validity_check` | P3 | docs + v1.5 wireframe | bundle-specific | optional preview over mediation choices; fallback to ordinary choice text | | `ui_hints.encounter_check` | P3 | docs + v1.5 wireframe | bundle-specific | optional risk badge / suffix; fallback to `ui_hints.emphasis` + prose | | `ui_hints.drag` | P3 | docs + v1.5 wireframe | n/a | optional enrichment; click-pick fallback remains required by §5.3 | | Genre transcript examples | P3 diagnostic | v1.5 wireframe samples | n/a | add executable transcripts when demo worlds stabilize; diagnostic, not gating | | `_common/EXTENSIONS.md` | deferred | n/a | n/a | do not create until a fourth cross-genre pattern is not already covered by core v1.5 | --- ## §E — API transport (Layer 2) The API surface is intentionally underspecified at the contract level — the spec commits to the data shapes (L1), and the API chooses how to route them. This table is what the engine team implements. | Endpoint | Method | Spec target | Current engine | Plan | |---|---|---|---|---| | `/story/do` | POST | accepts `ChoiceRequest{edge_id, payload}`; returns `RuntimeEnvelope` | accepts `{edge_id, payload}` | type response | | `/story/update` | GET | returns `RuntimeEnvelope` typed | partial | type response | | `/story/info` | GET | accepts `kind?`, `kinds?`, and `query?` params (JSON-encoded descriptor); returns `ProjectedState` | done | add bundle-specific providers | | `/system/info` | GET | public; rate-limited | done | — | | `/auth/whoami` | GET | returns current `Principal` | not_started | see auth thread | | `/auth/keys` (CRUD) | various | API key lifecycle | not_started | see auth thread | | `/auth/revoke` | POST | revoke a key | not_started | see auth thread | **CLI port and L2.** The CLI port does not call any of these endpoints. It consumes `engine/`'s Python surface directly via an in-process shim — typically a thin wrapper around `ServiceManager.do_action()` / `get_envelope()` / `get_projected_state()`. The L2 column is therefore not blocking for CLI conformance. --- ## §F — Conformance harness These tests verify spec contracts against the reference implementations. They serve as the gating mechanism for tier graduations and as the regression suite for cross-port parity. | Harness | What it checks | Status | Plan | |---|---|---|---| | `engine/contrib/conformance/cli_reference_port.py` | Tier S CLI rendering for every fragment / value_type | partial (Tier S + current P1 widgets) | extend coverage as Tier P1 → S graduations happen | | `engine/contrib/conformance/legibility.py` | §5.1 — referenced UIDs are rendered | initial | covers fixture and sequence choices; expand as new P1/P2 fields promote | | `engine/contrib/conformance/parity.py` | §5.3 input parity | initial | covers fixture and sequence submit floors; time parity remains next harness | | `engine/contrib/conformance/time_parity.py` | §5.2 time parity | initial | covers JSON-readable media/roll fallbacks; browser skip timing remains later | | `engine/contrib/conformance/test_conformance.py` | pytest harness binding fixtures + ports | not_started | wire into CI | | `engine/contrib/conformance/fixtures/*.json` | canonical envelopes per surface | partial (Tier S + current P1 fixtures) | keep promoting proposal fixtures as implementation lands | | `engine/contrib/conformance/proposals/*.json` | forward-compatible proposal envelopes | partial | current set covers carwars garage, piece realization, place accepts, record KvRow, roll fragment, and one UUID-shaped v1.5 wireframe interpretation sample; CLI/Tk can inspect them without promotion | | webapp Vitest conformance suite | webapp DOM matches expected for each fixture | not_started | written from fixtures; webapp regression mechanism | --- ## §G — Recent reconciliation events A short log of what changed across recent spec / UI / engine releases. Each entry names which layer moved and what the consequence is for the others. ### 2026-05 — spec v1.5 (L1 update only) - Adopted the v1.4 genre-audit additions: journal-as-narrative, per-cursor projection of shared state, and the genre extension index. **Impact:** no new top-level vocabulary surfaces; improves authoring discipline and cross-demo traceability. - Reconciled v1.4 language with repo-current implementation status. **Impact:** typed `Accepts` and `UIHints` are described as implemented; `Blocker`, `InterpretationFragment`, typed info metadata, and Tier P2 widgets remain pending or partial. - Updated the fixture inventory to match the current repository, including `compose_payload.json` and the existing proposal fixture set. - Imported and vetted Tier P3 extension docs for CarWars, credentials, training, and elefant_hunt, plus `GENRE_AUDIT_NOTES.md`. **Impact:** genre enrichments are advisory over core v1.5; no `_common/EXTENSIONS.md` until a repeated cross-genre pattern is not already covered by the core vocabulary. - Archived the v1.5 wireframe package under `docs/src/design/story/wireframes/v1_5/`. **Impact:** visual coverage now matches the reconciled v1.5 vocabulary and Tier P3 extension docs; most wireframe fixtures remain design fixtures until their symbolic ids are translated for engine conformance. - Translated one v1.5 wireframe interpretation sample into `engine/contrib/conformance/proposals/` with UUID-shaped ids. **Impact:** future ports have a concrete command-feedback fixture without promoting the whole wireframe bundle to gating conformance. ### 2026-05 — spec v1.3 (L1 update only) - Reverted `accepts.kind="select"` rename → `pieces`. **Impact:** L2/L3 may continue using `pieces`; no migration needed. - Demoted §1.5 cursors and §1.6 info channels to Tier P1. **Impact:** none on L2/L3 directly; clarifies that current single-cursor behavior is settled, multi-cursor is target. - Replaced `GET /story/info/{kind}` with query-descriptor model. **Impact:** L2 evolves the `/story/info` endpoint; no L1 break since the v1.2 URL form was never shipped. - Added §0.7 three-layer architecture. **Impact:** this document exists. - EXTENSIONS.md swept `tokens → pieces`. **Impact:** carwars Tier P3 conventions now consistent with main spec. ### 2026-05 — webapp v1.3 reference UI update - Reference webapp uses `accepts.kind="pieces"` throughout (15+ call sites). - Reference webapp implements `InfoAffordance.query` as an opaque JSON descriptor on `/story/info?kind=...&query=...`. - Reference webapp accepts nested `metadata.info_state`. This row is superseded by the v1.5 update, where `dirty_kinds` became an implemented cache hint in the reference client. - Reference webapp ships `place` accepts kind without `edge_ref` (proposal fixture not yet exercised). ### 2026-05 — webapp v1.5 reference UI reconciliation - Reference webapp renders `InterpretationFragment` with the spec's `result`, `text`, `message`, `blocked_reason`, `hint`, and `candidates` fields. **Impact:** command-resolution feedback no longer falls through to the unknown-fragment fallback. - Reference webapp renders typed `blockers[]` and plural `cost_previews[]` as choice decision details. **Impact:** locked choices expose more of the decision-legibility contract in the current shell. - Reference webapp posts `edge_id` to `/story/do`. **Impact:** client and endpoint server now use the same choice-edge identifier name. - Reference webapp treats `metadata.info_state.dirty_kinds` as cache hints for `/story/info`. **Impact:** the sidebar keeps old refresh behavior when no hint is provided, but skips clean side-channel refreshes when the backend sends explicit dirty-kind metadata. The tests cover both default status and selected affordance kinds. - Reference CLI/Tk ports render or inspect blockers, cost previews, typed accepts prompts, and v1.5 interpretation fields. **Impact:** new client ports can compare against portable JSON fixtures and proposal fixtures instead of copying the Vue shell. ### 2026-05 — engine current (L3 baseline) - Engine emits typed `Accepts` and `UIHints` models from `tangl.journal.intent`; blockers remain dictionary-shaped pending the next intent pass. - Engine HTTP body uses `edge_id`. - Engine `KvFragment.content`, projected `kv_list` values, web fixtures, and conformance fixtures use the unified `KvRow` record shape. - Engine has no `/story/info/{kind}` (consistent with v1.5 spec). --- ## §H — How to use this doc - **Spec authors.** When you commit a vocabulary change, update the appropriate row's "Spec tier" column and add a one-paragraph entry to §G. The implementation columns lag deliberately. - **Reference UI authors.** When the webapp implements a surface, update the "Reference UI" column to `done` or `partial`. If you're implementing against a fixture (not against a live engine), use `partial` and note the fixture in the Plan column. - **Engine authors.** When the engine ships a typed shape or endpoint, update the "Engine backend" column. If you're shipping the engine capability but not the API endpoint, that's still progress — the capabilities table in `ENGINE_CAPABILITIES.md` (forthcoming) tracks Python surface separately. - **CLI port authors.** Watch §A and §B; you don't care about §E. Your conformance is against `cli_reference_port.py` and the §F harness. A row becomes a candidate for the §G log when any column moves. A surface becomes a candidate for Tier S graduation when all three implementation columns read `done` and the CLI reference port renders it. --- ## §I — Forward-looking sequence This is the **inversion plan** the project committed to: UI leads, API + engine chase. Each step's deliverable is a single PR-shaped change to the named layer. **Phase 1 — Stabilize spec (current).** - ✅ Spec v1.5 with the v1.4 genre-audit additions reconciled to repo state. - ✅ EXTENSIONS.md swept to `pieces`. - ✅ Tier P3 extension docs imported and vetted for current demo genres. - ✅ This reconciliation doc as the three-layer spine. **Phase 2 — Reference UI catches up.** Wireframes and webapp align to v1.5. - ✅ Wireframes resync: v1.5 bundle is archived under `docs/src/design/story/wireframes/v1_5/`, with v1.2/v1.3 treated as visual precedent rather than contract truth. - ✅ The v1.5 wireframe annotates core vocabulary separately from Tier P3 enrichments (`stat_check`, `drag`, `validity_check`, `encounter_check`), keeping contract conformance and genre flavor separable. - Webapp PR sequence: 1. ✅ `compose` accepts rendering as nested ChoiceInputView. 2. ✅ `InterpretationFragment` renderer with the spec's `result` / `text` field names. 3. ✅ `metadata.info_state` behavior pass: use nested `dirty_kinds` / `available_kinds` as cache hints once the backend emits them. 4. Keep `info_affordances` opaque; clients pass `query` descriptors through rather than parsing bundle-specific fields. **Phase 3 — Engine + API catch up.** Backend declares capabilities, API maps them. - Engine PR sequence: 1. Typed `Blocker` model in `tangl/journal/intent.py`. 2. Typed `InterpretationFragment` with `result` / `text` fields. 3. Typed `metadata.grammar` model. 4. HTTP API: type responses on `/story/do` / `/story/update`. 5. Conformance harness: ✅ initial `legibility.py`; ✅ initial input-parity `parity.py`; ✅ initial `time_parity.py`; ✅ CLI-floor info-channel reachability; next add browser timing E2E only after promoted surfaces stabilize. **Phase 4 — Tier P1 → S graduation.** Surfaces in §B promote one at a time, gated on the CLI reference port and the conformance harness having coverage for each. **Phase 5 — Tier P2 surfaces gate on bundle MVPs.** Each Tier P2 surface (multi-cursor, pieces with position, edge_ref, predicate registration) waits for a real bundle author who needs it. We do not pre-build these. --- *End of v0.2.*