The planning package had grown to where a new agent could not tell what was authoritative. Phase 0 execution prompts sat beside the specification; four completed milestone reports sat beside the current one; and upstream AI-DnD's own `plan/` build log and `docs/` project site still described a hosted, scripted, multi-user product with accounts — every screenshot in it showed a Scripts tab and a Sign up button, none of which has existed since M2. `planning/archive/` now holds the history and says so in its own README: `phase0/` for the research that chose AI-DnD, `milestone-reports/` for M1 and M2, `decisions/` for ADR 008, the Phase-0-before-build gate Phase 0 satisfied. `planning/reports/` holds only the current milestone's report, because that is the one M4 planning has to read; it moves to the archive when M4's replaces it. Deleted rather than archived: the Phase 0B execution prompts and the handoff/status/summary documents, the Phase 0A discovery and triage reports, upstream's `plan/` and `docs/` trees, and `frontend/README.md`, which was Vite's template boilerplate. All of it is in Git history, and the two recommendation reports carry every conclusion the deleted research reached. Archived documents are kept verbatim. Paths written inside them point at where those files were when the document was written, which is the point: an evidence record that has been quietly edited is no longer evidence. Active documentation is corrected where it pointed at the removed trees or described removed capability as present. `DEVELOPMENT.md`'s "things M1 did not touch" list had gone stale at M2 and claimed QuickJS scripting was still tested; its test count was 604 against an actual 638. `README.md` loses the upstream CI badge, which reported upstream's pipeline rather than this fork's, and a reference to `backend/app/worldstate/engine.py`, a file that does not exist. `planning/README.md` is rewritten as the documentation index. New: `planning/PROJECT-SOURCES.md` and `planning/project-sources.txt`, the manifest of what belongs in the ChatGPT project's Sources. Source comments referring to the deleted trees are reworded; no behaviour changes. 638 backend tests pass, frontend lints and builds, and a reference scan over all 48 tracked Markdown files reports no unresolved path in active documentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCbwH7yLGKsj1rhXXzKSCu
4.7 KiB
Phase 0B — Experiment C: ai-adventure
C1. Tests
python -m unittest discover -s tests → Ran 76 tests in 1.9s OK
C2. Provider abstraction — the Ollama adapter is zero code
llm/backend.py defines a ModelBackend Protocol with a single generate
method plus typed ModelRequest/ModelResponse. llm/lm_studio.py is not
LM-Studio-specific at all: it POSTs /v1/chat/completions and reads /v1/models
— the same OpenAI-compatible surface Ollama serves. llm/scripted.py is a
deterministic fake used by the tests.
The only change needed was two lines of world.toml:
base_url = "http://127.0.0.1:11434" # was 127.0.0.1:1234
name = "qwen2.5:3b-instruct" # was "replace-with-a-local-model"
python -m local_adventure doctor --world <world>
[PASS] SQLite FTS5 available
[PASS] Database schema at version 2
[PASS] World valid: Ember Hollow
[PASS] LM Studio reachable: http://127.0.0.1:11434
[PASS] Configured model visible: qwen2.5:3b-instruct
A real turn then committed:
TURN: (30ad56d4…, 1, 'committed', 'I search the observatory for the brass k',
'You rummage through the dusty shelves, your fingers brushing away
decades of cobwebs...')
MODELCALL: ('lm_studio', 'qwen2.5:3b-instruct', attempt 1, 832 bytes, errors=False)
ModelSettings.backend is typed Literal["lm_studio"], so adding a second
backend means widening one literal and one factory call in cli.py — the
transport itself already works. Phase 0A's "MODIFY — LM Studio primary provider"
overstated this.
Caveat: with qwen2.5:0.5b the same turn failed with "The model response
could not be validated; no turn was saved." That is the validation contract
working as designed — the engine refuses to commit an unvalidated turn — but the
typed-event contract needs a reasonably capable model, where the freeform-prose
candidates tolerate a weak one.
C3. Undo / branch / checkpoint / replay — verified independently
Run directly against GameService with a temporary database, not by trusting the
project's own tests:
after 2 turns: turns=2 events=2 has_brass_key=False
UNDO
turns 2 → 2 (DELETED 0)
state after undo: has_brass_key=True
abandoned turn id_3 still in DB: True
BRANCH from the restored point, then play on the branch
branch state: has_brass_key=True gate_open=True
ORIGINAL session leaked 'gate_open'? False
total turns in DB: 3 (nothing destroyed)
CHECKPOINT RESTORE ("before_gate")
restored has_brass_key=True
later turn id_3 still present: True
turns in DB after restore: 3
This is the specification's history model, working. undo sets
session.head_turn_id to the parent; branch inserts a new session sharing the
same head; restore_checkpoint replays ancestry; _replay_to walks
turns.ancestry(head) and reduces state_events.
C4. Schema — the target data model
sessions(session_id, ..., head_turn_id, initial_state_json)
turns(turn_id, session_id, parent_turn_id, turn_number,
player_input, narration, status CHECK IN ('committed','failed'), model_call_id)
state_events(event_id, turn_id, sequence_number, event_type, payload_json) -- append-only
state_cache(session_id, head_turn_id, state_json) -- replay cache
model_calls(..., request_hash, response_hash, parsed_response_json,
validation_errors_json, prompt_eval_count, eval_count, duration_ms)
named_checkpoints(checkpoint_id, session_id, turn_id, name, UNIQUE(session_id,name))
summaries(summary_id, session_id, through_turn_id, kind CHECK IN ('scene','campaign'))
lore_documents + lore_documents_fts (FTS5)
Note summaries.through_turn_id — anchored to a turn, not a count. That is
exactly the property Open Dungeon lacks.
C5. Coupling to the CLI
The layering is clean: cli.py → app/commands.py → app/game_service.py /
app/turn_service.py → storage/repositories.py. Authoritative state lives in
state/ (events, reducer, validator) and context assembly in context/. None of
that imports the CLI. A browser/API layer could sit beside cli.py and call the
same services without moving state logic.
What it does not have, and would all be new work: any HTTP layer, streaming, in-app campaign setup (worlds are hand-authored TOML + Markdown on disk), embeddings or semantic memory, document import, media, and prompt inspection beyond stored hashes.
C6. Lore / FTS — reusable
lore/indexer.py + lore_documents_fts (FTS5) with content hashing and
modified_ns for incremental reindex, scoped by world_id, and a kind column
already carrying a classification axis. This is a good starting shape for the
Canon/Reference/Inspiration tiers in IMPORTED-KNOWLEDGE-DESIGN.md, and it is
deterministic and offline.