From d27ee349013d15003dbe5815bc197d3689e20088 Mon Sep 17 00:00:00 2001 From: JesseMarkowitz Date: Thu, 3 Sep 2026 14:33:07 -0400 Subject: [PATCH] Docs: consolidate active planning and archive historical material MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01NCbwH7yLGKsj1rhXXzKSCu --- DEVELOPMENT.md | 29 +- PROVENANCE.md | 10 +- README.md | 49 +- backend/app/migrations.py | 3 +- backend/tests/test_chat.py | 2 +- backend/tests/test_state_revert.py | 3 +- backend/tests/test_story_tree_baseline.py | 2 +- backend/tests/test_tree_migration.py | 3 +- backend/tools/memory_ab.py | 7 +- docs/GUIDE.md | 1227 ------------- docs/architecture.html | 1233 ------------- docs/guide.html | 1577 ----------------- docs/images/branch-map.jpg | Bin 36669 -> 0 bytes docs/images/branches-panel.jpg | Bin 105348 -> 0 bytes docs/images/home.jpg | Bin 68129 -> 0 bytes docs/images/insights.jpg | Bin 160403 -> 0 bytes docs/images/play-world-state.jpg | Bin 96735 -> 0 bytes docs/images/scenario-editor-npcs.jpg | Bin 85915 -> 0 bytes docs/images/script-editor.jpg | Bin 60108 -> 0 bytes docs/index.html | 267 --- docs/self-review.md | 156 -- frontend/README.md | 16 - frontend/src/pages/Play/index.jsx | 4 +- plan/00-OVERVIEW.md | 122 -- plan/01-phase-foundation.md | 38 - plan/02-phase-play-loop.md | 45 - plan/03-phase-context-insights.md | 62 - plan/04-phase-scripting.md | 84 - plan/05-phase-polish.md | 38 - plan/06-phase-memory-bank.md | 47 - plan/07-phase-public-repo.md | 62 - plan/08-phase-accounts.md | 81 - plan/09-phase-hardening.md | 93 - plan/10-phase-deploy.md | 56 - plan/11-state-revert-and-retry-fix.md | 81 - plan/12-phase-rpg-world-state.md | 269 --- plan/13-memory-embedding-cost.md | 317 ---- plan/14-phase-story-tree.md | 914 ---------- plan/15-pokemon-demo-handover.md | 117 -- plan/16-world-state-refusals.md | 343 ---- plan/17-refactor.md | 582 ------ plan/18-appendix-memory-ab-run-2.md | 145 -- plan/18-appendix-memory-ab-run.md | 134 -- plan/18-persona-and-memory-quality.md | 655 ------- plan/STATUS.md | 1162 ------------ planning/BUILD-MILESTONES.md | 19 +- planning/CODEX-HANDOFF-NOTE.md | 14 - planning/DECISIONS/002-ollama-only-v1.md | 2 +- .../011-local-inference-endpoint-policy.md | 2 +- planning/PHASE-0B-CODEX-BRIEF.md | 272 --- planning/PHASE-0B-CODEX-HANDOFF.md | 277 --- planning/PLANNING-UPDATE-SUMMARY.md | 49 - planning/PROJECT-SOURCES.md | 121 ++ planning/README.md | 294 +-- planning/TECHNICAL-DESIGN.md | 2 +- planning/VERSION.md | 46 +- planning/archive/README.md | 67 + .../008-phase0-before-build-plan.md | 0 .../milestone-reports}/M1-BASELINE-REPORT.md | 0 .../M1-IMPLEMENTATION-REPORT.md | 0 .../milestone-reports}/M2-BASELINE-REPORT.md | 0 .../M2-IMPLEMENTATION-REPORT.md | 0 .../phase0}/AI-ADVENTURE-ANALYSIS.md | 0 .../phase0}/AI-DND-ANALYSIS.md | 0 .../phase0}/OPEN-DUNGEON-ANALYSIS.md | 0 .../phase0}/PHASE-0B-AI-ADVENTURE-OLLAMA.md | 0 .../phase0}/PHASE-0B-AI-DND-EXPERIMENT.md | 0 .../phase0}/PHASE-0B-BASELINE.md | 0 .../phase0}/PHASE-0B-FOLLOWUP-CHECKS.md | 0 .../phase0}/PHASE-0B-OFFLINE-NETWORK.md | 0 .../phase0}/PHASE-0B-OPEN-DUNGEON-HISTORY.md | 0 .../phase0}/PHASE-0B-RECOMMENDATION.md | 0 .../phase0}/PHASE-0B-UNDO-SPIKE.md | 0 .../phase0}/PRELIMINARY-RECOMMENDATION.md | 0 .../{ => archive/phase0}/RESEARCH-PLAN.md | 0 .../phase0}/REUSE-MATRIX.md | 0 planning/project-sources.txt | 29 + planning/reports/AIMULTIFOOL-ANALYSIS.md | 46 - planning/reports/CANDIDATE-INVENTORY.md | 68 - planning/reports/LICENSING-REUSE.md | 75 - planning/reports/PHASE-0A-STATUS.md | 41 - planning/reports/PRIVACY-STATIC-ANALYSIS.md | 122 -- planning/reports/REFERENCE-PROJECTS.md | 111 -- planning/reports/SOURCE-INDEX.md | 83 - 84 files changed, 504 insertions(+), 11271 deletions(-) delete mode 100644 docs/GUIDE.md delete mode 100644 docs/architecture.html delete mode 100644 docs/guide.html delete mode 100644 docs/images/branch-map.jpg delete mode 100644 docs/images/branches-panel.jpg delete mode 100644 docs/images/home.jpg delete mode 100644 docs/images/insights.jpg delete mode 100644 docs/images/play-world-state.jpg delete mode 100644 docs/images/scenario-editor-npcs.jpg delete mode 100644 docs/images/script-editor.jpg delete mode 100644 docs/index.html delete mode 100644 docs/self-review.md delete mode 100644 frontend/README.md delete mode 100644 plan/00-OVERVIEW.md delete mode 100644 plan/01-phase-foundation.md delete mode 100644 plan/02-phase-play-loop.md delete mode 100644 plan/03-phase-context-insights.md delete mode 100644 plan/04-phase-scripting.md delete mode 100644 plan/05-phase-polish.md delete mode 100644 plan/06-phase-memory-bank.md delete mode 100644 plan/07-phase-public-repo.md delete mode 100644 plan/08-phase-accounts.md delete mode 100644 plan/09-phase-hardening.md delete mode 100644 plan/10-phase-deploy.md delete mode 100644 plan/11-state-revert-and-retry-fix.md delete mode 100644 plan/12-phase-rpg-world-state.md delete mode 100644 plan/13-memory-embedding-cost.md delete mode 100644 plan/14-phase-story-tree.md delete mode 100644 plan/15-pokemon-demo-handover.md delete mode 100644 plan/16-world-state-refusals.md delete mode 100644 plan/17-refactor.md delete mode 100644 plan/18-appendix-memory-ab-run-2.md delete mode 100644 plan/18-appendix-memory-ab-run.md delete mode 100644 plan/18-persona-and-memory-quality.md delete mode 100644 plan/STATUS.md delete mode 100644 planning/CODEX-HANDOFF-NOTE.md delete mode 100644 planning/PHASE-0B-CODEX-BRIEF.md delete mode 100644 planning/PHASE-0B-CODEX-HANDOFF.md delete mode 100644 planning/PLANNING-UPDATE-SUMMARY.md create mode 100644 planning/PROJECT-SOURCES.md create mode 100644 planning/archive/README.md rename planning/{DECISIONS => archive/decisions}/008-phase0-before-build-plan.md (100%) rename planning/{reports => archive/milestone-reports}/M1-BASELINE-REPORT.md (100%) rename planning/{reports => archive/milestone-reports}/M1-IMPLEMENTATION-REPORT.md (100%) rename planning/{reports => archive/milestone-reports}/M2-BASELINE-REPORT.md (100%) rename planning/{reports => archive/milestone-reports}/M2-IMPLEMENTATION-REPORT.md (100%) rename planning/{reports => archive/phase0}/AI-ADVENTURE-ANALYSIS.md (100%) rename planning/{reports => archive/phase0}/AI-DND-ANALYSIS.md (100%) rename planning/{reports => archive/phase0}/OPEN-DUNGEON-ANALYSIS.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-AI-ADVENTURE-OLLAMA.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-AI-DND-EXPERIMENT.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-BASELINE.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-FOLLOWUP-CHECKS.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-OFFLINE-NETWORK.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-OPEN-DUNGEON-HISTORY.md (100%) rename planning/{ => archive/phase0}/PHASE-0B-RECOMMENDATION.md (100%) rename planning/{reports => archive/phase0}/PHASE-0B-UNDO-SPIKE.md (100%) rename planning/{reports => archive/phase0}/PRELIMINARY-RECOMMENDATION.md (100%) rename planning/{ => archive/phase0}/RESEARCH-PLAN.md (100%) rename planning/{reports => archive/phase0}/REUSE-MATRIX.md (100%) create mode 100644 planning/project-sources.txt delete mode 100644 planning/reports/AIMULTIFOOL-ANALYSIS.md delete mode 100644 planning/reports/CANDIDATE-INVENTORY.md delete mode 100644 planning/reports/LICENSING-REUSE.md delete mode 100644 planning/reports/PHASE-0A-STATUS.md delete mode 100644 planning/reports/PRIVACY-STATIC-ANALYSIS.md delete mode 100644 planning/reports/REFERENCE-PROJECTS.md delete mode 100644 planning/reports/SOURCE-INDEX.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ddf7875..c30538e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -208,7 +208,7 @@ visible from within. ## Tests ```bash -cd backend && .venv/bin/python -m pytest tests/ -q # 604 tests +cd backend && .venv/bin/python -m pytest tests/ -q # 638 tests cd frontend && npm run lint && npm run build ``` @@ -266,21 +266,24 @@ docker exec app python -c "import socket; socket.create_connection(('1.1.1.1',44 # -> OSError: Network is unreachable, and story turns still work ``` -`planning/reports/M1-BASELINE-REPORT.md` records the run this procedure is +`planning/archive/milestone-reports/M1-BASELINE-REPORT.md` records the run this procedure is taken from, including the packet captures. -## Things inherited from upstream that M1 deliberately did not touch +## Things still inherited from upstream -These are M2's scope (`planning/BUILD-MILESTONES.md`), listed here so nobody -reports them as new: +M2 removed the hosted, cloud, account, analytics, Postgres/Render and QuickJS +scripting surfaces outright — `PROVENANCE.md` lists exactly what went. What is +left of upstream that a newcomer might report as a defect: -- hosted/multi-user/account/demo-key code, analytics tables, Postgres and - Render deployment paths, and the OpenRouter default endpoint constant all - still exist in the tree. None of them is reachable from a default local run, - and none requires a cloud service. -- `docs/*.html` is upstream's GitHub Pages project site and still links Google - Fonts. It is not served by the application and is not part of any build. -- `.github/workflows/ci.yml` is upstream's GitHub Actions pipeline. This +- **Inert legacy tables and columns.** Five tables and four columns M2 emptied + of meaning are still in the schema, unmapped, so an M1-era campaign database + opens unchanged. Nothing reads or writes them. A cleanup migration waits for + the schema to settle after M5 (`planning/BUILD-MILESTONES.md`). +- **Dual-dialect migration code.** `backend/app/migrations.py` still carries + SQLite/Postgres branches from upstream, although Postgres support itself is + gone and SQLite is the only store. Same cleanup, same milestone. +- **`.github/workflows/ci.yml`** is upstream's GitHub Actions pipeline. This repository lives on a self-hosted Gitea; the workflow is kept for provenance and is not what runs the tests here. -- QuickJS campaign scripting is still present and still tested. +- **No frontend tests.** `npm run lint && npm run build` is the whole frontend + check. A test runner is M8's job. diff --git a/PROVENANCE.md b/PROVENANCE.md index cc6ef26..17c23ac 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -26,9 +26,11 @@ import is a merge of the pinned commit with `--allow-unrelated-histories`, so: - `git log d72f7c1bda0f34fccd84afb7a25c34eb01c901de` shows the real upstream history, not a squashed snapshot; -- upstream paths are unchanged (`backend/`, `frontend/`, `docs/`, …), so a - later upstream commit can still be fetched and cherry-picked against - matching files; +- upstream code paths are unchanged (`backend/`, `frontend/`, …), so a later + upstream commit can still be fetched and cherry-picked against matching + files. Upstream's own documentation trees, `plan/` and `docs/`, were removed + on 2026-09-03: they described the hosted, scripted, multi-user product this + fork is not. They remain in this repository's history and in upstream; - the planning package that predates the fork keeps its own history on the other parent of the merge. @@ -104,7 +106,7 @@ unchanged. They are not product functionality and nothing reads or writes them. ## What this fork changed in Milestone M1 Nothing was removed from upstream. The changes are the offline/locality -hardening M1 called for; see `planning/reports/M1-BASELINE-REPORT.md` for the +hardening M1 called for; see `planning/archive/milestone-reports/M1-BASELINE-REPORT.md` for the evidence. - `backend/app/context/encoding.py` (new) and `backend/app/context/builder.py` — diff --git a/README.md b/README.md index 27996be..9fa5f10 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -# AI D&D +# Adventure Storyteller -[![CI](https://github.com/parththakkar106/AI-DnD/actions/workflows/ci.yml/badge.svg)](https://github.com/parththakkar106/AI-DnD/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) An interactive storytelling app that runs entirely on your own machine, with your own model. @@ -18,19 +17,17 @@ disabled. What is left is a storyteller you can run offline. > public address. There is no telemetry, no account, no cloud inference, and nothing is fetched > at runtime from the Internet. > -> For the internals, read the **[design notes](docs/GUIDE.md)**. They walk through the context -> budgeting, the world-state referee, and the memory bank, and state the reasoning behind each -> one. Some sections still describe upstream subsystems this fork has removed. +> For the internals, read [`planning/TECHNICAL-DESIGN.md`](planning/TECHNICAL-DESIGN.md) and +> [`planning/CONTEXT-AND-MEMORY.md`](planning/CONTEXT-AND-MEMORY.md), which cover the context +> budgeting, the state model and the memory bank as this fork builds them. Built with FastAPI and SQLAlchemy on the backend and React (Vite) on the frontend, storing everything in one SQLite file. -![The play screen, with the world-state rail open](docs/images/play-world-state.jpg) - -*The play screen. The left rail shows live world state. The AI proposes changes each turn, and -a Python engine decides what actually sticks. The chip under the narration reports what -changed. The `‹ 2/2 ›` under a turn steps between the takes it has. Writing below a take that -isn't the live one starts a new branch.* +On the play screen, the left rail carries live world state. The AI proposes changes each turn +and a Python engine decides what actually sticks; the chip under the narration reports what +changed. The `‹ 2/2 ›` under a turn steps between the takes it has, and writing below a take +that isn't the live one starts a new branch. ## Features @@ -49,7 +46,8 @@ isn't the live one starts a new branch.* cast; the adventure carries their live values. The AI proposes deltas, and a Python engine referees them: it clamps values to range, enforces per-turn caps and cooldowns, keeps counters monotonic and milestones sticky, then strips the machine-readable block out of the prose - (`backend/app/worldstate/engine.py`). Word-labeled bands (`40–60: minor damage`) make the + (`backend/app/worldstate/`: `apply.py` clamps, `parse.py` reads the block back). + Word-labeled bands (`40–60: minor damage`) make the model reliable at it. No dice and no scripting are required. - **AI Dungeon-compatible context engine.** Memory, author's note, and story cards (world info) are triggered by keywords in recent story text, then assembled under a token budget @@ -88,14 +86,10 @@ isn't the live one starts a new branch.* ## Screenshots -| | | -|---|---| -| ![Insights panel](docs/images/insights.jpg) | ![Scenario editor](docs/images/scenario-editor-npcs.jpg) | -| **Insights**: the exact prompt for the next turn, broken into components with token counts and the trigger word that pulled each story card in. | **Authoring**: stats with ranges, per-turn caps, cooldowns, and word-labeled bands; NPCs the AI addresses by id. | -| ![Home](docs/images/home.jpg) | ![The branch map](docs/images/branch-map.jpg) | -| **Home**: continue a story in progress or start from a scenario. | **The tree**: one lane per line, from the moment it left its parent to the moment it ends. The horizontal axis is the story's own clock, so a short branch reads as short. | -| ![The branches panel](docs/images/branches-panel.jpg) | | -| **Branches**: every line the story has taken, and the three things you can do to one. A line the one you're reading was forked from can't be deleted, and says so. | | +None yet. The inherited screenshots showed upstream's UI — a Scripts tab, Log in and Sign up, +a guest banner, scripting demo scenarios — none of which this fork has since M2, so they were +removed rather than left standing as a picture of a product that no longer exists. New ones +are taken when the browser smoke test M3 still owes is run. ## Quick start @@ -248,16 +242,15 @@ most interesting engineering in the repo. - `planning/` is this fork's own package: the product specification, the architecture decisions, the milestone plan, the acceptance contract, and a review report for every milestone shipped. Start at [`planning/README.md`](planning/README.md). -- `plan/` holds the *upstream* project's phased implementation plan, kept as a build log. The - later files (11, 12, 14) still serve as design notes for the state-revert, world-state, and - story-tree work this fork inherited. -- [`docs/GUIDE.md`](docs/GUIDE.md) holds upstream's design notes: how each subsystem works and - why it was built that way, with the measurements behind the decisions. Sections covering - scripting, accounts and hosted deployment describe subsystems this fork removed. +- [`planning/archive/`](planning/archive/README.md) holds the Phase 0 research that chose this + base and the completed milestone reports. It is history, not instruction. +- [`DEVELOPMENT.md`](DEVELOPMENT.md) is how to set the project up, point it at a model, and run + the tests. [`PROVENANCE.md`](PROVENANCE.md) records what came from upstream and what changed. - `backend/.env.example` lists the two environment variables the backend reads. Everything about the model is a runtime setting on the Settings page instead. -- [`docs/self-review.md`](docs/self-review.md) records a full-codebase self-review pass and - what came out of it. All correctness findings are resolved. +- Upstream's own `plan/` build log and `docs/` project site were removed in the 2026-09-03 + documentation pass: they described AI-DnD's hosted, scripted, multi-user product. Both are + still in Git history, and in upstream. ## License diff --git a/backend/app/migrations.py b/backend/app/migrations.py index 90aa1dd..7e098db 100644 --- a/backend/app/migrations.py +++ b/backend/app/migrations.py @@ -301,7 +301,8 @@ MIGRATIONS: list[tuple[int, str | dict[str, str]]] = [ (63, "ALTER TABLE actions ADD COLUMN parent_id INTEGER REFERENCES actions(id) ON DELETE SET NULL"), (64, "CREATE INDEX IF NOT EXISTS ix_actions_parent ON actions (parent_id)"), # Phase 17: `Settings.stream` was dead state. Nothing ever read it, and every - # turn streams. This is item S1 in `docs/self-review.md`. The table holds one + # turn streams; upstream's self-review log flagged it as dead state. The + # table holds one # row per user, so the rewrite is small and needs no VACUUM FULL. (65, "ALTER TABLE settings DROP COLUMN stream"), # Phase 17, SP8: drop the eight columns the story tree replaced. Each one was diff --git a/backend/tests/test_chat.py b/backend/tests/test_chat.py index 36294df..4332926 100644 --- a/backend/tests/test_chat.py +++ b/backend/tests/test_chat.py @@ -4,7 +4,7 @@ Most of this file used to be about the shared demo key: an access gate on a "power user" email allowlist, and a pinning rule that stopped a public visitor reaching paid models on a server-funded key. M2 removed the hosted deployment those defended, so the rules they tested no longer exist to be tested. See -`planning/reports/M2-*` for the accounting. +`planning/archive/milestone-reports/M2-*` for the accounting. What remains is what the page still does: stream a reply from the configured model, honour a system prompt and a per-request model override, and refuse a diff --git a/backend/tests/test_state_revert.py b/backend/tests/test_state_revert.py index 2225766..83bf720 100644 --- a/backend/tests/test_state_revert.py +++ b/backend/tests/test_state_revert.py @@ -1,5 +1,4 @@ -"""Tests for undo and retry rolling back the shared `world_state` -(plan/11-state-revert-and-retry-fix.md). +"""Tests for undo and retry rolling back the shared `world_state`. The state being rolled back was the scripting engine's `script_state` until M2 removed campaign scripting. The machinery under test — `attempts.restore_state`, diff --git a/backend/tests/test_story_tree_baseline.py b/backend/tests/test_story_tree_baseline.py index d4bd8d8..8d43714 100644 --- a/backend/tests/test_story_tree_baseline.py +++ b/backend/tests/test_story_tree_baseline.py @@ -15,7 +15,7 @@ This file must pass unmodified through SP1 (schema), SP2 (branch clause), and SP3 (memories on nodes). If a change here looks necessary in one of those subphases, the change is wrong, not the test. SP4 is the first subphase allowed to move it, and only for the variant-count semantics -called out in plan/14. +the story-tree migration changed. """ import pytest from fastapi import Depends diff --git a/backend/tests/test_tree_migration.py b/backend/tests/test_tree_migration.py index 541b006..7c25640 100644 --- a/backend/tests/test_tree_migration.py +++ b/backend/tests/test_tree_migration.py @@ -31,8 +31,7 @@ from app.main import app # The three tables as they stood at schema 45, frozen. This is a snapshot # of a past schema. It must not be updated to track `models.py`, because # the whole point is that it lacks what SP1 adds. This DDL uses SQLite -# syntax only. The migration's Postgres half is exercised against a real -# server at deploy time (see plan/14). +# syntax only. PRE_TREE_DDL = ( """ CREATE TABLE adventures ( diff --git a/backend/tools/memory_ab.py b/backend/tools/memory_ab.py index 65c2efe..1c9c3fd 100644 --- a/backend/tools/memory_ab.py +++ b/backend/tools/memory_ab.py @@ -4,10 +4,9 @@ Reviewing a prompt tells you what it asks for. It does not tell you what a model does with it. This runs the real pipeline twice over the *same* blocks of the *same* story, changing only the prompt, and prints the memories side by side. -What it showed the first time it was run, and why `MEMORY_MAX_WORDS` exists, is -written up in `plan/18-persona-and-memory-quality.md`. Two consecutive memories -from one story came back in two different persons, and the same model wrote 34 -words for one block and 105 for the next. +Why `MEMORY_MAX_WORDS` exists is what it showed the first time it was run: two +consecutive memories from one story came back in two different persons, and the +same model wrote 34 words for one block and 105 for the next. Three things make it a fair test rather than a demonstration: diff --git a/docs/GUIDE.md b/docs/GUIDE.md deleted file mode 100644 index 40d4d10..0000000 --- a/docs/GUIDE.md +++ /dev/null @@ -1,1227 +0,0 @@ -# AI D&D — design notes - -How the engine works and why it is built this way. The README covers what the project does -and how to run it; this covers the reasoning behind the parts that had a real choice in -them. - -Part 1 is the AI layer, which is where most of the design effort went. Parts 2 and 3 are -what makes it a service rather than a demo. Part 4 is the web plumbing, kept short. - ---- - -## Contents - -- [Part 0 — Orientation](#part-0--orientation) -- [Part 1 — The AI layer](#part-1--the-ai-layer) - - [1.1 The turn pipeline](#11-the-turn-pipeline) - - [1.2 Context assembly is a budget problem](#12-context-assembly-is-a-budget-problem) - - [1.3 World state: the AI proposes, Python referees](#13-world-state-the-ai-proposes-python-referees) - - [1.4 Output length, by measurement](#14-output-length-by-measurement) - - [1.5 The memory bank](#15-the-memory-bank) - - [1.6 Streaming](#16-streaming) - - [1.7 The scripting sandbox](#17-the-scripting-sandbox) - - [1.8 Why there is no agent framework](#18-why-there-is-no-agent-framework) -- [Part 2 — Data and correctness](#part-2--data-and-correctness) -- [Part 3 — Production concerns](#part-3--production-concerns) - - [3.6 Counting visits](#36-counting-visits) -- [Part 4 — The web plumbing, briefly](#part-4--the-web-plumbing-briefly) -- [Part 5 — Measured results and known limitations](#part-5--measured-results-and-known-limitations) - ---- - -# Part 0 — Orientation - -## What the thing is - -An AI Dungeon clone. You write a scenario, then play an open-ended text adventure where a -language model narrates the world. You type "I open the door", the model writes what -happens next, and it remembers what came before. - -Four things make it more than a chat wrapper: - -1. **A context engine.** The model has a limited input window. The app decides, every - single turn, which pieces of the story get to be in the prompt and which get dropped. -2. **A world-state engine.** The scenario declares stats (`hp`, `trust`, `day`). The model - proposes changes to them each turn; a Python engine decides what actually sticks. -3. **A story tree.** The story is not a list. Any turn can hold more than one take, and - writing below one that isn't the live one starts a branch that borrows every turn above - the fork rather than copying it. -4. **A scripting sandbox.** Real AI Dungeon JavaScript scripts import and run, inside an - embedded QuickJS interpreter. - -Runs locally against Ollama for free, or hosted against any OpenAI-compatible endpoint. - -## The stack, and what each part is doing - -| Piece | What it does here | -|---|---| -| **FastAPI** (Python) | The HTTP server. Every URL like `/api/adventures/3/actions` maps to a Python function. Also does the SSE streaming. | -| **SQLAlchemy** | The ORM. `Adventure`, `Action`, `Memory` are Python classes; SQLAlchemy turns them into tables and turns attribute access into `SELECT`s. | -| **SQLite / Postgres** | The database. SQLite is a single file on disk (local). Postgres is a server (hosted, on Neon). Same code talks to both. | -| **React** (JavaScript) | The UI. Describes what the screen should look like for a given state; when the state changes it re-renders. | -| **Vite** | The frontend build tool and dev server. Bundles React into plain JS the browser can load. | -| **httpx** | The HTTP client used to call the model endpoint. | -| **tiktoken** | Counts tokens, so the budgeting is arithmetic rather than a guess. | -| **QuickJS** | A small embeddable JavaScript engine, used as a sandbox for user scripts. | - -The whole thing is one process in production: FastAPI serves the API *and* the built React -files from the same port. - -## The shape of one request - -``` -you tap "Do" - → browser sends POST /api/adventures/3/actions {type:"do", text:"open the door"} - → FastAPI route: check ownership, rate limit, turn lock - → assemble the prompt - → POST to the model endpoint with stream=true - → tokens come back one at a time - → each token is forwarded to the browser as a Server-Sent Event - → React appends it to the screen as it arrives - → when the stream ends: parse the state block, referee it, save the action -``` - ---- - -# Part 1 — The AI layer - -## 1.1 The turn pipeline - -Everything that happens between "player pressed a button" and "text is on screen". -Source: `backend/app/routers/adventures.py` (`_generate_turn`). - -``` -player input - → onInput script hook (user JS may rewrite or block it) - → store the player action - → retrieve memories (embed recent story, cosine-rank the bank) - → build_context() (the budget allocator) - → onModelContext script hook (user JS may rewrite the whole prompt) - → snapshot the exact prompt (for the Insights panel) - → provider.generate() (streamed, token by token) - → onOutput script hook - → extract the fenced state block, referee the delta, strip it from the prose - → save the action, stamping on it the script + world state it leaves behind - → fire-and-forget: summarize + embed in the background -``` - -Two design choices are visible in that list before any of the details. - -**The prompt is snapshotted, not reconstructed.** Every AI action stores the exact text -that was sent to the model. That's what powers the Insights panel: open any turn and see -each context component, its token cost, and why it was included. It's also what makes -prompt bugs findable. The cost is storage (~74 KB per turn), which turns into a real -performance problem later. See [2.5](#25-the-189x-egress-fix). - -**Every node records the state it leaves behind.** `state_after` and `world_state_after` -are attached to the action once its hooks and its delta have run, so a node carries the -stats and the RPG values as they stood when that turn finished. Rewinding to *before* -a turn is then a read of the node in front of it, which is the same operation as switching -to another branch: one mechanism, and the reason undo, retry, and branch switching all put -the numbers back instead of only rewriting text. (These were `*_before` fields originally; a -tree needs the *after* value, because a branch's tip is what a reader standing on it should -see.) - ---- - -## 1.2 Context assembly is a budget problem - -Source: `backend/app/context/builder.py`. - -### The problem - -The model can only read so much. Say the budget is 8,000 tokens. A 200-turn adventure has -far more story than that. Something has to be dropped, and *what* gets dropped decides -whether the story stays coherent. - -### The naive version - -Send the last N turns. That breaks in two directions: N turns of short exchanges wastes the -window, and N turns of long ones overflows it. It also throws away the things that matter -most: the premise, the character sheet, the fact that you promised the innkeeper you'd -return. - -### What this app does - -Split the prompt into **fixed** sections and **elastic** ones. - -Fixed (always included, whatever they cost): - -| Section | What it is | -|---|---| -| `narrator` | The system prompt: how to write. | -| `world_state_guide` | The stat legend: what each stat means, its range, its bands. | -| `world_state` | Current values of every stat, plus NPCs in scene. | -| `world_state_rule` | How to report changes. | -| `ai_instructions` | Per-adventure steering. | -| `persona` | Who the player is playing as: name, pronouns, description. Only the player edits it, so it never moves. | -| `plot_essentials` | AI Dungeon's "Memory": the premise. | -| `story_summary` | The auto-maintained running summary. | -| `used_memories` | Top-K retrievals from the memory bank. | - -Elastic (fit into what's left): - -| Section | Rule | -|---|---| -| `world_lore` | Story cards triggered by keywords in recent text. Capped at 40% of the remaining budget. | -| `history` | Story turns, newest first, until the budget runs out. | - -The algorithm is three lines of arithmetic: - -```python -reserved = sum of every fixed section + author's note + length hint + reminder -available = max(256, context_token_budget - reserved) -``` - -Then cards spend up to `available * 0.4`, and history spends `available - cards_used`, -filling backwards from the newest turn. - -### The details that are decisions - -**Cards are capped at 40% of the elastic budget.** Story cards are triggered by keyword -match, so a scene mentioning six named things could pull in six lore entries and leave no -room for the story itself. The cap makes the failure mode "some lore is missing" instead -of "the model has no idea what just happened". Cards that don't fit are still *reported* -to Insights with `included: false`, so the UI can show the lore that got squeezed out. - -**History fills newest-first and stops.** Oldest turns fall out. This is the right -direction because the old material is not actually lost: it has been summarized into -memories and the running summary, which are in the fixed section. - -**If even the single newest turn is over budget, it gets hard-truncated** rather than -dropped. A prompt with no story at all would produce nonsense; a prompt with the tail end -of the last turn produces something. - -**The author's note is injected 3 actions from the end**, not at the top. -`AUTHORS_NOTE_DEPTH = 3`. Instructions placed near the end of a prompt have more influence -on what comes next than instructions at the top, because of recency. The author's note is a -steering control ("keep it tense"), so it goes where steering works. - -**The world-state reminder goes dead last.** The full emit rule lives up in the system -block, hundreds of tokens away from where the model starts writing. A one-line reminder -occupies the final slot. Same recency logic, applied to the thing most likely to be -forgotten. - -**Past AI turns get their state block re-attached.** The state block is stripped from the -text before it's stored, so a replayed history would show the model twenty of its own past -turns that *contain no state block*. That teaches it, by imitation, to stop emitting one. -So `_history_text()` reconstructs the block from the stored delta and re-appends it when -building history. The model sees its own pattern and keeps following it. - -### The performance trap hiding in this - -Building the context needs the newest ~6,000 tokens of story. The obvious implementation -reads `adventure.actions`, which loads every row of the adventure, and then throws 90% of -it away. At turn 200 that was 839 KB of database reads to use maybe 70 KB, and it grew -every single turn. - -`backend/app/context/history.py` fixes it by serving three shapes directly from SQL: a -tail, a slice, and a count. `window_covering()` fetches the newest 32 actions, measures -their real token count, and if that's short of the budget it *projects* how many more it -needs from the average length just measured rather than blindly doubling: - -```python -average = tokens / len(actions) -projected = int(budget / average * 1.15) + 8 -``` - -Each round fetches only what it doesn't already hold, so no row is read twice. Result: the -same turn costs 129 KB instead of 839 KB, and stops growing at around turn 50. The cost is -bounded by the context budget instead of by the length of the story. - -There's a second rule in that module worth naming: **if the actions are already loaded in -memory, slice them instead of querying.** The scripting pipeline hands the whole history to -user scripts (AI Dungeon's API requires it), so on a scripted adventure the rows are already -there. Issuing a query beside them would mean paying twice. - ---- - -## 1.3 World state: the AI proposes, Python referees - -Source: `backend/app/worldstate/engine.py`, `plan/12-phase-rpg-world-state.md`. - -### The problem - -You want an RPG layer: hit points, trust, quest progress. Who owns the numbers? - -### Three options, and why two lose - -**Option A: a deterministic dice engine.** The player types "attack the goblin", the -engine rolls, applies damage, and the model narrates the result. This is what a real RPG -does. It loses here because the action space is unbounded: the player can type anything, and -mapping arbitrary natural language onto a fixed rules system is a harder problem than the -one being solved. - -**Option B: the model owns the numbers.** Let it track hp in the prose and trust it. This -fails immediately. Models are bad at arithmetic, worse at remembering a number across -twenty turns, and completely unable to obey their own frequency rules. Tell one "only -change this every 5 turns" and it will change it every turn. - -**Option C, chosen: the model proposes, the engine disposes.** The model narrates and -appends a JSON delta of what changed. Python validates and clamps it before anything is -stored. - -```` -narration: "The blade catches your shoulder. Gwen shouts and drags you back." - -```state -{"player.hp": -15, "npc.gwen.trust": 5, "milestones.escaped": true} -``` -```` - -The engine then applies, in order: - -| Rule | What it stops | -|---|---| -| Path must exist in the schema | Hallucinated stats | -| Value must be the right type | `"a lot"` instead of `-15` | -| Cooldown | Changing a stat more often than the scenario allows | -| Counters can't decrease | The in-game day going backwards | -| `max_delta_per_turn` | Losing 90 hp to a stubbed toe | -| Clamp to `min`/`max` | Negative hp, trust above 100 | -| Milestones are sticky, `true` only | Un-completing a quest | -| Flags are two-way booleans | (Deliberately unrestricted: that's what flags are for) | - -Everything it rejects is *reported*, not silently swallowed. The Insights panel shows -applied, clamped, and rejected paths per turn, and the chip under each narration shows what -actually changed. - -### The reliability mechanism: word bands - -A stat can carry **bands**: - -```json -"hp": { "min": 0, "max": 100, "initial": 100, - "bands": [[0,20,"very weak"],[20,40,"hurt"],[40,60,"minor damage"], - [60,90,"healthy"],[90,100,"full health"]] } -``` - -Two things use them. The live state block shows the current band label, `hp 55/100 (minor -damage)`, so the model reads a *word*, not just a number. And the stat guide shows the -whole ladder once per turn, so the model can see the full scale it's reasoning across. - -The point: models reason well over semantics and badly over arithmetic. "He's badly hurt, -so a solid hit should take him to very weak" is a judgment a model can make. "55 minus 22 is -33" is one it will get wrong often enough to matter. - -### The failure philosophy - -Nothing in the world-state engine raises. A malformed delta returns `{}` and the turn -continues. The parser is deliberately tolerant: it strips trailing commas and leading `+` -signs on numbers, both of which weaker free models emit and strict JSON rejects. It accepts -a fence labeled `state`, one labeled `json`, or an unlabeled one, and falls back to a bare -JSON object at the end of the text, but only if it parses into something that looks like a -delta, so prose ending in `}` is never eaten. - -This matters because the hosted demo runs on free-tier models. A stricter parser would mean -a good model works and a free one doesn't. - -### One call, not two - -The model narrates *and* emits the delta in a single request. The alternative, narrating and -then making a second call to extract structured state, is more reliable per call and costs -twice the latency and twice the rate-limit budget. On the free tier (20 requests/minute) -that would halve the playable turn rate. The tolerant parser plus the terminal reminder was -the cheaper way to buy the same reliability. - ---- - -## 1.4 Output length, by measurement - -### The problem - -`max_output_tokens` is a hard wall the endpoint enforces mid-sentence. Hit it and whatever -is being written gets cut off. Since the state block is emitted *last*, the state block is -what gets lost. The turn narrates fine and silently records nothing. - -### First attempt - -Tell the model its budget: *"keep this turn under about N words"*. - -### What the measurement showed - -Average turn length went from **174 words to 246**, and every run was longer than every -unhinted run (n=5). Phrased as a budget, the number reads as a *target to fill*. The hint -pushed turns toward the very wall it existed to protect. - -### The fix - -Phrase it as a ceiling, and say explicitly that a typical turn is much shorter: - -``` -[Hard limit: this turn must not exceed 412 words. Write only as much as the -moment needs — a typical turn is much shorter. Finish the narration and append -the state block well inside the limit.] -``` - -Average came back to 170 words, and the state block survived at tight caps. - -### And the arithmetic around it - -```python -words = int((max_output_tokens - 50) * 0.75 * 0.90) -``` - -- `- 50` (`LENGTH_HEADROOM`): tokens held back for the state block itself. -- `* 0.75` (`WORDS_PER_TOKEN`): models can't count their own tokens, but they do follow a - word budget. English prose is roughly 0.75 words per token. -- `* 0.90` (`LENGTH_BUFFER`): a word budget is a suggestion the model overshoots, and the cap - it protects is a hard wall. Aim 10% short so the overshoot lands in slack. -- Below 40 words the hint is dropped entirely: it stops earning its tokens. - ---- - -## 1.5 The memory bank - -Source: `backend/app/memorybank.py`. - -### The problem - -Story history falls out of the context window as the adventure grows. Turn 4 said you -promised the innkeeper you'd return. At turn 90 that's long gone from the prompt, but if -you walk back into the inn, it should come back. - -### The three layers - -``` -raw turns → memories → story summary -(verbatim) (every 6 turns) (rewritten every 15 turns) - ↓ - embeddings → cosine similarity → top-K into the prompt -``` - -| Layer | Cadence | Purpose | -|---|---|---| -| **Memory** | Every 6 actions, starting at 12, once one action sits past the block | One or two past-tense sentences of concrete fact. | -| **Story summary** | Every 15 actions | A single ≤250-word overview of the whole plot, rewritten by folding in the new memories. | -| **Retrieval** | Every turn | Embed the last 4 actions (≤600 tokens), cosine-rank the bank, inject the top K (default 5). | - -Retrieval is the part that answers the innkeeper problem: the promise is a memory, the -memory has a vector, walking into the inn produces a query vector near it, and it comes -back into the prompt. - -### The decisions inside it - -**A memory attaches to the node whose block it ends on.** Not to the adventure, and not to -a *position* in a list of actions, but to a `(branch_id, depth)` coordinate. That is what -makes "which memories described this turn?" an indexed lookup rather than a scan for rows -whose covered range has fallen off the end of the story, and it is what makes memories -inherit correctly across a fork: the ones above the fork point already sit on ancestors -both lines read. - -This is also how repair works. When a turn's text is replaced or removed (a retry, an undo, -a deleted action), `forget_node` withdraws the memory attached to that coordinate and -rewinds both marks to just before the stretch it covered, so the ground is summarized again -from what the story now says. An earlier version instead held the newest action back a turn -so it could never be summarized before it stopped being retryable. Correctness no longer -rests on that, because the repair exists whether or not the invalidation happens at the tip. - -**A block still waits for one action to settle past it** (`SETTLE_SLACK`), and that is a -cost rule rather than a correctness one. Retry and take-switching both refuse anything but -the newest action, so a memory whose block ends on the tip is the one memory a player can -still throw away: every retry of that turn writes it, withdraws it, and writes it again. A -block closes every 6 actions and a normal turn writes 2, so without the slack that is one -turn in three. The slack costs nothing in return, because the block that just closed is -still in the history window in full — a memory of it says what the model can already read. -Memories earn their place once the raw text has scrolled out, which is never the turn the -block closed. - -**Cursors only advance on success.** Every AI call in this module is best-effort. If -summarization fails, the function returns and the cursor is unchanged, so the same block is -retried on a later turn. There is no retry loop, no dead-letter queue, and no backoff: the -cadence *is* the retry mechanism. Failures are logged to the debug page. - -**Summarization is fire-and-forget, in a background task with its own DB session.** The -player's turn is already on screen; making them wait for a summarization call would add a -second or two of latency to every sixth turn for no visible benefit. The task holds strong -references to itself (the event loop only keeps weak ones, so a fire-and-forget task can -otherwise be garbage-collected mid-run) and a per-adventure guard set stops two from -overlapping. - -**Pinned memories count toward `top_k`.** Pinned ones are always injected; unpinned ones -fill up to `top_k - len(pinned)`. Without that, 6 pinned memories plus `top_k=5` injects 11 -and blows the budget the whole context engine exists to respect. - -**A dimension mismatch scores 0.0, it doesn't crash.** If the user changes their embedding -model, old 768-dim vectors get compared against a new 1536-dim query. `zip()` would happily -truncate and score garbage silently. An explicit length check returns 0.0 instead. - -**Eviction is LRU-ish, and evicted memories are kept.** Over capacity (default 200), the -least-used, least-recently-used unpinned memories are marked `forgotten` rather than -deleted, so the UI can still show them and you can un-forget one. - -**Background calls never spend the shared demo key.** The summarization and embedding -providers are built directly from the user's own settings and never from the demo config, -and their call sites are skipped when the turn is running on the demo key. Summarization is -unmetered background spend; the demo key is server-funded. Both facts together would be a -bill. - ---- - -## 1.6 Streaming - -The model produces tokens one at a time. Waiting for the whole reply before showing -anything makes a 20-second generation feel broken. - -**Server-Sent Events (SSE)** is the mechanism: an HTTP response that stays open and pushes -`data: {...}` lines as they become available. It's one-directional (server → browser), -which is exactly what's needed here. WebSockets would be a bidirectional connection for a -unidirectional problem. - -The chain: - -``` -model endpoint --SSE--> FastAPI --SSE--> browser --> React state --> screen -``` - -FastAPI reads the provider's stream, and for each chunk yields -`data: {"type":"chunk","text":"..."}`. The frontend reads the response body with a -`ReadableStream` reader, buffers on `\n\n` boundaries, and dispatches each parsed event. - -Event types: `player` (the stored player action), `reasoning` (thinking-model traces, which -stream into a separate collapsible panel with their own token budget), `chunk` (story -text), `stopped` (a script blocked the turn), `error`, `done`. - -Two production details that only show up when hosted: - -- `X-Accel-Buffering: no`: nginx-style reverse proxies buffer responses by default, which - turns a stream into one big delivery at the end. This header tells them to flush each - event. -- The security-headers and body-size middlewares are written as **pure ASGI** rather than - Starlette's `BaseHTTPMiddleware`, because the latter buffers the response body and would - break streaming. - -**The empty-reply case is diagnosed, not reported as "empty".** If a reasoning model -streams thinking but no story text, it spent its whole budget thinking. The error says so -and tells you which three settings to change. - ---- - -## 1.7 The scripting sandbox - -Source: `backend/app/scripting/`. - -Real AI Dungeon scripts are JavaScript files defining `modifier(text)` and calling it as -the last line, with globals like `state`, `history`, `storyCards`. To be compatible, this -app runs the same contract in an embedded **QuickJS** interpreter. - -The safety properties are mostly structural: - -| Property | How | -|---|---| -| No filesystem, network, or process access | QuickJS has none by default: nothing was removed, nothing was added | -| Memory cap | 16 MB per run | -| CPU cap | 2 seconds per run | -| No shared state between runs | A fresh `Context` per hook execution | -| A broken script can't break a turn | Every failure comes back as `.error` with text/state/cards unchanged; the pipeline logs it and continues | - -Data crosses the boundary as JSON: Python serializes `{state, text, history, storyCards, -info}` in, and the script's results out. There is no object bridge to exploit. - -One deliberate bug-compatibility: `addStoryCard` returns the new card's *index*, so the -first card returns `0`, which is falsy, so `if (!addStoryCard(...))` misfires. That's -upstream AI Dungeon's behavior. It's documented in the code and left alone, because -matching real scripts is the whole point of the feature. - ---- - -## 1.8 Why there is no agent framework - -Graph-based agent frameworks (LangGraph and similar) earn their complexity with -**branching, cyclic, multi-step control flow**: a graph of nodes where the path depends on -what the model decides, with loops, retries, tool calls, and persisted state between steps. - -This turn pipeline is a **fixed linear sequence with exactly one model call**. There is no -routing decision, no tool selection, no loop. The "graph" is: - -``` -hook → retrieve → build → hook → call → hook → referee → store -``` - -Every turn takes that path. Adding a graph framework would mean carrying its state -abstraction, its serialization model, and its debugging surface to express a straight line. - -There is also a specific reason a framework's context handling wouldn't fit: **the -budgeting logic is the product.** Buffer-window and summary-memory abstractions are -opinionated about how to fit history into a window. Here the Insights panel exposes each -context component, its token cost, and the trigger word that pulled it in, so the assembly -has to be explicit and inspectable. - -**When it would be the right call:** if the design went toward the two-call version, where -narration is followed by a separate structured-extraction step, with a retry branch when -extraction fails and a tool-calling path for dice, that is a graph, and hand-rolling it -would get ugly fast. - -**This is not the same branching as the story tree (§2.2).** "No branching" here -describes *control flow*: the code path a single turn takes through the backend. That path -never forks. It is not true of the *data* the app stores. When a player rewinds and plays a -turn differently, that action creates a new branch in the saved history. One turn's -execution is a straight line. The sequence of turns across a playthrough is a tree. The two -claims are about different things and do not conflict. - ---- - -# Part 2 — Data and correctness - -## 2.1 The domain model - -``` -User - ├─ Scenario (the template) ── stat_schema, prompt, memory, author's note - │ └─ StoryCard, Script - └─ Adventure (the playthrough) ── world_state, script_state, head_branch_id/head_depth - ├─ Branch (one line of it) ── parent_branch_id, fork_depth, lineage, name - ├─ Action (one node) ── branch_id, depth, parent_id, live, - │ text, context_snapshot, state_after - ├─ StoryCard (its own copy) - ├─ Memory (text, embedding, branch_id, depth, use_count) - └─ AdventureScript -``` - -**The decision that shapes everything: template vs instance.** A scenario declares what -stats *exist*; an adventure holds what they *are* right now. Creating an adventure copies -the scenario's story cards, scripts and plot fields into it, so editing a scenario later -never mutates a game in progress. (There's an explicit opt-in "Update from scenario" flow -for when you *do* want that, which diffs the two and shows you what would change.) - -Same reasoning as instantiating a class: shared definition, independent state. - -## 2.2 The story is a tree - -The largest structural change the project has had, and the one with the most reasoning -behind it. - -### The problem - -The story used to be a list, and a mutable one. Retry rewrote the last entry in place; -undo and delete removed entries from the middle. Everything derived from the story, such as -the memories, the running summary, and the two marks saying how far each had got, was -indexed by *position in that list*, and a position means something different after -anything in front of it is deleted. - -That single fact produced a family of bugs that all looked different: - -- Deleting a middle action slid a never-summarized action down into the "already - covered" range, so a *recent* action silently never became a memory. -- Discarding a memory left its actions behind the mark, describing nothing. -- Retry rewrote an action's text after the mark had passed it, so its memory described - narration that was no longer in the story. -- The retried row was still attached to the adventure while its replacement was being - written, so the model was shown the attempt it was meant to replace and wrote a - *continuation* of it. That exclusion had to be threaded through four separate readers. -- Attempts lived in a JSON array on the row with a mirrored copy of the live one in the - ordinary columns, which is a repeating group and a denormalisation in one. - -Each was fixed where it was found. The pattern only becomes visible when you line them -up: **they are all the same bug, and it is that the story is a list nobody may reorder.** - -### The shape - -Make the story a tree, and none of them are reachable. - -Every action is a **node** with a `branch_id` and a `depth`. A **branch** is one line -through the tree; it holds the nodes played on it and *borrows* everything before its -fork point from its ancestors. Nothing is ever copied, and apart from an explicit delete, -nothing is ever removed. - -``` -branches(id, adventure_id, parent_branch_id, fork_depth, lineage, name) -actions(id, adventure_id, branch_id, depth, parent_id, live, text, …, state_after) -memories(…, branch_id, depth) -adventures(…, head_branch_id, head_depth) -``` - -`depth` is a position along *a* path, not a global turn number: `A4` and `B4` are two -alternatives, not two turns. Reading branch C, whose tip is at depth 7 and which left B -at 5, which left A at 3: - -```sql -SELECT * FROM actions -WHERE (branch_id = 'C') - OR (branch_id = 'B' AND depth <= 5) - OR (branch_id = 'A' AND depth <= 3) -ORDER BY depth DESC LIMIT 32 -``` - -→ `A0 A1 A2 A3 B4 B5 C6 C7`. - -**Why `branch_id` + `depth` rather than parent pointers alone.** Parent pointers are the -obvious way to store a tree and the wrong way to read one: reading a story would be N -round trips up a chain, which throws away the windowed history work (§1.2) that made a -turn's read cost flat. Depth replaces the old `index` as the ordering key, so the reads -keep the shape they already had. - -### The lineage, and why fork count doesn't cost anything - -The OR-clause above is not reconstructed per read. It is stored on the branch row as -`lineage`, for example `[(C, ∞), (B, 5), (A, 3)]`, computed once when the fork happens, -from the parent's lineage plus one entry. `context/lineage.py` is the only module that -knows how to turn it into a query, which is deliberate: one forgotten clause shows the -wrong story and reports nothing. - -Two properties of the shape do the real work: - -- **The ranges are disjoint and descending.** A branch's own nodes always sit deeper than - its fork point, and each ancestor is capped at the fork depth of the branch beneath it. - So ordering the whole clause by `depth DESC` reads entry 0's nodes, then entry 1's, - then entry 2's, which means a tail read can use the newest few entries and stop. -- **Clause count is bounded by the context window, not by fork count.** A 200-fork story - whose newest branch is 40 turns long reads with *one* clause, because the window is - covered before the second entry is reached. - -A branch stores no story of its own, so a fork costs an id, a parent, a fork depth and a -cached ancestry. Measured on a 40-turn story forked twenty times against the same story -flat: a page load of **31,652 B against 31,433 B, a 1.007× ratio**, or about **103 bytes -per branch**. No migration, no vacuum, no copy. - -### What a player actually does - -None of the above is what the screen shows. In the player's words: - -> Any turn can gain another **take**. On an AI turn that means regenerate; on your own -> message it means type something else. Stepping between takes with `‹ 2/4 ›` is free: -> the story below simply empties, because that take has no children yet. **A branch is -> created when you write below a take that is not the live one**, never before. - -That rule collapses two operations into one and deletes a distinction from the UI. The -first version of this screen had a chip that *switched* at the tip and only *previewed* -above it, with a second button to take that line: one control whose meaning depended on -where the reader was standing. The rule above replaced it with a pager that only ever -steps, a fork button on every turn, and no tip-versus-past distinction at all. The -distinction survives in the implementation, where it decides whether a write needs a -branch: at the tip the attempts are still leaves nobody has built on, so taking one is a -switch and no branch is created. - -### Takes are grouped by parent, not by coordinate - -The load-bearing detail, and the one that is not obvious. - -The natural way to find "the other takes of this turn" is by coordinate: same branch, -same depth. It is wrong in both directions: - -``` -B ── C C1 C2 <- three takes, one parent (B) - │ └── D1' D2' <- two takes, parent C2 - └── D1 D2 D3 <- three takes, parent C1 -``` - -Standing on the C2 path at that depth must read `2/2`, not `5`. Coordinate grouping gets -that one right by accident, because writing under a non-live take forks and the two sets -land on different branches. It gets `C` wrong: once C has been forked onto a branch of -its own it is alone at its coordinate and reads `1/1`, having lost C1 and C2 from a pager -that must still say `1/3`. - -So a node carries `parent_id`, read for nothing but this. The alternative, making a -branch's fork point a *node* rather than a depth so a promoted take never moves, was -rejected: the whole point of `lineage` is that a read is an OR-clause per branch instead -of a walk up parent pointers, and re-pointing the fork at a node changes path resolution -itself, dragging in the cursors, memory depths and both bundle formats. `parent_id` is -one indexed lookup, never a walk, and nothing about how a path resolves changes. - -### Cursors become anchors - -The two marks, how far the memory bank has got and how far the summary has got, used to -be counts. A count is a position in a list, and every rule about sliding them, rewinding -them and translating between positions and `Action.index` existed to patch up the fact -that the list moves. - -A cursor is now an **anchor**: `(branch_id, depth)`, the node up to and including which -the work is done. Deleting an action does not move it, because a depth is a coordinate -along a path rather than a slot in a list. "What is not covered yet" becomes a question -about the story instead of about a list index, and it answers correctly whatever has been -deleted in front of it. The branch half is what makes it survive forking: a depth alone -is ambiguous once two branches both have a node 41. - -`position_of_index`, `note_action_removed`, `settled_story_actions` and the cursor-rewind -machinery were **deleted**, not left unused. So was the one-turn memory holdback that -existed because a retry could rewrite an action the mark had already passed. (`SETTLE_SLACK` -later put one action of slack back, for what redoing a block costs rather than for what it -could get wrong. See "The memory system".) - -### Derived work attaches to the node that produced it - -Generalize the rule and a lot falls out: *anything derived attaches to the node that -produced it*. A memory covering depths 37–42 attaches to that branch's node 42 and is -invisible to any path that does not run through it. Shared ancestors are therefore shared -automatically, so **a fork needs nothing recreated**: the memories above the fork point -are already on the ancestors both lines read. - -The subtle case is the memory sitting *at* the forked coordinate. The first cut moved it -onto the new branch and re-anchored the marks naming it. Both are wrong for the same -reason: that memory describes whichever attempt was live at that coordinate, which is the -one staying on the parent. The right answer needs no code: the lineage caps the parent one -depth short of the fork, so the memory is simply out of range from the new branch, -invisible to both the retrieval clause and the anchor read. The new line summarizes that -ground again, from the text it actually tells. - -Hand-written memories obey the same rule. One used to carry a NULL depth, described as -"belongs to the adventure rather than to a path". That sounds harmless and is not: a NULL -is a coordinate no fork can cap, so a note typed on one line followed the reader onto -branches whose events it never described. They are anchored at the head instead: *the -story you were reading when you wrote it*. - -### Deleting, and why the branch UI was a hard dependency - -Nothing is ever auto-pruned. That is the guarantee the whole design rests on, and it is -also why branch management could not be a nice-to-have: without a way to delete a line, -storage grows without limit. - -The delete rule has two halves and the second is easy to miss. Refusing to delete the -line being read is obvious. The other half is refusing any line it was **forked from**: -`parent_branch_id` cascades, so deleting an ancestor takes the head with it and leaves -`head_branch_id` pointing at a row that is gone. One membership test against the head's -own lineage covers both, because a lineage already names itself and every branch it -borrows from. The server is the authority; the client computes the same set only so a -button can say so before it is pressed. - -### The migration, and what it deliberately did not do - -There is no feature flag. **A linear story is a tree with one branch**, so the -intermediate states were not half-migrated: they were the same product with a superset -schema underneath, which made "existing adventures are unaffected" a literal, testable -pass condition at every step. A flag would have bought two live code paths through the -context builder, the memory bank, undo and retry at once. - -The legacy columns (`index`, `variants`, `variant_index`, the two `*_before` snapshots) -were kept unread for a release rather than dropped with the migration that stopped using -them, so that a redeploy of the previous build is still a way out. Dropping columns is the -one step that isn't. - -One operational note that generalizes: on Postgres, a migration that rewrites every row of -`actions` roughly doubles the table, and only `VACUUM FULL` gives it back: 79 MB reclaimed -in 5.5 s on one occasion. But bloat scales with the **heap**, and `context_snapshot` is 94% -of this table and lives out of line, so a migration touching only small columns reuses the -existing TOAST pointer and costs a tenth of that. Read the sizes from `sum(octet_length())` -per column, not from `n_live_tup`, which is a stale estimate in exactly the direction that -makes bloat look smaller. - -### What this is honest about - -- **The two marks are one pair on the adventure**, not one per branch. Switching branches - makes the mark on the line being left unreadable from the new one, and that ground is - summarized again. It answers "nothing covered", which is the safe direction: redo the - work, never skip it. But switching back and forth costs AI calls. Per-branch cursors - are the fix if it ever matters. -- **Story cards stay adventure-wide.** A card invented on branch B shows on branch A. - Event-sourcing card changes onto nodes was considered and rejected. -- **Editing an already-summarized action still leaves its memory stale.** The machinery to - fix it now exists: an edit could write a sibling take and switch to it, which is a retry - the player typed. It does not do that yet. - -## 2.3 Undo and retry that actually rewind - -Most implementations of undo delete the last message. That's wrong here, because a turn -mutates three things: the text, the scripting scoreboard (`script_state`), and the RPG -stats (`world_state`). - -**The mechanism:** every node carries `state_after` and `world_state_after`, deep copies -of what the adventure looked like once that turn had played. Rewinding to before a turn is -a read of the node in front of it, so undo, retry and a branch switch are the same -restore. The cooldown clock comes along for free: it lives inside the world state, in -`_meta.last_changed`, so each line of the story carries its own without anything having to -know there is one. - -**Nothing a retry replaces is discarded.** The old attempt stays as another **take** of -that turn, a sibling node at the same coordinate with `live` set to false, and the pager -steps between them. Retry is not a special case: it is the tree, with the branch not yet -created. See [2.2](#22-the-story-is-a-tree). - -Three details that are easy to get wrong: - -**The turn being retried is excluded from its own context.** Its takes are still attached -to the adventure, so without `exclude_action_id` the model would be shown the attempt it is -replacing as established story and would write a continuation of it. The exclusion had -leaked into four readers, not one: history replay, story-card trigger matching, in-scene -NPC detection, and the memory-bank similarity query. The invariant is worth stating flatly: -*anything reading the story during generation takes the exclusion.* - -**A retry reuses the turn's depth**, not the next one. Cooldowns are measured along the -path, so allocating a new depth would advance the clock the cooldown rules run on and a -retry would quietly unlock stats that should still be waiting. - -**`delete_turn` used to mean "every take at this coordinate".** Once a take can be forked -onto a branch of its own, the group spans branches, and undo reached across and deleted a -take belonging to a line nobody asked about. Anything that reads a take group and then -*writes* has to say whether it means the turn or the coordinate. - -**If the regeneration fails, the rollback is reversed.** `generate_turn` wraps the -generator in a `try/finally`: if it ends without saving, whether from a provider error, an -empty reply, a script `stop`, or the browser hanging up, the previous take is put back in -charge. Otherwise the state on the server would drift from the text still on the user's -screen. - -## 2.4 The turn lock - -One turn at a time per adventure. Double-clicking "Continue" must not run two generations. - -The subtlety: the check has to happen in the **request phase**, not when the SSE generator -first runs. A `StreamingResponse` doesn't start iterating its generator until the response -begins, so a check-inside-the-generator lets two rapid requests both pass before either one -claims the slot. And because sync FastAPI endpoints run in a threadpool, the test-and-set -needs a real `threading.Lock`. - -```python -def acquire_turn_lock(adventure_id): # in the request handler - with _active_turns_guard: - if adventure_id in _active_turns: - raise HTTPException(409, "A turn is already generating…") - _active_turns.add(adventure_id) - -async def with_turn_lock(adventure_id, gen): # wraps the SSE generator - try: - async for event in gen: yield event - finally: - _active_turns.discard(adventure_id) -``` - -In-memory, so it's a single-process guarantee. That's honest for the deployment this -targets: one Render web service. Two processes would need the lock in the database. - -## 2.5 The 189x egress fix - -**The setup:** `Action.context_snapshot` holds the entire assembled prompt for a turn, -about 74 KB per row, 94% of the database. - -**The bug:** every adventure load pulled that column for every action, to read two small -fields out of it (the world-state delta, for the "what changed" chip, and the applied -report). SQLAlchemy loads all columns by default. - -**The fix, in three parts:** - -1. Move the two small things that *are* needed for every action into their own column - (`Action.world_delta`). -2. Mark the heavy columns `deferred` (`context_snapshot`, `variants`, `reasoning`), so - they're only fetched when explicitly asked for. -3. Backfill the new column with dialect-specific server-side SQL, so the old data is - extracted inside the database and never crosses the wire. - -**The result:** one adventure load went from **38.5 MB to 0.20 MB**. - -**The part that makes it stick:** `tests/test_egress.py` hooks into SQLAlchemy's -`before_cursor_execute` event, captures every statement the ORM sends, and fails if a bulk -load ever names those columns again. The regression is caught by asserting on the *SQL*, -not on a timing. - -One more detail from that test's design: the count query is written as a real -`SELECT count(...)` rather than `query.count()`, because SQLAlchemy's `.count()` wraps the -entity select in a subquery, so the emitted SQL names every column, including the deferred -ones. No bytes come back either way, but the database still has to read them, and a guard -that greps SQL cannot tell the two apart. - -There's a companion denormalization for the same reason: the pager has to know how many -takes a turn has without fetching any of them, so `variant_index` and `variant_count` are -cached on the row and refreshed by one function (`attempts.renumber`), precisely so they -can't drift and the pager can't lie. `variant_count` is 0 rather than 1 for a turn nobody -retried, because the question it answers is "is there anything to page through?" - -## 2.6 Migrations, hand-rolled - -No Alembic. An append-only list of `(version, SQL)` pairs, with the current version stored -in SQLite's `PRAGMA user_version` or a one-row table on Postgres. 64 versions so far. - -- A **fresh** database is created by `Base.metadata.create_all()` (always current) and - stamped at the latest version; it never replays history. -- An **existing** database runs every migration above its stored version, in order. - -Why this and not Alembic: for a single-file SQLite app that a user might have been running -for months, the entire requirement is "add a column, don't lose their data". Alembic's -autogenerate, branching, and down-migrations are machinery for a team with a staging -environment. This is 250 lines and you can read all of it. - -The constraint it creates is written at the top of the file: change `models.py` (so fresh -databases are current) *and* append a pair here (so existing ones upgrade). Migrations 2–23 -predate Postgres support and use SQLite-only syntax. This is harmless, because every -Postgres database starts fresh and never replays them, but anything added since must run -on both dialects. - -One migration worth reading (#10, repairing duplicate action indexes) uses `UPDATE … FROM` -with a window function rather than a correlated subquery, because SQLite may evaluate a -correlated subquery against partially-updated rows and produce duplicates again while -"repairing" them. - ---- - -# Part 3 — Production concerns - -## 3.1 Two modes, one codebase - -`AIDND_MULTI_USER` switches the whole app between two personalities: - -| | Local (default) | Hosted | -|---|---|---| -| Users | One auto-created "local user" | Guest on first visit, optional account | -| Auth | None: no cookies, no login UI | Signed session cookie | -| Rate limits | Off | On | -| Row caps | Off | On | -| API docs (`/docs`) | On | Off | -| Provider | Whatever Settings points at | User's key, or the shared demo key | - -The reasoning: a person running this on their own laptop should never be throttled by their -own app, never see a login screen, and should get the interactive API docs. A hosted -deployment needs all four of those to be the opposite. Rather than two builds, the -differences are gated at each site. - -**Guests upgrade in place.** A visitor gets a guest `User` row on first load. Registering -sets `email` and `password_hash` on that *same row*, so every adventure they played as a -guest survives with no re-parenting and no migration step. Three kinds of row share the -users table: local (email NULL, not guest), guest (email NULL, guest), registered (email -set). - -**A seed file owns its scenario's whole life.** `seed.py` inserts a demo that is -missing, reconciles one whose file changed, and deletes a seeded row no file claims any -more. Matching is by title, so a rename needs the old name under `previous_titles` or it -inserts a second scenario and strands the first. Only rows with a NULL owner and -`is_public` are seeded rows, which is what keeps the sweep away from anything a player -made. An adventure started from a demo that is later removed survives: -`adventures.scenario_id` is `ON DELETE SET NULL`, and the adventure holds its own copies -of the cards and scripts, so it loses only the inherited cover art. - -**Guests start with a story already in progress.** `starter.py` copies a shipped export -bundle into each new guest account at the same point the row is created. An empty account -gives a visitor nothing to read, and the daily demo turns are limited, so learning what -the app does used to cost one of them. The copy is the guest's own from the first moment: -they can edit, branch, delete, or export it, and nothing links it back to the file. The -guest row is committed before the copy is attempted, so a failure there still leaves them -with an account, and the copy itself runs inside a savepoint. - -**Guests expire; accounts don't.** One row per curious visitor adds up, so `cleanup.py` -deletes guests idle for `AIDND_GUEST_RETENTION_DAYS` (default 5), measured as -`COALESCE(last_seen_at, created_at)`, because `_touch` only writes `last_seen_at` hourly -and a guest minted by `/auth/me` has NULL until its second request. The filter requires -both `is_guest` *and* `email IS NULL`, so upgrading in place is also how you opt out of -expiry. It runs once at startup (the reliable trigger on a host that sleeps) and then -every few hours. - -It's a single Core `DELETE`, not `db.delete(user)`: the ORM path would SELECT every -adventure, action and memory into Python purely to delete them, and the FK graph is -`ON DELETE CASCADE` from `users` all the way down, so the database can do the whole graph -in one statement. Nothing a guest owns is visible to anyone else either: `is_public` is -output-only, so shared content is exactly the seeded scenarios, which have `user_id NULL` -and never match the filter. - -## 3.2 The shared demo key - -The demo lets people play with no signup and no API key, on a key the server pays for. That -is a spending surface, so it's the most defended code in the project. - -`resolve_provider_config()` is the single place the BYOK-vs-demo decision is made, and on -the demo branch it pins **two** things: - -- **The model**, pinned to a whitelist. A caller-supplied override or a hand-edited - settings row can't aim a server-funded key at an expensive model. Anything unrecognized - falls back to the first whitelisted model. -- **The endpoint**, pinned to the configured demo URL. Otherwise the key could be - redirected to a URL the user controls and harvested. - -Plus a daily per-user turn cap (default 20), checked *before* the player's input is stored -so a capped player doesn't get their message saved with no reply, and counted only after a -successful turn. - -There's a defensive `__post_init__` on the config object that raises if a demo config -somehow carries a non-whitelisted model. The comment on it records a real bug: the check -tests `using_demo`, **not** `api_key == DEMO_API_KEY`. Keying on the key value looks -stricter but is wrong: the demo key is an ordinary OpenRouter key, so a user can -legitimately paste that same key into their own settings as BYOK, and then every resolution -raised, 500ing even `GET /auth/me` and taking the whole SPA down. `using_demo` is what -actually means "the server is paying". - -Background work (summarization, embeddings) is excluded from the demo key entirely: those -are unmetered calls, and unmetered calls on a server-funded key is a bill. - -## 3.3 Secrets - -Everything derives from one server-side secret (`AIDND_SECRET_KEY`). - -| Thing | Mechanism | -|---|---| -| Passwords | `hashlib.scrypt`, N=2^14, r=8, p=1, per-password salt, constant-time compare. Stdlib, so no extra dependency. | -| Sessions | `v1..`, no expiry: long-lived guest sessions are the point. A cookie can outlive a swept guest row; that resolves to a 401, which the frontend already turns into a fresh session. | -| Stored LLM API keys | Fernet (AES) encryption at rest, key derived from the secret, `enc:` prefix so legacy plaintext rows are recognizable and migratable. | - -The secret auto-generates into a file next to the database for local installs (zero config), -but **multi-user mode refuses to start without the env var**, with an error message that -explains why and gives you the command to generate one. Hosted filesystems are ephemeral; a -regenerated secret on every deploy would silently log out every user and orphan their stored -API keys. - -A rotated secret makes stored keys undecryptable. `decrypt_secret` treats that as "unset" -rather than raising, so the user just re-enters their key instead of hitting a 500. - -## 3.4 Abuse guards - -| Guard | Value | -|---|---| -| Turn generation | 10 / minute | -| Auth attempts | 10 / 5 min, per IP | -| Guest creation | 30 / 5 min, per IP (each guest is a DB row) | -| Script test runs | 30 / minute (each costs up to 2s CPU) | -| Connection test | 10 / minute (outbound HTTP to a user-supplied URL) | -| Adventures / scenarios / scripts per user | 100 / 200 / 200 | -| Actions per adventure | 5,000 | -| Request body | 2 MB, 20 MB on import endpoints | - -Rate limits are keyed per user when one is known (accounts survive IP changes) and per IP -otherwise, in fixed windows held in memory, with a pruning pass so the per-IP dict can't -grow without bound. Import endpoints check bundle list lengths against the same caps live -creation enforces, otherwise the cap is trivially bypassed by uploading a file. - -Security headers on every response: `nosniff`, `X-Frame-Options: DENY`, -`Referrer-Policy: same-origin`, and a CSP allowing exactly what the SPA uses: same-origin -everything, inline styles (React needs them), and Google Fonts. - -## 3.5 Deployment - -One Docker web service on Render, serving the SPA and the API same-origin, with Postgres on -Neon. - -The Postgres decision was forced: Render's free tier has no persistent disk, so a SQLite -file wouldn't survive a deploy. The database lives off-box on Neon's free tier. - -Two things worth knowing about the free tier: - -- The service **sleeps after ~15 minutes idle**, and the first request then takes 30–60s. -- `/api/health` deliberately **doesn't touch the database**, so a keep-warm pinger wakes the - web service without waking the database. Waking a database around the clock costs far more - than the cold start is worth. - -CI runs the backend tests, the frontend lint and build, and a Docker image build on every -push. - -## 3.6 Counting visits - -A hosted demo raises a question a local app never does: is anyone using it, and do they get -anywhere? The answer is an owner-only dashboard at `/analytics`, gated on -`AIDND_ANALYTICS_EMAILS`, a list kept separate from `AIDND_POWER_USERS`, since an unmetered -tester is not automatically someone who should see the traffic. - -**Why it isn't a third-party script.** The CSP allows `script-src 'self'`, so a tracker would -mean loosening it. Ad blockers block the popular ones, which silently biases exactly the -technical audience this project is shown to. And none of them can see the measurement that -matters here: a *turn*. The interesting funnel step is not a pageview. - -**Egress is the budget.** After the 189x fix (§2.5) it would be perverse to add a feature -that reads rows per request. So counts accumulate in a process-local dict and flush every 60 -seconds as UPSERTs: **a visit is a write and never a read**. Storage is a generic -`(day, metric, label) -> hits` counter plus one row per visitor per day for the funnel flags. -Every dashboard query is a `GROUP BY` that returns tens of rows regardless of the traffic -behind it, so a month costs a few kilobytes to read back. The cost of the buffer is that a -hard restart can lose up to a minute; the flusher also runs on shutdown, and on a tier that -sleeps when idle, the buffer it sleeps on is empty anyway. - -**The numbers are the server's, not the browser's.** The client reports one fact, which page -was viewed, and even that is normalized to a route (`/play/12` → `/play/:id`) against a -whitelist, so the page list cannot be polluted by anything a stranger posts. Everything that -means something, such as a turn, an adventure, or a sign-up, is recorded by the code that -performs it. That also fixes a blind spot: a failed turn is an HTTP 200 with a bad ending, so -a status-code tally cannot see it, and a demo whose model has started refusing looks -perfectly healthy from outside. `turn_error` is counted where the SSE error is written. - -The funnel counts **people, not clicks**: a player who starts six adventures is one person -who started an adventure, which is the entire reason the per-visitor-day table exists. - -One smaller decision worth naming: error buckets are labeled by the matched *route -template*, never the requested path. That gives one bucket per endpoint instead of one per -adventure id. The reason it isn't merely tidier is that an unmatched path is entirely -attacker-chosen, so labeling by it would let anyone mint rows. - ---- - -# Part 4 — The web plumbing, briefly - -For the parts that are just how the web works, not decisions. - -**Frontend and backend are two programs.** In development they're two servers: Vite on 5173 -serving React, and FastAPI on 8000 serving the API. Vite proxies `/api` to FastAPI so -the browser thinks it's all one origin, which avoids CORS entirely. In production there's -one server: FastAPI serves the built React files as static assets from the same port. - -**SPA routing.** React Router handles URLs like `/play/3` in the browser without a round -trip. But if you *reload* that URL, the browser asks the server for `/play/3`, which isn't a -file. So `SPAStaticFiles` catches the 404 and returns `index.html`, letting React take over -and read the URL itself. API routes are matched before the static mount, so they're -unaffected. - -**Sessions.** A cookie is a small value the browser stores and automatically attaches to -every request to that site. Here it holds `v1..`. The server doesn't -store sessions anywhere; it re-verifies the signature on each request, which is why there's -no session table. - -**The 401 retry.** If the cookie is missing or stale, any API call returns 401. The frontend -catches that once, calls `/api/auth/me` (which mints a fresh guest session), and retries the -original request. So a returning visitor with an expired cookie never sees an error. - -**React, in one paragraph.** A component is a function that returns a description of some -UI. `useState` holds a value; changing it re-renders the component. The streaming turn is -the clearest example: each SSE chunk appends to a state string, React re-renders, and the -text appears to type itself. - ---- - -# Part 5 — Measured results and known limitations - -## Measured results - -| | | -|---|---| -| Database egress per adventure load | 38.5 MB → **0.20 MB** (~189x) | -| Prompt snapshot size | ~74 KB/turn, 94% of the database | -| Turn read cost at turn 200 | 839 KB → **129 KB**, flat after ~turn 50 | -| Cost of a branch | ~**103 B**; 20 forks load at **1.007×** the same story flat | -| Length-hint phrasing | 174 → 246 words phrased as a budget; **170** phrased as a ceiling (n=5) | -| Backend tests | 440, LLM mocked, real QuickJS engine | -| Schema versions | 64 | -| Sandbox limits | 16 MB, 2 s CPU, fresh context per run | -| Context defaults | author's note at depth 3, cards capped at 40% of elastic budget | -| Memory cadence | memory / 6 turns, summary / 15 turns, top-5 retrieval | - -Two of the tests encode a performance property rather than a behavior: -`test_egress.py` asserts on the SQL the ORM emits, and `test_history_window.py` asserts -that the read cost stops growing with story length. - -## Known limitations - -Deliberate trades for a single-user-first app that also happens to be hosted, listed so -nobody has to discover them the hard way. - -- **Single process.** The turn lock, the rate limiter and the summarization task all assume - one worker. A second worker would need the lock in the database (a row-level advisory - lock) and the rate limiter in Redis. -- **No vector index.** Retrieval does cosine similarity in Python over the whole bank. Fine - at the 200-memory cap; at 10,000 it would want pgvector. -- **Prompt snapshots are heavy** even after the egress fix; they're deferred, not smaller. - Compressing them or expiring old ones is the real fix. -- **In-memory rate-limit windows reset on restart**, so a restart grants a brief extra - allowance. -- **Background summarization is a fire-and-forget asyncio task**, so it does not survive a - restart. At real load it belongs in a queue. -- **The demo key depends on a free-tier provider's daily cap**, which the app can only - detect after the fact by string-matching the 429 body. -- **The two memory marks are one pair on the adventure, not one per branch.** Switching - lines makes the mark on the line being left unreadable from the new one, so that ground is - summarized again. It fails in the safe direction: redo, never skip. But switching back - and forth costs AI calls. Per-branch cursors are the fix if it matters. -- **Story cards are adventure-wide**, so a card invented on one branch shows on all of them. -- **Editing an already-summarized turn leaves its memory stale.** Replacing a turn withdraws - what was derived from it; editing one in place does not. - -## Cleanup backlog - -`docs/self-review.md` carries an open list of non-bugs (reuse, simplification, and -efficiency items) kept deliberately separate from the correctness list, which is empty. -The largest ones: - -- `Section.tokens` is uncached, so the context gets tokenized two or three times a turn. -- `onModelContext` flattens system and story into one string before handing it to user - scripts; if a script modifies it, the structure is gone and everything ships as user - content. Passing structure through the hook would be better but would break AI Dungeon - compatibility, which is the point of the feature. -- The import endpoints hand-coerce raw dicts instead of using Pydantic bundle schemas. -- The legacy pre-tree columns (`index`, `variants`, `variant_index`, and the two `*_before` - snapshots) are still on `actions`, unread, kept for one release so redeploying the previous - build remains a way out. Dropping them is a migration that rewrites every row, so it owes a - `VACUUM FULL actions;` after it. - ---- - -*Source: [github.com/parththakkar106/AI-DnD](https://github.com/parththakkar106/AI-DnD) · -[Project page](https://parththakkar106.github.io/AI-DnD/)* diff --git a/docs/architecture.html b/docs/architecture.html deleted file mode 100644 index 49b14f2..0000000 --- a/docs/architecture.html +++ /dev/null @@ -1,1233 +0,0 @@ - - - - - -AI D&D Engine Room - - - - - - - - - - - - - -
- - - -
- -
-

AI-DnD · architecture notes

-

The Engine Room

-

An AI Dungeon clone is a chat wrapper until four things are true. The prompt is budgeted. The numbers are refereed. The story is a tree. Someone else’s JavaScript runs in a sandbox. This page shows how each one is built, and what it cost to learn.

-
-
440backend tests
-
64schema versions
-
189×egress cut
-
103 Bcost of a branch
-
1model call per turn
-
-
- -
- -
-
HLD
-

0.1  What the thing is

-

You write a scenario, then play an open-ended text adventure where a language model narrates the world. You type “I open the door”, the model writes what happens next, and it remembers what came before.

-

Four subsystems carry the weight, and every hard problem in the codebase belongs to one of them:

-
    -
  1. A context engine. The model has a finite input window. The app decides, every single turn, which pieces of the story get into the prompt and which get dropped.
  2. -
  3. A world-state engine. The scenario declares stats (hp, trust, day). The model proposes changes each turn; Python decides what actually sticks.
  4. -
  5. A story tree. The story is not a list. Any turn can hold more than one take, and writing below a take that isn’t live starts a branch that borrows every turn above the fork rather than copying it.
  6. -
  7. A scripting sandbox. Real AI Dungeon JavaScript imports and runs, inside an embedded QuickJS interpreter.
  8. -
-

It runs locally against Ollama for free, or hosted against any OpenAI-compatible endpoint.

-
- -
-
HLD
-

0.2  The stack, and what each part is doing

-
- - - - - - - - - - - -
PieceJob in this system
FastAPIHTTP server. Routes, plus the SSE streaming.
SQLAlchemyORM. Adventure, Action, Memory are Python classes; attribute access becomes SELECTs — which is exactly how the egress bug happened.
SQLite / PostgresOne file on disk locally; Neon Postgres when hosted. Same code, two dialects.
React + ViteThe SPA. Built files are served by FastAPI, same origin, same port.
httpxCalls the model endpoint, streaming.
tiktokenCounts tokens, so budgeting is arithmetic rather than a guess.
QuickJSEmbeddable JS engine used as the user-script sandbox.
-
-
- -
- -
-

Part 1 — The high-level design

-

What the boxes are, who talks to whom, and the two or three decisions that every later decision inherits.

-
- -
- -
-
HLD
-

1.1  One process, two things outside it

-

Production is a single Docker web service on Render. It serves both the API and the built SPA. Only two things live off-box: the database and the model endpoint.

-

This shape is not an accident. It is what makes the in-process turn lock and the in-memory rate limiter honest. It is also what the Known limitations list is measured against.

-
- -
- -
-
- - - - - - - - - - - - - Browser - React SPA - ReadableStream - reader - localStorage - - - - POST - - SSE tokens - - - - One Render web service - uvicorn · single worker - - - FastAPI routes - - static SPA - (same origin) - - - context builder - + referee - - QuickJS - 16 MB / 2 s - - - in-memory: turn lock · rate windows - visit counters · summarize tasks - all single-process by design - - - - SQL - - Neon Postgres - scale-to-zero, 5 min - egress is the bill - - - - https - - Model endpoint - OpenAI-compatible - Ollama / OpenRouter - - - - GET /api/health never touches the DB — a keep-warm - pinger wakes the web service without waking Postgres. - -
-
The whole system. The brass box is one process: everything inside it shares memory, which is why the turn lock and rate limiter work and also why a second worker would break both.
-
- -
- -
-
HLDbackend/app/models.py
-

1.2  The domain model: template versus instance

-
User
- ├─ Scenario   (the template)     ── stat_schema, prompt, memory, author's note
- │    └─ StoryCard, Script
- └─ Adventure  (the playthrough)  ── world_state, script_state,
-      │                              head_branch_id, head_depth
-      ├─ Branch  (one line of it) ── parent_branch_id, fork_depth, lineage, name
-      ├─ Action  (one node)       ── branch_id, depth, parent_id, live,
-      │                              text, context_snapshot, state_after
-      ├─ StoryCard  (its own copy)
-      ├─ Memory     (text, embedding, branch_id, depth, use_count)
-      └─ AdventureScript
-

The decision that shapes everything else: a scenario declares what stats exist. An adventure holds what they are right now. Creating an adventure copies the scenario’s story cards, scripts, and plot fields into it. Editing a scenario later never mutates a game in progress. This is the same reasoning as instantiating a class: shared definition, independent state.

-

The opt-in escape hatch is Update from scenario. GET /adventures/{id}/refresh returns a diff: per-field old and new values, card additions, updates and removals, and world-state paths added or removed. POST to the same path applies it under the turn lock. It never touches the opening action, the adventure’s title, its summary, or player-authored cards.

-
- The trap that made it possible -

Re-copying scenario text would have re-injected literal ${Hero} placeholders. The answers were consumed once at creation and thrown away. And without story_cards.source_ref (card:<id> / npc:<key>, NULL for player-authored), there was no link back. A rename read as delete-plus-add, and player cards would have been clobbered. Two migrations were the price of one feature.

-
-
- -
-
HLDrouters/adventures.py · _generate_turn
-

1.3  The turn pipeline

-

Everything between “player pressed a button” and “text is on screen”. Teal steps are user-script hooks — the points where someone else’s JavaScript gets to rewrite the turn.

-
    -
  1. onInput hookUser JS may rewrite or block the input outright.
  2. -
  3. store the player actionWritten before generation, so a failed turn still shows what you typed.
  4. -
  5. retrieve memoriesEmbed the last 4 actions, cosine-rank the bank, take the top K.
  6. -
  7. build_context()The budget allocator. Fixed sections first, elastic ones into what is left.
  8. -
  9. onModelContext hookUser JS may rewrite the entire assembled prompt.
  10. -
  11. snapshot the exact promptStored on the action. Powers Insights; costs ~74 KB a turn, which becomes §3.1.
  12. -
  13. provider.generate()One streamed call. Tokens forwarded to the browser as they arrive.
  14. -
  15. onOutput hookLast chance for user JS to touch the text.
  16. -
  17. extract + referee the state blockParse the fenced JSON delta, clamp it, strip it from the prose.
  18. -
  19. save the nodeStamped with state_after and world_state_after — the scoreboard as this turn leaves it.
  20. -
  21. fire-and-forget: summarize + embedBackground task, own DB session, never on the shared demo key.
  22. -
-

Two choices are visible in that list before any detail. The prompt is snapshotted, not reconstructed. That makes prompt bugs findable, and it makes the database expensive. Every node also records the state it leaves behind. Rewinding to before a turn is a read of the node in front of it. That single move is why undo, retry, and a branch switch are the same restore.

-
- -
-
HLDAIDND_MULTI_USER
-

1.4  Two personalities, one codebase

-

One env var switches the whole app. Not two builds — the differences are gated at each site.

-
- - - - - - - - - - -
Local (default)Hosted
UsersOne auto-created local userGuest on first visit, optional account
AuthNone — no cookies, no loginSigned session cookie
Rate limitsOffOn
Row capsOffOn
/docsOnOff
ProviderWhatever Settings points atUser’s key, or the shared demo key
-
-

Someone running this on their own laptop should never be throttled by their own app. They should never see a login screen, and they should get the interactive API docs. A hosted deployment needs all four of those to be the opposite.

-

Guests upgrade in place. A visitor gets a guest User row on first load. Registering sets email and password_hash on that same row, so every adventure played as a guest survives with no re-parenting.

-

Guests idle past AIDND_GUEST_RETENTION_DAYS are swept by a single Core DELETE. The FK graph is ON DELETE CASCADE the whole way down. Without it, the ORM path would have pulled every action and memory into Python just to delete them.

-
- -
-
HLD
-

1.5  Why there is no agent framework

-

Graph-based agent frameworks earn their complexity with branching, cyclic, multi-step control flow. Their path depends on what the model decides, with loops, tool calls, and persisted state between steps. This pipeline is a fixed linear sequence with exactly one model call. There is no routing decision, no tool selection, no loop.

-

There is a second, more specific reason. The budgeting logic is the product. Buffer-window and summary-memory abstractions are opinionated about how to fit history into a window. Here, the Insights panel exposes each context component, its token cost, and the trigger word that pulled it in. Assembly has to be explicit and inspectable.

-
When it would flip: imagine the design used two calls instead. First narrate, then run a separate structured-extraction step, with a retry branch when extraction fails and a tool path for dice. That is a graph, and hand-rolling it would get ugly fast.
-
This is not the same branching as the story tree. “No branching” here describes control flow: the code path a single turn takes through the backend. That path never forks. “No branching” is not true of the data the app stores. See §2.6: when a player rewinds and plays a turn differently, that action does create a new branch in the saved history. One turn's execution is a straight line. The sequence of turns across a playthrough is a tree. The two claims are about different things and do not conflict.
-
- -
- -
-

Part 2 — The low-level design

-

The arithmetic, the schemas and the invariants. Each of these is a place where the obvious implementation is wrong, and the reason it is wrong was found by measurement or by a bug.

-
- -
- -
-
LLDbackend/app/context/builder.py
-

2.1  Context assembly is a budget problem

-

Say the budget is 8,000 tokens. A 200-turn adventure has far more story than that. The naive fix is to send the last N turns, but that breaks in both directions. N short exchanges waste the window. N long ones overflow it. Either way, it throws away the premise and the promise you made to the innkeeper.

-

So the prompt is split into two kinds of sections. Fixed sections are included whatever they cost. Elastic ones fit into what is left.

- -
-
- fixed sections - cards ≤ 40% - history, newest first -
-
- fixed → narrator · stat guide · live state · emit rule · ai_instructions · plot · summary · memories - elastic → story cards, then story turns -
-
- -
reserved  = every fixed section + author's note + length hint + emit reminder
-available = max(256, context_token_budget - reserved)
-cards      spend up to available * 0.4
-history    spends available - cards_used, filling backwards from newest
- -

The details that are decisions

-
    -
  • Cards are capped at 40% of the elastic budget. Story cards trigger on keyword match. A scene naming six things could pull six lore entries and leave no room for the story. The cap turns that failure into “some lore is missing” instead of “the model has no idea what just happened”. Cards that do not fit are still reported to Insights with included: false.
  • -
  • History fills newest-first and stops. Old material is not lost. It has already been summarized into memories and the running summary, both of which sit in the fixed block.
  • -
  • If even the newest turn is over budget, it is hard-truncated, not dropped. A prompt with no story produces nonsense. A prompt with the tail of the last turn produces something.
  • -
  • The author’s note is injected 3 actions from the end (AUTHORS_NOTE_DEPTH = 3), not at the top. It is a steering control, so it goes where steering works best: recency.
  • -
  • The world-state reminder takes the very last slot. The full emit rule lives hundreds of tokens up in the system block. A one-line reminder occupies the position closest to where the model starts writing.
  • -
- -
- Reliability by imitation -

The state block is stripped from text before storage. Replayed history then showed the model twenty of its own past turns with no state block, teaching it by example to stop emitting one. Once it missed a turn, it never recovered, and retry did not help.

-

The fix has two halves, both gated on the adventure actually having a schema. First, a one-line EMIT_REMINDER sits in the recency slot. Second, _history_text() reconstructs each past turn’s delta block from the stored delta and re-appends it. Action.text stays clean, so the UI, the embeddings, and card trigger-matching are unaffected. History carries only the per-turn delta, for format imitation. The full scoreboard is rendered once, up top.

-
-
- -
-
LLDbackend/app/context/history.py
-

2.2  The performance trap hiding inside that

-

Building the context needs only the newest ~6,000 tokens of story. The obvious implementation reads adventure.actions, which loads every row of the adventure and then discards 90% of it. At turn 200 that was 839 KB read to use about 70 KB, and it grew every turn.

-

history.py serves three shapes straight from SQL: a tail, a slice, and a count. window_covering() fetches the newest 32 actions and measures their real token count. If that falls short, it projects how many more it needs from the average it just measured, instead of blindly doubling:

-
average   = tokens / len(actions)
-projected = int(budget / average * 1.15) + 8
-

Each round fetches only what it does not already hold, so no row is read twice. The same turn now costs 129 KB, flat from about turn 50. The cost is bounded by the context budget, not by the length of the story.

-
Second rule in that module: if the actions are already loaded, slice them instead of querying. The scripting pipeline hands the whole history to user scripts because AI Dungeon’s API requires it. On a scripted adventure the rows are already in memory, so a query beside them would mean paying twice.
-
- -
-
LLDbackend/app/worldstate/engine.py
-

2.3  World state: the AI proposes, Python referees

-

Three options, and two of them lose.

-
    -
  • A deterministic dice engine. What a real RPG does. It loses here because the action space is unbounded. Mapping arbitrary natural language onto a fixed rules system is harder than the problem being solved.
  • -
  • Let the model own the numbers. Fails immediately. Models are bad at arithmetic, worse at holding a number across twenty turns, and unable to obey their own frequency rules. Tell one “change this at most every 5 turns” and it changes it every turn.
  • -
  • The model proposes, the engine disposes. Chosen. The model narrates and appends a JSON delta. Python validates and clamps it before anything is stored.
  • -
-
narration: "The blade catches your shoulder. Gwen shouts and drags you back."
-
-```state
-{"player.hp": -15, "npc.gwen.trust": 5, "milestones.escaped": true}
-```
-
- - - - - - - - - - - - -
Rule applied, in orderWhat it stops
Path must exist in the schemaHallucinated stats
Value must be the right type"a lot" instead of -15
CooldownChanging a stat more often than the scenario allows
Counters can’t decreaseThe in-game day going backwards
max_delta_per_turnLosing 90 hp to a stubbed toe
Clamp to min/maxNegative hp, trust above 100
Milestones are sticky, true onlyUn-completing a quest
Flags are two-way booleansNothing — deliberately unrestricted
-
-

Everything rejected is reported, not silently swallowed: Insights shows applied, clamped and rejected paths per turn, and a chip under each narration shows what actually changed.

- -

Word bands are the reliability mechanism

-
"hp": { "min": 0, "max": 100, "initial": 100,
-        "bands": [[0,20,"very weak"], [20,40,"hurt"], [40,60,"minor damage"],
-                  [60,90,"healthy"], [90,100,"full health"]] }
-

The live state line shows the current band label, like hp 55/100 (minor damage). The model reads a word, not just a number. The stat guide also prints the whole ladder once per turn. Models reason well over semantics and badly over arithmetic. “He’s badly hurt, so a solid hit takes him to very weak” is a judgment a model can make. “55 minus 22 is 33” is one it gets wrong often enough to matter.

- -

Two philosophies underneath

-

Nothing in this engine raises. A malformed delta returns {} and the turn continues. The parser strips trailing commas and leading + signs. It accepts a fence labelled state, one labelled json, or an unlabelled one. It falls back to a bare object hugging the end of the text, but only if that object parses into something delta-shaped, so prose ending in } is never eaten. This tolerance exists because the hosted demo runs on free-tier models. A stricter parser would mean good models work and free ones don’t.

-

One call, not two. Narrate-then-extract is more reliable per call, but it costs twice the latency and twice the rate-limit budget. On a 20 req/min free tier, that halves the playable turn rate. The tolerant parser plus the terminal reminder buys the same reliability for less cost.

-
- -
-
LLDbuilder.length_hint()
-

2.4  Output length, decided by measurement

-

The hint’s whole job is protecting the state block, which is emitted last and is therefore what truncation eats. So it must shorten output. The first version lengthened it.

-
- - - - - - - -
Phrasing, cap 800, n=5Mean words
No hint174
“Keep this turn under about 506 words.”246
“Hard limit … must not exceed 506 words … a typical turn is much shorter.”170
-
-

A budget reads to the model as a target to fill. Every one of the five budget runs was longer than every unhinted run, 41% longer on average. That pushes output toward the wall the hint exists to avoid. Ceiling phrasing is statistically indistinguishable from no hint at loose caps, but it still works at tight ones. At cap 250, unhinted runs hit finish_reason: length 2 times in 6. Ceiling-hinted runs hit it 0 times in 6.

-

A one-sided ceiling turned out to be half a fix. Across other models, the same prompt gave wildly different lengths. A terse model has nothing to act on but “much shorter”, so it collapses to two paragraphs. The hint is now a band with deliberately asymmetric bounds, so neither side reads as a number to hit:

-
must not exceed 506 words, and it should not stop short of about 177.
-Prefer the lower end of that range unless the scene genuinely needs more.
-
-LENGTH_FLOOR_SHARE   = 0.35 of the ceiling
-MIN_LENGTH_FLOOR_WORDS = 60    # below this the floor is dropped and the
-MAX_LENGTH_FLOOR_WORDS = 300   # tight-cap string stays byte-identical
-
The transferable rule: phrase every number in a prompt as a bound, never a target. Give it two sides. A one-sided hint just moves each model further in whichever direction it already leaned. Test it against no-hint on at least two models with opposite verbosity biases before trusting it.
-
- Shipped unmeasured, and known to be -

No test was run on the band wording. Two risks remain open. A stated range may invite landing mid-range on verbose models. Truncation is still silent, since nothing in the app reads finish_reason yet. The better design is a target_length preference separate from max_output_tokens, which is currently a safety wall doubling as the length dial. It was rejected as too big for the ask.

-
-
- -
-
LLDbackend/app/memorybank.py
-

2.5  The memory bank

-

Turn 4 said you promised the innkeeper you’d return. At turn 90, that is long gone from the prompt. But if you walk back into the inn, it should come back. Three layers make that happen:

-
- - - - - - - -
LayerCadenceWhat it is
Memoryevery 6 actions, from 12One or two past-tense sentences of concrete fact.
Story summaryevery 15 actionsA single ≤250-word overview, rewritten by folding in the new memories.
Retrievalevery turnEmbed the last 4 actions (≤600 tokens), cosine-rank the bank, inject the top 5.
-
-

Retrieval is what answers the innkeeper problem. The promise is a memory. The memory has a vector. Walking into the inn produces a query vector near it.

-
    -
  • A memory hangs off the node whose block it ends on. That is a (branch_id, depth) coordinate, not the adventure and not a position in a list. This makes “which memories described this turn?” an indexed lookup. It is also why memories inherit correctly across a fork: the ones above the fork point already sit on ancestors both lines read.
  • -
  • Cursors only advance on success. Every AI call here is best-effort. If summarization fails, the cursor is unchanged, and the same block is retried on a later turn. There is no retry loop, no backoff, no dead-letter queue. The cadence is the retry mechanism.
  • -
  • Pinned memories count toward top_k. Otherwise, 6 pinned plus top_k=5 injects 11 and blows the budget the whole context engine exists to respect.
  • -
  • A dimension mismatch scores 0.0, it does not crash. Change your embedding model, and old 768-dim vectors meet a 1536-dim query. zip() would happily truncate and score garbage silently.
  • -
  • Eviction is LRU-ish and non-destructive. Over capacity (default 200), the least-used unpinned memories are marked forgotten instead of deleted, so you can un-forget one.
  • -
  • Background calls never spend the shared demo key. Summarization and embedding providers are built from the user’s own settings, never from the demo config. Unmetered background calls on a server-funded key would be a bill.
  • -
-
- The measurement that had been running with the feature off -

Two full rounds of egress work ran with the memory bank effectively disabled. Retrieval needs an embedding model, and embedding providers are BYOK-only by construction. So the demo never embeds, and the stress harness had none configured. Measured later in production: 134 memories, 1536 dims, ~31 KB each as JSON text. The ranking walked adventure.memories, so the whole bank crossed the wire every turn just to pick five. On a 100-memory adventure that was 3,024 KB per turn against 129 KB for everything else, about 96% of a turn. Break-even is 4.2 memories.

-

This is not a repeat of the earlier fix. There is no repeating group, no denormalization. It is a format problem (JSON floats at 20 bytes where a float is 4) plus a fetch-frequency problem. Rule: any egress measurement must run with an embedding model configured.

-
-
- -
- -
-
-
LLDHLDcontext/lineage.py · tree.py · attempts.py
-

2.6  The story is a tree

-

The largest structural change the project has had, and the one with the most reasoning behind it.

-

The story used to be a list, and a mutable one. Retry rewrote the last entry in place. Undo and delete removed entries from the middle. Everything derived from the story was indexed by position in that list: memories, the running summary, the two marks saying how far each had got. A position means something different once anything in front of it is deleted. That one fact produced a family of bugs that all looked different:

-
    -
  • Deleting a middle action slid a never-summarized action into the “already covered” range. A recent action silently never became a memory.
  • -
  • Discarding a memory left its actions behind the mark, describing nothing.
  • -
  • Retry rewrote text after the mark had passed it. The memory then described narration no longer in the story.
  • -
  • The retried row stayed attached while its replacement was written. The model was shown the attempt it was meant to replace and wrote a continuation of it. That exclusion had to be threaded through four separate readers.
  • -
  • Attempts lived in a JSON array on the row, with a mirrored copy of the live one in the ordinary columns. That is a repeating group and a denormalization in one.
  • -
-
Each was fixed where it was found. Lined up, the pattern becomes visible: they are all the same bug. The story is a list nobody may reorder.
-
-
- -
-
- - - - - - - - - depth - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - - - - A - - - - - - - not on C’s path - - - - - - B - - - - - not on C’s path - - - - - - C - - - head - - - - branches.lineage on C, computed once at fork, never walked: - [ (C, ∞), (B, 5), (A, 3) ] - → ORDER BY depth DESC reads entry 0, then 1, then 2 — and stops. - -
-
Reading branch C. Filled nodes are on the path; the ranges are disjoint and descending, so a tail read consumes the newest entries and never reaches the rest. Clause count is bounded by the context window, not by fork count.
-
- -
-
-

The shape

-
branches(id, adventure_id, parent_branch_id, fork_depth, lineage, name)
-actions (id, adventure_id, branch_id, depth, parent_id, live, text, …, state_after)
-memories(…, branch_id, depth)
-adventures(…, head_branch_id, head_depth)
-

depth is a position along a path, not a global turn number: A4 and B4 are two alternatives, not two turns. Reading branch C is one query:

-
SELECT * FROM actions
-WHERE (branch_id = 'C')
-   OR (branch_id = 'B' AND depth <= 5)
-   OR (branch_id = 'A' AND depth <= 3)
-ORDER BY depth DESC LIMIT 32          -- → A0 A1 A2 A3 B4 B5 C6 C7
-

Why branch_id + depth, and not parent pointers alone. Parent pointers are the obvious way to store a tree, but they are the wrong way to read one. Reading a story would need N round trips up a chain, which throws away the windowing work of §2.2. Depth replaces the old index as the ordering key, so reads keep the shape they already had. ltree was rejected because it breaks SQLite dev parity and its path column grows per row. Closure tables were rejected for being O(n²).

-

A branch stores no story of its own. A fork costs only an id, a parent, a fork depth, and a cached ancestry. Measured on a 40-turn story forked twenty times, against the same story flat: a page load of 31,652 B against 31,433 B, or 1.007×, about 103 bytes per branch. No migration, no vacuum, no copy.

- -

What a player actually does

-
Any turn can gain another take. On an AI turn that means regenerate. On your own message it means type something else. Stepping between takes with ‹ 2/4 › is free, since the story below simply empties: that take has no children yet. A branch is created when you write below a take that is not the live one, never before.
-

That rule collapses two operations into one and deletes a distinction from the UI. The first version of this screen had a chip that switched at the tip and only previewed above it, with a second button to take that line. One control's meaning depended on where the reader was standing. It shipped, was driven by hand, and was found unusable. The replacement is a pager that only ever steps, a fork button on every turn, and no tip-versus-past distinction at all.

- -

Takes are grouped by parent, not by coordinate

-

This is the load-bearing detail, and it is not obvious. The natural way to find “the other takes of this turn” is by coordinate: same branch, same depth. That is wrong in both directions:

-
B ── C   C1   C2              <- three takes, one parent (B)
-          │    └── D1' D2'    <- two takes, parent C2
-          └── D1 D2 D3        <- three takes, parent C1
-

Standing on the C2 path at that depth must read 2/2, not 5. Coordinate grouping gets that right by accident: writing under a non-live take forks, and the two sets land on different branches. It gets C wrong. Once C is forked onto a branch of its own, it sits alone at its coordinate and reads 1/1, having lost C1 and C2 from a pager that must still say 1/3.

-

So a node carries parent_id, read for nothing else. The alternative was making a branch’s fork point a node rather than a depth, and it was rejected. The whole point of lineage is that a read is an OR-clause per branch instead of a walk. Re-pointing the fork at a node changes path resolution itself, dragging in the cursors, the memory depths, and both bundle formats. parent_id is one indexed lookup, never a walk.

- -

Cursors become anchors

-

The two marks, how far the memory bank has got and how far the summary has got, used to be counts. A count is a position in a list. Every rule about sliding, rewinding, and translating between positions and Action.index existed to patch up the fact that the list moves.

-

A cursor is now an anchor: (branch_id, depth), the node up to and including which the work is done. Deleting an action does not move it. “What is not covered yet?” becomes a question about the story instead of a list index, and it answers correctly no matter what has been deleted in front of it. The branch half is what makes it survive forking. A depth alone is ambiguous once two branches both have a node 41.

-

position_of_index, note_action_removed, settled_story_actions, and the cursor-rewind machinery were deleted, not left unused. So was the one-turn memory holdback that existed because a retry could rewrite an action the mark had already passed. (SETTLE_SLACK later put one action of slack back, for what redoing a block costs rather than for what it could get wrong.)

- -
- Three findings only hand-driving produced -

A sibling group breaks every query that assumed one row per depth. _latest_narration ordered by (depth desc, id desc) and got the newest attempt rather than the live one. The index screen quoted a take the player had thrown away. Anything ranking actions by coordinate needs live in the filter.

-

delete_turn meant “every take at this coordinate”. Once the group spans branches, undo reached onto another line and deleted a take nobody asked about. Anything that reads a take group and then writes has to say whether it means the turn or the coordinate.

-

The adventure GET does not build ActionOut. It hands the window to the relationship with set_committed_value and lets Pydantic walk it. Patching every place that builds ActionOut still misses this one path, and every page load takes it.

-
- -

One review finding was rejected, and that is the part worth keeping. A memory the player types lands on the head’s coordinate. A retry withdraws every memory at that coordinate, so the note disappears. That is reproduced and real, but it is the rule working: a memory anchored to a node describes that node and goes when the node goes. The root node is the one exception. The pre-tree bank was parked on depth 0, the one depth every branch can see, so withdrawing it would retire a whole bank in a click.

-
- -
-
LLD
-

2.7  Undo and retry that actually rewind

-

Most implementations of undo delete the last message. That is wrong here, because a turn mutates three things: the text, the scripting scoreboard (script_state), and the RPG stats (world_state).

-

Every node carries state_after and world_state_after, deep copies of the adventure once that turn had played. Rewinding to before a turn is a read of the node in front of it, so undo, retry, and a branch switch are the same restore. The cooldown clock comes along free. It lives inside the world state at _meta.last_changed, so each line of the story carries its own without anything having to know there is one.

-

Nothing a retry replaces is thrown away. The old attempt stays as another take, a sibling node with live false, and the pager steps between them. Retry is not a special case. It is the tree with the branch not yet created.

-
    -
  • The turn being retried is excluded from its own context. Its takes are still attached to the adventure. Without exclude_action_id, the model is shown the attempt it is replacing as established story. The exclusion had leaked into four readers: history replay, story-card trigger matching, in-scene NPC detection, and the memory-bank similarity query. Anything reading the story during generation takes the exclusion.
  • -
  • A retry reuses the turn’s depth, not the next one. Cooldowns are measured along the path. A new depth would advance the clock the cooldown rules run on, and it would quietly unlock stats that should still be waiting.
  • -
  • If regeneration fails, the rollback is reversed. generate_turn wraps the generator in try/finally. A provider error, an empty reply, a script stop, or the browser hanging up puts the previous take back in charge. Otherwise server state drifts from the text still on the user’s screen.
  • -
-
- -
-
LLD
-

2.8  The turn lock

-

One turn at a time per adventure. The subtlety is where the check goes. A StreamingResponse does not start iterating its generator until the response begins. A check inside the generator would let two rapid requests both pass before either claims the slot. And because sync FastAPI endpoints run in a threadpool, the test-and-set needs a real threading.Lock.

-
def acquire_turn_lock(adventure_id):          # in the REQUEST handler
-    with _active_turns_guard:
-        if adventure_id in _active_turns:
-            raise HTTPException(409, "A turn is already generating…")
-        _active_turns.add(adventure_id)
-
-async def with_turn_lock(adventure_id, gen):  # wraps the SSE generator
-    try:
-        async for event in gen: yield event
-    finally:
-        _active_turns.discard(adventure_id)
-

The lock is in-memory, so it is a single-process guarantee. That is honest for the deployment it targets. Two workers would need the lock in the database.

-
- -
-
LLD
-

2.9  Streaming

-

The app uses Server-Sent Events, not WebSockets. Traffic is one-directional, and a bidirectional connection for a unidirectional problem is cost with no return. FastAPI reads the provider’s stream and yields data: {"type":"chunk","text":"…"}. The frontend reads the body with a ReadableStream reader, buffers on \n\n boundaries, and dispatches each event. Event types: player, reasoning (thinking traces, into their own collapsible panel with their own budget), chunk, stopped, error, done.

-
- Two things that only appear when hosted -

X-Accel-Buffering: no. nginx-style reverse proxies buffer by default, turning a stream into one delivery at the end.

-

The security-headers and body-size middlewares are written as pure ASGI rather than Starlette’s BaseHTTPMiddleware. The latter buffers the response body, which would break streaming outright.

-
-

The empty-reply case is diagnosed, not just reported as “empty”. If a reasoning model streams thinking but no story text, it spent its whole budget thinking. The error says so and names the three settings to change.

-
- -
-
LLDbackend/app/scripting/
-

2.10  The QuickJS sandbox

-

Real AI Dungeon scripts define modifier(text) and call it as the last line, with globals like state, history, and storyCards. The same contract runs here, inside an embedded QuickJS interpreter. The safety properties are mostly structural. Nothing was removed, because nothing was there to begin with.

-
- - - - - - - - - -
PropertyHow
No filesystem, network or process accessQuickJS has none by default
Memory cap16 MB per run
CPU cap2 seconds per run
No shared state between runsA fresh Context per hook execution
A broken script can’t break a turnEvery failure returns .error with text, state and cards unchanged
-
-

Data crosses the boundary as JSON. Python serializes {state, text, history, storyCards, info} in, and the script’s results come back out the same way. There is no object bridge to exploit.

-

One bug-compatibility is deliberate: addStoryCard returns the new card’s index. The first card returns 0, which is falsy, so if (!addStoryCard(...)) misfires. That is upstream AI Dungeon’s behavior. Matching real scripts is the entire point of the feature.

-
- -
- -
-

Part 3 — Running it in public

-

A hosted demo on a free tier turns three things into engineering problems that a local app never has: bytes on the wire, a spending surface, and strangers.

-
- -
- -
-
LLDmigration 36 · tests/test_egress.py
-

3.1  The 189× egress fix

-

Neon’s free tier allows 5 GB of network transfer per project per billing period, and it hard-blocks at connection time. There is no read-only grace. You cannot even pg_dump your way out once it trips. This app tripped it.

-

The diagnosis came from the monitoring tab, not a guess. The database was ~55 MB, and had transferred 5 GB. Compute was idle most of the day, and the pooler never exceeded three connections. That is the whole database, ninety times over: a payload-per-request problem, not traffic volume and not a leak.

-

Action.context_snapshot holds the entire assembled prompt, about 74 KB per row and 94% of the database. Every adventure load pulled it for every action, just to read two small fields out of it. SQLAlchemy loads all columns by default.

-
- - - - - - - -
StepWhat it does
1Move the two things actually needed per action into their own column, Action.world_delta.
2Mark the heavy columns deferred — context_snapshot, variants, reasoning — so they load only when asked for.
3Backfill with dialect-specific server-side SQL (json_extract on SQLite, #> on Postgres) so the 39 MB never crosses the wire.
-
-

One adventure load went from 38.5 MB to 0.20 MB. A second round found two more problems. Action.variants was bulk-fetched just to compute a count, fixed the same way plus a real variant_count column. And story_actions() walked the relationship, so a turn was O(story length), which is what §2.2 replaced. A 200-turn playthrough went from 84.5 MB to 23.0 MB. A delete went from 115 KB to 5 KB.

-
What makes it stick: tests/test_egress.py hooks SQLAlchemy’s before_cursor_execute. It captures every statement the ORM emits and fails if a bulk load ever names those columns again. The regression is caught by asserting on the SQL, not on a timing. It was verified by sabotage.
-
- Four traps from that work -

Query.count() wraps the entity select in a subquery, so the emitted SQL names every deferred column. No bytes come back, but the database still reads them. A SQL-grepping guard cannot tell that apart from a real bulk fetch. Use db.query(func.count(Action.id)) instead.

-

SQL and Python must agree exactly on what counts as a story action, or cursors point at the wrong one. trim() strips only spaces, so fold \n\r\t with replace() first.

-

Read table sizes from column sums, not n_live_tup. After a migration rewrites a table, that estimate goes stale in the direction that makes bloat look smaller. It said ~40 MB was reclaimable; the real figure was 79 MB. sum(octet_length(col)) is the honest number.

-

After a migration that rewrites actions, run one VACUUM FULL actions; on the direct endpoint, not the pooler. It takes an ACCESS EXCLUSIVE lock. And DROP COLUMN is metadata-only in Postgres, so it frees nothing by itself.

-
-
The cost trap, recorded so nobody undoes the fix: Neon compute is ~95% of the bill and depends only on awake hours. Never add an uptime pinger that touches the database to dodge Render’s cold start. It holds the database awake around the clock and turns ~$0.30/month into ~$19. Point any warmer at /api/health instead, which deliberately does not query the database.
-
- -
-
LLDbackend/app/migrations.py
-

3.2  Migrations, hand-rolled

-

No Alembic. An append-only list of (version, SQL) pairs, with the current version in SQLite’s PRAGMA user_version or a one-row table on Postgres. 64 versions so far.

-
    -
  • A fresh database is created by Base.metadata.create_all() — always current — and stamped at the latest version. It never replays history.
  • -
  • An existing database runs every migration above its stored version, in order.
  • -
-

Someone may run this single-file SQLite app for months. The entire requirement is “add a column, don’t lose their data”. Alembic’s autogenerate, branching, and down-migrations are machinery for a team with a staging environment. This is 250 lines, and you can read all of it.

-

The constraint it creates is written at the top of the file. Change models.py so fresh databases are current, and append a pair so existing ones upgrade. Migrations 2–23 predate Postgres support and use SQLite-only syntax. That is harmless, because every Postgres database starts fresh, but anything added since must run on both dialects.

-
- Migration #10, and a subtle correctness bug -

Repairing duplicate action indexes uses UPDATE … FROM with a window function rather than a correlated subquery. SQLite may evaluate a correlated subquery against partially-updated rows, which would produce fresh duplicates while “repairing” them.

-

Separately, the test suite only ever builds fresh databases. It does not cover migrations as upgrades. Check the upgrade path by hand whenever one is added.

-
-
- -
-
LLDHLD
-

3.3  The spending surface, and the strangers

-

The demo lets people play with no signup and no API key, on a key the server pays for. That makes it the most defended code in the project.

-

resolve_provider_config() is the single place the BYOK-versus-demo decision is made. On the demo branch it pins two things. It pins the model to a whitelist, so no caller-supplied override can aim a server-funded key at an expensive model. It pins the endpoint to the configured demo URL, so the key cannot be redirected to a URL the user controls and harvested. A defensive __post_init__ raises if a demo config somehow carries a non-whitelisted model.

-
- The check that took the site down -

That backstop tests using_demo, not api_key == DEMO_API_KEY. Keying on the key value looks stricter, but it is wrong. The demo key is an ordinary OpenRouter key, so a user can legitimately paste that same value into their own settings as BYOK. Every resolution then raised, 500ing even GET /auth/me, which is the SPA’s bootstrap call. Nothing rendered at all.

-

/auth/me is a single point of failure for the whole frontend. Anything it touches must not be able to raise. And using_demo is what actually means “the server is paying”.

-
- -

Secrets

-

Everything derives from one server-side secret. Passwords use hashlib.scrypt (N=214, r=8, p=1, per-password salt, constant-time compare); this is a stdlib function, so it needs no extra dependency. Sessions are v1.<user_id>.<HMAC-SHA256> with no expiry, because long-lived guest sessions are the point. Stored LLM keys are Fernet-encrypted at rest with an enc: prefix, so legacy plaintext rows stay recognizable. The secret auto-generates for local installs, but multi-user mode refuses to start without the env var. Hosted filesystems are ephemeral. A regenerated secret on every deploy would silently log out every user and orphan their stored API keys.

- -

Two findings from an authorized pen-test

-

Rate limits were bypassable (High, confirmed live). The Dockerfile ran uvicorn with --forwarded-allow-ips "*", which trusts the leftmost X-Forwarded-For value. Render forwards rather than strips the inbound client XFF, appending the real client IP on the right. So the leftmost hop was fully attacker-controlled, and every rotation bought a fresh rate-limit bucket. Proof: a fixed IP hit 429 after 10 login attempts; rotating a spoofed XFF passed 14 of 14. That defeated the only anti-brute-force control in the app.

-

Fixed in two independent layers. limits._client_ip() now reads the rightmost hop, the one Render appends, which a client cannot push past. It is tunable via AIDND_TRUSTED_PROXY_HOPS. A new per-account throttle also applies: 8 failures per 15 minutes, keyed on the target email, so a botnet with many real IPs still cannot brute one account. The accepted tradeoff is that an attacker can keep a known account in a 15-minute cooldown. That is a nuisance, and it is strictly better than brute force.

-

SSRF via the BYOK endpoint (Medium). A hosted user could point Settings.endpoint_url at an internal or metadata address, and the connection test echoed part of the response back. netguard.endpoint_block_reason resolves the host and refuses anything where not ip.is_global. It checks at request time, so it resists DNS rebinding. It is a deliberate no-op in local mode, since a local install reaching localhost:11434 is the intended case.

-

What held, and is worth naming: per-object authorization was solid throughout. Every {id} route filters on user_id, and sub-resources re-check they belong to their parent. The API key is write-only in every response shape. Session tokens are unforgeable. The sandbox has no host bindings.

- -

The guard rails, in numbers

-
- - - - - - - - - - - - - -
GuardLimit
Turn generation10 / min
Auth attempts, per IP10 / 5 min
Guest creation, per IP (each is a DB row)30 / 5 min
Script test runs (each costs up to 2 s CPU)30 / min
Connection test (outbound HTTP to a user URL)10 / min
Adventures / scenarios / scripts per user100 / 200 / 200
Actions per adventure5,000
Request body, and on import endpoints2 MB / 20 MB
Demo turns per user per day20
-
-

Import endpoints check bundle list lengths against the same caps live creation enforces. Otherwise the cap is bypassed by uploading a file. That check had a bug worth remembering: a cap has to count what gets written, not what the file says. The import counted a v1 file’s turns, but each turn expands into a row per saved attempt. A file inside a 5,000-action cap could write 50,000 rows.

-
- -
-
LLDanalytics.py · accesslog.py
-

3.4  Counting visits without undoing §3.1

-

The obvious way to build analytics is to write a row per request and read rows per dashboard query. That would have undone the entire egress fix. So a visit is a write and never a read. Counts accumulate in a process-local dict and flush every 60 seconds as UPSERTs into a generic (day, metric, label) → hits table, plus one row per visitor per day for the funnel flags. Every dashboard query is a GROUP BY. It returns tens of rows no matter how much traffic sits behind it.

-

The counters are anonymous and the access log beside them is not, on purpose. A visitor in the counter tables is HMAC(secret, "visitor:<user id>") truncated to 32 chars. It is one-way, so those tables cannot be joined back to users, and keyed, so no client can compute one. Story content never reaches that module. The identifying half lives in a separate module and a separate table, so the anonymity of the counters is a property of the code rather than a convention.

-
    -
  • The funnel counts people, not clicks. A player who starts six adventures is one person who started an adventure. That is the whole reason the per-visitor-day table exists. Its flags only ever turn on.
  • -
  • A failed turn is an HTTP 200 with a bad ending. Status-code middleware cannot see one. A demo whose model started refusing every request would look perfectly healthy. All five SSE error paths now go through one turn_error() helper.
  • -
  • The tests run on SQLite; production is Neon. A flush that raises is caught and logged. A dialect mistake in the UPSERTs would have stayed invisible while the dashboard quietly stayed empty. One test compiles both statements against the Postgres dialect without connecting to one.
  • -
-
- -
- -
-

Part 4 — The scoreboard

-

What the work bought, and what it deliberately did not.

-
- -
- -
-
HLD
-

Measured results

-
- - - - - - - - - - - - - - - - -
PropertyFigure
Database egress per adventure load38.5 MB → 0.20 MB
Turn read cost at turn 200839 KB → 129 KB, flat
200-turn playthrough, total reads84.5 MB → 23.0 MB
Cost of a branch~103 B · 20 forks load at 1.007×
Prompt snapshot size~74 KB/turn, 94% of the DB
Length hint, budget vs ceiling phrasing246 vs 170 words (n=5)
Backend tests440
Schema versions64
Sandbox limits16 MB, 2 s, fresh context
Context defaultsnote at depth 3, cards ≤ 40%
Memory cadencememory /6, summary /15, top-5
Actual hosting bill~$0.30/mo, mostly $0 collected
-
-

Two of those tests encode a performance property rather than a behavior: test_egress.py asserts on the SQL the ORM emits, and test_history_window.py asserts that the read cost stops growing with story length.

-
- -
-
HLD
-

Known limitations

-

Deliberate trades for a single-user-first app that also happens to be hosted, written down so nobody has to discover them the hard way.

-
    -
  • Single process. The turn lock, the rate limiter and the summarization task all assume one worker. A second would need a row-level advisory lock and Redis.
  • -
  • No vector index. Retrieval does cosine similarity in Python over the whole bank. Fine at the 200-memory cap; at 10,000 it wants pgvector.
  • -
  • Prompt snapshots are heavy even after the egress fix. They are deferred, not smaller. Compressing or expiring them is the real fix.
  • -
  • In-memory rate-limit windows reset on restart, so a restart grants a brief extra allowance.
  • -
  • Background summarization is a fire-and-forget asyncio task, so it does not survive a restart. At real load it belongs in a queue.
  • -
  • The two memory marks are one pair on the adventure, not one per branch. Switching lines makes the mark on the line being left unreadable, so that ground is summarized again. It fails in the safe direction — redo, never skip — but switching back and forth costs AI calls.
  • -
  • Story cards are adventure-wide, so a card invented on one branch shows on all of them.
  • -
  • Editing an already-summarized turn leaves its memory stale. Replacing a turn withdraws what was derived from it; editing one in place does not.
  • -
  • The frontend has no test runner. This is the standing reason the project keeps finding UI bugs by hand — every one of the tree-UI findings above was invisible to 440 green backend tests.
  • -
  • Truncation is silent. Nothing reads finish_reason yet, which is the obvious next step for §2.4.
  • -
-
- - - -
- -
-
- - diff --git a/docs/guide.html b/docs/guide.html deleted file mode 100644 index 1db98b8..0000000 --- a/docs/guide.html +++ /dev/null @@ -1,1577 +0,0 @@ - - - - - -AI D&D — the engineering guide - - - - - - - - - -
- -
- ⚔ AI D&D - Repo → -
- -
- -
-

Engineering guide

-

How this thing works, and why it works that way

-

An AI Dungeon-style storytelling engine. The chat loop is the boring part — - the interesting parts are the token-budget allocator, the world-state referee, the story tree - that lets a turn have more than one answer, and the memory system that decides what the model is - allowed to remember.

-

- Written to be read end to end. Every section states the decision, the reasoning behind - it, and what it cost. · - Project page · - Source -

-
- - - - -
-

Part 0

-

Orientation

-

What the thing is, in the fewest words that are still true.

-
- -
- -

0.1What it is

- -

An AI Dungeon clone. You write a scenario, then play an open-ended text adventure where a -language model narrates the world. You type “I open the door”, the model writes what happens -next, and it remembers what came before.

- -

Four things make it more than a chat wrapper:

- -
    -
  1. A context engine. The model has a limited input window. The app decides, - every single turn, which pieces of the story get to be in the prompt and which get dropped.
  2. -
  3. A world-state engine. The scenario declares stats — hp, - trust, day. The model proposes changes each turn; a Python engine - decides what actually sticks.
  4. -
  5. A story tree. The story is not a list. Any turn can hold more than one - take, and writing below one that isn’t the live one starts a branch — which borrows every turn - above the fork instead of copying it.
  6. -
  7. A scripting sandbox. Real AI Dungeon JavaScript scripts import and run, - inside an embedded QuickJS interpreter.
  8. -
- -

Runs locally against Ollama for free, or hosted against any OpenAI-compatible endpoint.

- -

0.2The stack, and what each part is doing

- -
- - - - - - - - - - - - -
PieceWhat it actually does here
FastAPIThe HTTP server. Every URL like /api/adventures/3/actions maps to a Python function. Also does the SSE streaming.
SQLAlchemyLets you write Python classes instead of SQL. Adventure, Action, Memory are Python classes; SQLAlchemy turns them into tables and turns attribute access into SELECTs.
SQLite / PostgresThe database. SQLite is one file on disk (local). Postgres is a server (hosted, on Neon). Same code talks to both.
ReactThe UI. Describes what the screen should look like for a given state; when the state changes it re-renders.
ViteThe frontend build tool and dev server. Bundles React into plain JS the browser can load.
httpxThe Python HTTP client used to call the model endpoint.
tiktokenCounts tokens, so the budgeting is real arithmetic and not a guess.
QuickJSA small embeddable JavaScript engine, used as a sandbox for user scripts.
-
- -

The whole thing is one process in production: FastAPI serves the API and the built -React files from the same port.

- -

The shape of one request

- -
you tap "Do"
-  → POST /api/adventures/3/actions
-       {type: "do", text: "open the door"}
-  → check ownership, rate limit, turn lock
-  → assemble the prompt      ← the interesting part
-  → POST to the model endpoint, stream=true
-  → tokens come back one at a time
-  → each is forwarded on as a Server-Sent Event
-  → React appends it to the screen as it arrives
-  → stream ends: parse the state block, referee
-       it, save the action
- -
- - -
-

Part 1

-

The AI layer

-

Where most of the design effort went. Everything here is a decision someone could - reasonably disagree with.

-
- -
- -

1.1The turn pipeline

- -

Everything that happens between “player pressed a button” and “text is on screen”. -Source: backend/app/routers/adventures.py.

- -
-
onInput — user JS may rewrite or block the input
-
store the player action
-
retrieve memories — embed recent story, cosine-rank the bank
-
build_context() — the budget allocator
-
onModelContext — user JS may rewrite the whole prompt
-
snapshot the exact prompt — for the Insights panel
-
provider.generate() — streamed, token by token
-
onOutput
-
extract + referee the state block — then strip it from the prose
-
save the action — stamped with the state it leaves behind
-
background: summarize + embed — fire-and-forget
-
- model or prompt work - user script hook - persistence -
-
- -

Two design choices are visible in that list before any of the details.

- -

The prompt is snapshotted, not reconstructed. Every AI action stores the -exact text that was sent to the model. That’s what powers the Insights panel — open any turn and -see each context component, its token cost, and why it was included. It’s also what makes prompt -bugs findable. The cost is storage, about 74 KB per turn, which turns into a real performance -problem later (see 2.5).

- -

Every node records the state it leaves behind. state_after and -world_state_after are stapled onto the action once its hooks and its delta have run, -so a node carries the scoreboard and the RPG stats as they stood when that turn finished. Rewinding -to before a turn is then a read of the node in front of it — the same move as switching to -another branch. One mechanism, and it is why undo, retry and a branch switch all put the numbers -back rather than only rewriting text.

- -

1.2Context assembly is a budget problem

- -

Source: backend/app/context/builder.py.

- -

The problem

- -

The model can only read so much. Say the budget is 8,000 tokens. A 200-turn adventure has far -more story than that. Something has to be dropped, and what gets dropped decides whether -the story stays coherent.

- -

The naive version, and why it breaks

- -

Send the last N turns. That fails in two directions: N turns of short exchanges wastes the -window, and N turns of long ones overflows it. Worse, “the last N turns” throws away the things -that matter most — the premise, the character sheet, the fact that you promised the innkeeper -you’d return.

- -

What this app does

- -

Split the prompt into fixed sections and elastic ones.

- -
- - - - - - - CONTEXT TOKEN BUDGET - - - - - - - - - - - - RESERVED - CARDS ≤ 40% - HISTORY, NEWEST FIRST - - - - available = budget − reserved - - narrator · stat guide · world state · emit rule · ai instructions - plot essentials · story summary · retrieved memories - ↑ these are always included, whatever they cost - -
Fixed sections are reserved first and never dropped. What’s left is the elastic -budget: triggered story cards may take up to 40% of it, and story history spends the remainder -filling backwards from the newest turn.
-
- -

The algorithm is three lines of arithmetic:

- -
reserved  = every fixed section + note + hint + reminder
-available = max(256, token_budget - reserved)
-
-cards   ≤ available * 0.4
-history = available - cards_used, newest first
- -

The details that are actually decisions

- -

Cards are capped at 40% of the elastic budget. Story cards are triggered by -keyword match, so a scene mentioning six named things could pull in six lore entries and leave no -room for the story itself. The cap makes the failure mode “some lore is missing” instead of “the -model has no idea what just happened”. Cards that don’t fit are still reported to -Insights with included: false, so the UI can show the lore that got squeezed out.

- -

History fills newest-first and stops. Oldest turns fall out. That’s the right -direction because the old material isn’t actually lost — it’s been summarized into memories and -the running summary, which live in the fixed section.

- -

If even the single newest turn is over budget, it gets hard-truncated rather -than dropped. A prompt with no story at all produces nonsense; a prompt with the tail end of the -last turn produces something.

- -

The author’s note is injected three actions from the end, not at the top. -Instructions placed near the end of a prompt have more influence on what comes next than -instructions at the top — recency. The author’s note is a steering control (“keep it tense”), so -it goes where steering works.

- -

The world-state reminder goes dead last. The full emit rule lives up in the -system block, hundreds of tokens away from where the model starts writing. A one-line reminder -occupies the final slot. Same recency logic, applied to the thing most likely to be forgotten.

- -
- The subtle one -

Past AI turns get their state block re-attached. The block is stripped from - the text before storage, so a replayed history would show the model twenty of its own past - turns that contain no state block — teaching it, by imitation, to stop emitting one. - So the history builder reconstructs the block from the stored delta and re-appends it. The - model sees its own pattern and keeps following it.

-
- -

The performance trap hiding in this

- -

Building the context needs the newest ~6,000 tokens of story. The obvious implementation reads -adventure.actions — which loads every row of the adventure — then throws 90% of it -away. At turn 200 that was 839 KB of database reads to use maybe 70 KB, growing every turn.

- -

context/history.py fixes it by serving three shapes directly from SQL: a tail, a -slice, and a count. window_covering() fetches the newest 32 actions, measures their -real token count, and if that’s short of the budget it projects how many more it needs -from the average length just measured, rather than blindly doubling:

- -
average   = tokens / len(actions)
-projected = int(budget / average * 1.15) + 8
- -

Each round fetches only what it doesn’t already hold, so no row is read twice. The same turn -costs 129 KB instead of 839 KB, and stops growing at around turn 50 — the cost is bounded by the -context budget instead of by the length of the story.

- -

There’s a second rule in that module worth naming: if the actions are already loaded -in memory, slice them instead of querying. The scripting pipeline hands the whole history -to user scripts, because AI Dungeon’s API requires it, so on a scripted adventure the rows are -already there — issuing a query beside them would mean paying twice.

- -

1.3World state: the AI proposes, Python referees

- -

Source: backend/app/worldstate/engine.py.

- -

The question

- -

You want an RPG layer — hit points, trust, quest progress. Who owns the numbers?

- -
-
-
Option A — a deterministic dice engine
-
The engine rolls and applies damage, the model narrates the result. This is what a real - RPG does. It loses here because the action space is unbounded: the player can type anything, - and mapping arbitrary natural language onto a fixed rules system is a harder problem than the - one being solved.
-
-
-
Option B — the model owns the numbers
-
Track hp in the prose and trust it. Fails immediately. Models are bad at arithmetic, worse - at holding a number across twenty turns, and completely unable to obey their own frequency - rules — tell one “only change this every 5 turns” and it changes it every turn.
-
-
-
Option C — chosen: propose and dispose
-
The model narrates and appends a JSON delta of what changed. Python validates and clamps - it before anything is stored. The model owns intent; the engine owns arithmetic.
-
-
- -
narration: "The blade catches your shoulder. Gwen shouts and drags you back."
-
-```state
-{"player.hp": -15, "npc.gwen.trust": 5, "milestones.escaped": true}
-```
- -

The engine then applies, in order:

- -
- - - - - - - - - - - - -
RuleWhat it stops
Path must exist in the schemaHallucinated stats
Value must be the right type"a lot" instead of -15
CooldownChanging a stat more often than the scenario allows
Counters can’t decreaseThe in-game day going backwards
max_delta_per_turnLosing 90 hp to a stubbed toe
Clamp to min/maxNegative hp, trust above 100
Milestones sticky, true onlyUn-completing a quest
Flags are two-way booleansDeliberately unrestricted — that’s what flags are for
-
- -

Everything rejected is reported, not silently swallowed. The Insights panel shows -applied, clamped and rejected paths per turn, and the chip under each narration shows what -actually changed.

- -

The reliability mechanism: word bands

- -

A stat can carry bands:

- -
"hp": { "min": 0, "max": 100, "initial": 100,
-        "bands": [[0,20,"very weak"],[20,40,"hurt"],[40,60,"minor damage"],
-                  [60,90,"healthy"],[90,100,"full health"]] }
- -

Two things use them. The live state block shows the current band label — -hp 55/100 (minor damage) — so the model reads a word, not just a number. And -the stat guide shows the whole ladder once per turn, so the model can see the full scale it’s -reasoning across.

- -

The point: models reason well over semantics and badly over arithmetic. “He’s badly hurt, so a -solid hit should take him to very weak” is a judgement a model can make. “55 minus 22 is 33” is -one it will get wrong often enough to matter.

- -

The failure philosophy

- -

Nothing in the world-state engine raises. A malformed delta returns {} and the -turn continues. The parser is deliberately tolerant — it strips trailing commas and leading -+ signs on numbers, both of which weaker free models emit and strict JSON rejects. It -accepts a state, json or unlabelled fence, and falls back to a bare JSON -object at the end of the text, but only if it parses into something that looks like a delta, so -prose ending in } is never eaten.

- -

This matters because the public demo runs on free-tier models. A stricter parser would mean a -good model works and a free one doesn’t.

- -

One call, not two

- -

The model narrates and emits the delta in a single request. The alternative — narrate, -then a second call to extract structured state — is more reliable per call and costs twice the -latency and twice the rate-limit budget. On the free tier (20 requests/minute) that would halve -the playable turn rate. The tolerant parser plus the terminal reminder was the cheaper way to buy -the same reliability.

- -

1.4Output length, by measurement

- -

The problem

- -

max_output_tokens is a hard wall the endpoint enforces mid-sentence. Hit it and -whatever is being written gets cut off. Since the state block is emitted last, the state -block is what gets lost. The turn narrates fine and silently records nothing.

- -

First attempt, and the measurement

- -

Tell the model its budget: “keep this turn under about N words”.

- -
-
174 → 246
average words per turn once the “budget” hint was added — every run longer than every unhinted run (n=5)
-
170
average after rephrasing the same number as a hard ceiling
-
- -

Phrased as a budget, the number reads as a target to fill. The hint pushed turns -toward the very wall it existed to protect.

- -

The fix

- -
[Hard limit: this turn must not exceed 412 words. Write only as much as the
-moment needs — a typical turn is much shorter. Finish the narration and append
-the state block well inside the limit.]
- -

And the arithmetic around it

- -
words = int((max_output_tokens - 50) * 0.75 * 0.90)
- -
    -
  • - 50 — tokens held back for the state block itself.
  • -
  • * 0.75 — models can’t count their own tokens, but they do follow a word budget. - English prose is roughly 0.75 words per token.
  • -
  • * 0.90 — a word budget is a suggestion the model overshoots; the cap it protects - is a hard wall. Aim 10% short so the overshoot lands in slack.
  • -
  • Below 40 words the hint is dropped entirely — it stops earning its tokens.
  • -
- -

1.5The memory bank

- -

Source: backend/app/memorybank.py.

- -

The problem

- -

Story history falls out of the context window as the adventure grows. Turn 4 said you promised -the innkeeper you’d return. At turn 90 that’s long gone from the prompt — but if you walk back -into the inn, it should come back.

- -

Three layers

- -
- - - - - - - -
LayerCadencePurpose
Memoryevery 6 actions, from 12, once one action sits past the blockOne or two past-tense sentences of concrete fact.
Story summaryevery 15 actionsA single ≤250-word overview, rewritten by folding in the new memories.
Retrievalevery turnEmbed the last 4 actions (≤600 tokens), cosine-rank the bank, inject the top 5.
-
- -

Retrieval is what answers the innkeeper problem: the promise is a memory, the memory has a -vector, walking into the inn produces a query vector near it, and it comes back into the prompt.

- -

The decisions inside it

- -

A memory hangs off the node whose block it ends on. Not off the adventure, and -not off a position in a list of actions — off a (branch_id, depth) coordinate. -That makes “which memories described this turn?” an indexed lookup rather than a scan for rows whose -covered range has fallen off the end of the story, and it is what makes memories inherit correctly -across a fork: the ones above the fork point already sit on ancestors both lines read.

- -

It is also the repair. When a turn’s text is replaced or removed — a retry, an undo, a deleted -action — forget_node withdraws the memory hanging off that coordinate and -rewinds both marks to just before the stretch it covered, so that ground is summarized again from -what the story now says. An earlier version instead held the newest action back a turn so it could -never be summarized before it stopped being retryable; correctness no longer rests on that, because -the repair exists whether or not the invalidation happens at the tip.

- -

A block still waits for one action to settle past it -(SETTLE_SLACK), and that is a cost rule rather than a correctness one. Retry and -take-switching both refuse anything but the newest action, so a memory whose block ends on the tip -is the one memory a player can still throw away: every retry of that turn writes it, withdraws it, -and writes it again. A block closes every 6 actions and a normal turn writes 2, so without the -slack that is one turn in three. The slack costs nothing in return, because the block that just -closed is still in the history window in full — a memory of it says what the model can already -read. Memories earn their place once the raw text has scrolled out, which is never the turn the -block closed.

- -

Cursors only advance on success. Every AI call here is best-effort. If -summarization fails, the function returns and the cursor is unchanged, so the same block is -retried on a later turn. There’s no retry loop, no dead-letter queue, no backoff — the cadence -is the retry mechanism.

- -

Summarization is fire-and-forget, in a background task with its own DB session. -The player’s turn is already on screen; making them wait would add a second or two of latency -every sixth turn for no visible benefit. The task holds a strong reference to itself — the event -loop only keeps weak ones, so a fire-and-forget task can otherwise be garbage-collected mid-run — -and a per-adventure guard stops two from overlapping.

- -

Pinned memories count toward top_k. Pinned ones are always -injected; unpinned fill up to top_k − len(pinned). Without that, 6 pinned memories -plus top_k=5 injects 11 and blows the budget the whole context engine exists to -respect.

- -

A dimension mismatch scores 0.0, it doesn’t crash. If the user changes their -embedding model, old 768-dim vectors get compared against a new 1536-dim query. -zip() would happily truncate and score garbage, silently. An explicit length check -returns 0.0 instead.

- -

Eviction is LRU-ish, and evicted memories are kept. Over capacity (default -200), the least-used unpinned memories are marked forgotten rather than deleted — so -the UI can still show them and you can un-forget one.

- -
- Money trap -

Background calls never spend the shared demo key. The summarization and - embedding providers are built directly from the user’s own settings, never from the demo config, - and their call sites are skipped when the turn is running on the demo key. Summarization is - unmetered background spend; the demo key is server-funded. Both facts together would be a bill.

-
- -

1.6Streaming

- -

The model produces tokens one at a time. Waiting for the whole reply before showing anything -makes a 20-second generation feel broken.

- -

Server-Sent Events is the mechanism: an HTTP response that stays open and -pushes data: {...} lines as they become available. It’s one-directional -(server → browser), which is exactly the shape of this problem — WebSockets would be a -bidirectional connection for a unidirectional need.

- -
model endpoint  --SSE-->  FastAPI  --SSE-->  browser  -->  React state  -->  screen
- -

FastAPI reads the provider’s stream and for each chunk yields -data: {"type":"chunk","text":"…"}. The frontend reads the response body with a -ReadableStream reader, buffers on \n\n boundaries, and dispatches each -parsed event. Event types: player, reasoning (thinking-model traces, -which stream into a separate collapsible panel with their own token budget), chunk, -stopped, error, done.

- -

Two production details that only show up when hosted:

- -
    -
  • X-Accel-Buffering: no — nginx-style reverse proxies buffer responses by default, - which turns a stream into one big delivery at the end. This header tells them to flush each event.
  • -
  • The security-headers and body-size middlewares are written as pure ASGI - rather than Starlette’s BaseHTTPMiddleware, because the latter buffers the response - body and would break streaming.
  • -
- -

The empty-reply case is diagnosed, not reported as “empty”. If a reasoning -model streams thinking but no story text, it spent its whole budget thinking — the error says so -and names the three settings that fix it.

- -

1.7The scripting sandbox

- -

Real AI Dungeon scripts are JavaScript files defining modifier(text) and calling it -as the last line, with globals like state, history, -storyCards. To be compatible, this app runs the same contract in an embedded -QuickJS interpreter.

- -

The safety properties are mostly structural:

- -
- - - - - - - - - -
PropertyHow
No filesystem, network or process accessQuickJS has none by default — nothing was removed, nothing was added
Memory cap16 MB per run
CPU cap2 seconds per run
No shared state between runsA fresh context per hook execution
A broken script can’t break a turnEvery failure returns as .error with text, state and cards unchanged; the pipeline logs it and continues
-
- -

Data crosses the boundary as JSON — Python serializes {state, text, history, storyCards, -info} in and the results out. There is no object bridge to exploit.

- -

One deliberate bug-compatibility: addStoryCard returns the new card’s -index, so the first card returns 0, which is falsy, so -if (!addStoryCard(…)) misfires. That’s upstream AI Dungeon’s behaviour. It’s -documented in the code and left alone, because matching real scripts is the entire point of the -feature.

- -

1.8Why there is no agent framework

- -

Graph-based agent frameworks (LangGraph and similar) earn their complexity with -branching, cyclic, multi-step control flow -— a graph of nodes where the path depends on what the model decides, with loops, retries, tool -calls, and persisted state between steps.

- -

This turn pipeline is a fixed linear sequence with exactly one model call. -There is no routing decision, no tool selection, no loop. Every turn takes the same path. Adding a -graph framework would mean carrying its state abstraction, its serialization model and its -debugging surface to express a straight line.

- -

There’s also a specific reason a framework’s context handling wouldn’t fit here: -the budgeting logic is the product. Buffer-window and summary-memory abstractions -are opinionated about how to fit history into a window. This app shows the user every context -component, its token cost, and the trigger word that pulled it in — which means the assembly has -to be explicit and inspectable.

- -

When it would be the right call: if the design went toward the -two-call version — narrate, then a separate structured-extraction step, with a retry branch when -extraction fails and a tool-calling path for dice — that is a graph, and hand-rolling it would get -ugly fast.

- -
- - -
-

Part 2

-

Data and correctness

-

The bugs in this section are the kind that don’t crash. They just quietly produce the wrong - answer, which is why each one has a test.

-
- -
- -

2.1The domain model

- -
User
- ├─ Scenario   (the template)      ── stat_schema, prompt, memory, author's note
- │    └─ StoryCard, Script
- └─ Adventure  (the playthrough)   ── world_state, script_state, head_branch_id/head_depth
-      ├─ Branch  (one line of it)  ── parent_branch_id, fork_depth, lineage, name
-      ├─ Action  (one node)        ── branch_id, depth, parent_id, live,
-      │                               text, context_snapshot, state_after
-      ├─ StoryCard  (its own copy)
-      ├─ Memory     (text, embedding, branch_id, depth, use_count)
-      └─ AdventureScript
- -

The one decision that shapes everything: template vs instance. A scenario -declares what stats exist; an adventure holds what they are right now. Creating -an adventure copies the scenario’s story cards, scripts and plot fields into it, so editing a -scenario later never mutates a game in progress. There’s an explicit opt-in “Update from scenario” -flow for when you do want that, which diffs the two and shows what would change.

- -

Same reasoning as instantiating a class: shared definition, independent state.

- -

2.2The story is a tree

- -

The largest structural change the project has had, and the one with the most reasoning behind -it.

- -

The problem

- -

The story used to be a list, and a mutable one. Retry rewrote the last entry in place; undo and -delete removed entries from the middle. Everything derived from the story — the memories, the -running summary, the two marks saying how far each had got — was indexed by position in that -list, and a position means something different after anything in front of it is deleted.

- -

That single fact produced a family of bugs that all looked different:

- -
    -
  • Deleting a middle action slid a never-summarized action down into the “already covered” - range, so a recent action silently never became a memory.
  • -
  • Discarding a memory left its actions behind the mark, describing nothing.
  • -
  • Retry rewrote an action’s text after the mark had passed it, so its memory described - narration that was no longer in the story.
  • -
  • The retried row was still attached to the adventure while its replacement was being written, - so the model was shown the attempt it was meant to replace and wrote a continuation of - it. That exclusion had to be threaded through four separate readers.
  • -
  • Attempts lived in a JSON array on the row with a mirrored copy of the live one in the - ordinary columns — a repeating group and a denormalisation in one.
  • -
- -
- The pattern -

Each was fixed where it was found. The shape only becomes visible when you line them up: - they are all the same bug, and it is that the story is a list nobody may reorder.

-
- -

The shape

- -

Make the story a tree and none of them are reachable. Every action is a node -with a branch_id and a depth. A branch is one line -through the tree: it holds the nodes played on it and borrows everything before its fork -point from its ancestors. Nothing is ever copied, and — apart from an explicit delete — nothing is -ever removed.

- -
branches(id, adventure_id, parent_branch_id, fork_depth, lineage, name)
-actions(id, adventure_id, branch_id, depth, parent_id, live, text, …, state_after)
-memories(…, branch_id, depth)
-adventures(…, head_branch_id, head_depth)
- -

depth is a position along a path, not a global turn number: -A4 and B4 are two alternatives, not two turns. Reading branch C, whose -tip is at depth 7 and which left B at 5, which left A at 3:

- -
SELECT * FROM actions
-WHERE (branch_id = 'C')
-   OR (branch_id = 'B' AND depth <= 5)
-   OR (branch_id = 'A' AND depth <= 3)
-ORDER BY depth DESC LIMIT 32
- -

→ A0 A1 A2 A3 B4 B5 C6 C7.

- -

Why branch_id + depth rather than parent pointers alone. -Parent pointers are the obvious way to store a tree and the wrong way to read one: reading a story -would be N round trips up a chain, which throws away the windowed history work (1.2) -that made a turn’s read cost flat. Depth replaces the old index as the ordering key, so -the reads keep the shape they already had.

- -

The lineage, and why fork count costs nothing

- -

The OR-clause above is not reconstructed per read. It is stored on the branch row as -lineage — [(C, ∞), (B, 5), (A, 3)] — computed once when the fork happens, -from the parent’s lineage plus one entry. One module knows how to turn it into a query, which is -deliberate: one forgotten clause shows the wrong story and reports nothing.

- -

Two properties of the shape do the real work:

- -
    -
  • The ranges are disjoint and descending. A branch’s own nodes always sit - deeper than its fork point, and each ancestor is capped at the fork depth of the branch beneath - it. So ordering the whole clause by depth DESC reads entry 0’s nodes, then entry 1’s, - then entry 2’s — which lets a tail read use the newest few entries and stop.
  • -
  • Clause count is bounded by the context window, not by fork count. A 200-fork - story whose newest branch is 40 turns long reads with one clause, because the window is - covered before the second entry is reached.
  • -
- -
-
1.007×
page load of a 40-turn story forked twenty times, against the same story flat — 31,652 B vs 31,433 B
-
~103 B
what one branch costs: an id, a parent, a fork depth and a cached ancestry. No copy, no migration, no vacuum.
-
- -

What a player actually does

- -

None of the above is what the screen shows. In the player’s words:

- -
-

Any turn can gain another take. On an AI turn that means regenerate; on your - own message it means type something else. Stepping between takes with ‹ 2/4 › is free - — the story below simply empties, because that take has no children yet. A branch is - created when you write below a take that is not the live one, never before.

-
- -

That rule collapses two operations into one and deletes a distinction from the UI. The first -version of this screen had a chip that switched at the tip and only previewed -above it, with a second button to take that line — one control whose meaning depended on where the -reader was standing. The rule above replaced it with a pager that only ever steps, a fork button on -every turn, and no tip-versus-past distinction at all. The distinction survives in the -implementation, where it decides whether a write needs a branch: at the tip the attempts are still -leaves nobody has built on, so taking one is a switch and no branch is created.

- -

Takes are grouped by parent, not by coordinate

- -

The load-bearing detail, and the one that isn’t obvious. The natural way to find “the other -takes of this turn” is by coordinate — same branch, same depth. It is wrong in both directions:

- -
B ── C   C1   C2              <- three takes, one parent (B)
-          │    └── D1' D2'    <- two takes, parent C2
-          └── D1 D2 D3        <- three takes, parent C1
- -

Standing on the C2 path at that depth must read 2/2, not 5. Coordinate -grouping gets that one right by accident, because writing under a non-live take forks and the two -sets land on different branches. It gets C wrong: once C has been forked onto a branch -of its own it is alone at its coordinate and reads 1/1, having lost C1 and C2 from a -pager that must still say 1/3.

- -

So a node carries parent_id, read for nothing but this. The alternative — making a -branch’s fork point a node rather than a depth, so a promoted take never moves — was -rejected: the whole point of lineage is that a read is an OR-clause per branch instead -of a walk up parent pointers, and re-pointing the fork at a node changes path resolution itself, -dragging in the cursors, memory depths and both bundle formats. parent_id is one -indexed lookup, never a walk, and nothing about how a path resolves changes.

- -

Cursors become anchors

- -

The two marks — how far the memory bank has got, how far the summary has got — used to be -counts. A count is a position in a list, and every rule about sliding them, rewinding them and -translating between positions and Action.index existed to patch up the fact that the -list moves.

- -

A cursor is now an anchor: (branch_id, depth), the node up to and -including which the work is done. Deleting an action doesn’t move it, because a depth is a -coordinate along a path rather than a slot in a list. “What is not covered yet” becomes a question -about the story instead of about a list index, and it answers correctly whatever has been deleted -in front of it. The branch half is what makes it survive forking: a depth alone is ambiguous once -two branches both have a node 41.

- -

position_of_index, note_action_removed, -settled_story_actions and the cursor-rewind machinery were deleted, -not left unused.

- -

Derived work attaches to the node that produced it

- -

Generalise the rule and a lot falls out: anything derived hangs off the node that produced -it. A memory covering depths 37–42 hangs off that branch’s node 42 and is invisible to any path -that doesn’t run through it. Shared ancestors are therefore shared automatically, so a fork -needs nothing recreated — the memories above the fork point are already on the ancestors -both lines read.

- -
- The subtle case -

The memory sitting at the forked coordinate. The first cut moved it onto the new - branch and re-anchored the marks naming it. Both are wrong for the same reason: that memory - describes whichever attempt was live at that coordinate, which is the one staying on the parent.

-

The right answer needs no code. The lineage caps the parent one depth short of the fork, so - the memory is simply out of range from the new branch — invisible to both the retrieval clause and - the anchor read. The new line summarizes that ground again, from the text it actually tells.

-
- -

Hand-written memories obey the same rule. One used to carry a NULL depth, described as “belongs -to the adventure rather than to a path” — which sounds harmless and is not: a NULL is a coordinate -no fork can cap, so a note typed on one line followed the reader onto branches whose events it never -described. They are anchored at the head instead: the story you were reading when you wrote -it.

- -

Deleting, and why the branch UI was a hard dependency

- -

Nothing is ever auto-pruned. That is the guarantee the whole design rests on, and it is also why -branch management couldn’t be a nice-to-have: without a way to delete a line, storage grows without -limit.

- -

The delete rule has two halves and the second is easy to miss. Refusing to delete the line being -read is obvious. The other half is refusing any line it was forked from — -parent_branch_id cascades, so deleting an ancestor takes the head with it and leaves -head_branch_id pointing at a row that is gone. One membership test against the head’s -own lineage covers both, because a lineage already names itself and every branch it borrows from. -The server is the authority; the client computes the same set only so a button can say so before it -is pressed.

- -

The migration, and what it deliberately did not do

- -

There is no feature flag. A linear story is a tree with one branch, so the -intermediate states weren’t half-migrated — they were the same product with a superset schema -underneath, which made “existing adventures are unaffected” a literal, testable pass condition at -every step. A flag would have bought two live code paths through the context builder, the memory -bank, undo and retry at once.

- -

The legacy columns (index, variants, variant_index, the -two *_before snapshots) were kept unread for a release rather than dropped with the -migration that stopped using them, so that redeploying the previous build is still a way out. -Dropping columns is the one step that isn’t.

- -
- Operational, and it generalises -

On Postgres a migration that rewrites every row of actions roughly doubles the - table, and only VACUUM FULL gives it back — 79 MB reclaimed in 5.5 s on one occasion. - But bloat scales with the heap, and context_snapshot is 94% of this - table and lives out of line, so a migration touching only small columns reuses the existing TOAST - pointer and costs a tenth of that.

-

Read the sizes from sum(octet_length(col)) per column, not from - n_live_tup — that one is a stale estimate in exactly the direction that makes bloat - look smaller.

-
- -

What this is honest about

- -
    -
  • The two marks are one pair on the adventure, not one per branch. Switching - lines makes the mark on the line being left unreadable from the new one, and that ground is - summarized again. It answers “nothing covered”, which is the safe direction — redo the work, never - skip it — but switching back and forth costs AI calls.
  • -
  • Story cards stay adventure-wide. A card invented on branch B shows on branch - A. Event-sourcing card changes onto nodes was considered and rejected.
  • -
  • Editing an already-summarized action still leaves its memory stale. The - machinery to fix it now exists — an edit could write a sibling take and switch to it, which is a - retry the player typed — but it doesn’t do that yet.
  • -
- -

2.3Undo and retry that actually rewind

- -

Most implementations of undo delete the last message. That’s wrong here, because a turn mutates -three things: the text, the scripting scoreboard, and the RPG stats.

- -

The mechanism: every node carries state_after and -world_state_after — deep copies of what the adventure looked like once that turn had -played. Rewinding to before a turn is a read of the node in front of it, so undo, retry and a branch -switch are the same restore. The cooldown clock comes along for free: it lives inside the world -state, so each line of the story carries its own without anything having to know there is one.

- -

Nothing a retry replaces is thrown away. The old attempt stays as another -take of that turn — a sibling node at the same coordinate, live false — and the -pager steps between them. Which is to say retry isn’t a special case: it is the tree, with the branch -not yet created (2.2).

- -

Four details that are easy to get wrong:

- -
    -
  • The turn being retried is excluded from its own context. Its takes are still - attached to the adventure, so without an explicit exclusion the model would be shown the attempt - it’s replacing as established story — and would write a continuation of it. The exclusion had - leaked into four readers, not one: history replay, story-card trigger matching, in-scene NPC - detection, and the memory-bank similarity query. Anything reading the story during generation - takes the exclusion.
  • -
  • A retry reuses the turn’s depth, not the next one. Cooldowns are measured - along the path, so allocating a new depth would advance the clock the cooldown rules run on and a - retry would quietly unlock stats that should still be waiting.
  • -
  • delete_turn used to mean “every take at this coordinate”. Once a - take can be forked onto a branch of its own the group spans branches, and undo reached across and - deleted a take belonging to a line nobody asked about. Anything that reads a take group and then - writes has to say whether it means the turn or the coordinate.
  • -
  • If the regeneration fails, the rollback is reversed. The generator is wrapped - in a try/finally: if it ends without saving — provider error, empty reply, a script - stop, or the browser hanging up — the previous take is put back in charge. Otherwise - the state on the server drifts from the text still on the user’s screen.
  • -
- -

2.4The turn lock

- -

One turn at a time per adventure. Double-clicking “Continue” must not run two generations.

- -

The subtlety: the check has to happen in the request phase, not when the SSE -generator first runs. A streaming response doesn’t start iterating its generator until the response -begins, so a check inside the generator lets two rapid requests both pass before either claims the -slot. And because sync FastAPI endpoints run in a threadpool, the test-and-set needs a real lock.

- -
def acquire_turn_lock(adventure_id):          # in the request handler
-    with _active_turns_guard:
-        if adventure_id in _active_turns:
-            raise HTTPException(409, "A turn is already generating…")
-        _active_turns.add(adventure_id)
-
-async def with_turn_lock(adventure_id, gen):  # wraps the SSE generator
-    try:
-        async for event in gen: yield event
-    finally:
-        _active_turns.discard(adventure_id)
- -

In-memory, so it’s a single-process guarantee. That’s honest for the deployment this targets — -one Render web service. Two processes would need the lock in the database.

- -

2.5The 189× egress fix

- -
-
38.5 MB → 0.20 MB
database egress for one adventure load
-
~74 KB
per-turn prompt snapshot — 94% of the database
-
- -

The bug: Action.context_snapshot holds the entire assembled prompt -for a turn. Every adventure load pulled that column for every action, to read two small fields out -of it — the world-state delta for the “what changed” chip, and the applied report. SQLAlchemy loads -all columns by default.

- -

The fix, in three parts:

- -
    -
  1. Move the two small things that are needed for every action into their own column.
  2. -
  3. Mark the heavy columns deferred — snapshot, variants, reasoning — so they’re only - fetched when explicitly asked for.
  4. -
  5. Backfill the new column with dialect-specific server-side SQL, so the old data is extracted - inside the database and never crosses the wire.
  6. -
- -

The part that makes it stick: tests/test_egress.py hooks into -SQLAlchemy’s before_cursor_execute event, captures every statement the ORM sends, and -fails if a bulk load ever names those columns again. The regression is caught by asserting on the -SQL, not on a timing.

- -

One more detail from that test’s design: the count query is written as a real -SELECT count(…) rather than query.count(), because SQLAlchemy’s -.count() wraps the entity select in a subquery whose SQL names every column — -including the deferred ones. No bytes come back either way, but the database still reads them, and -a guard that greps SQL can’t tell the two apart.

- -

There’s a companion denormalization for the same reason: the pager has to know how many takes a -turn has without fetching any of them, so variant_index and variant_count -are cached on the row and refreshed by exactly one function, precisely so they can’t drift and the -pager can’t lie. variant_count is 0 rather than 1 for a turn nobody retried, because -the question it answers is “is there anything to page through?”

- -

2.6Migrations, hand-rolled

- -

No Alembic. An append-only list of (version, SQL) pairs, with the current version -stored in SQLite’s PRAGMA user_version or a one-row table on Postgres. 64 versions so -far.

- -
    -
  • A fresh database is created by create_all() — always current — - and stamped at the latest version. It never replays history.
  • -
  • An existing database runs every migration above its stored version, in order.
  • -
- -

Why this and not Alembic: for a single-file SQLite app someone may have been running for months, -the entire requirement is “add a column, don’t lose their data”. Alembic’s autogenerate, branching -and down-migrations are machinery for a team with a staging environment. This is 250 lines and you -can read all of it.

- -

The constraint it creates is written at the top of the file: change models.py so -fresh databases are current, and append a pair here so existing ones upgrade. Migrations -2–23 predate Postgres support and use SQLite-only syntax — harmless, because every Postgres -database starts fresh and never replays them, but anything added since must run on both dialects.

- -

One migration worth reading (repairing duplicate action indexes) uses UPDATE … FROM -with a window function rather than a correlated subquery, because SQLite may evaluate a correlated -subquery against partially-updated rows and produce duplicates again while “repairing” them.

- -
- - -
-

Part 3

-

Production concerns

-

What changes when the app stops being yours and starts being a URL strangers can open.

-
- -
- -

3.1Two modes, one codebase

- -

AIDND_MULTI_USER switches the whole app between two personalities:

- -
- - - - - - - - - - -
Local (default)Hosted
UsersOne auto-created local userGuest on first visit, optional account
AuthNone — no cookies, no login UISigned session cookie
Rate limitsOffOn
Row capsOffOn
API docsOnOff
ProviderWhatever Settings points atUser’s key, or the shared demo key
-
- -

The reasoning: someone running this on their own laptop should never be throttled by their own -app, never see a login screen, and should get the interactive API docs. A hosted deployment needs -all four to be the opposite. Rather than two builds, the differences are gated at each site.

- -

Guests upgrade in place. A visitor gets a guest User row on first -load. Registering sets email and password_hash on that same row -— so every adventure they played as a guest survives with no re-parenting and no migration step. -Three kinds of row share the users table: local, guest, and registered.

- -

Guests expire; accounts don't. One row per curious visitor adds up, so -cleanup.py deletes guests idle for AIDND_GUEST_RETENTION_DAYS -(default 5) — measured as COALESCE(last_seen_at, created_at), since -_touch only writes last_seen_at hourly and a freshly minted guest -has NULL until its second request. The filter requires both is_guest -and email IS NULL, so upgrading in place is also how you opt out of -expiry. It sweeps once at startup — the reliable trigger on a host that sleeps — and then -every few hours.

- -

It's a single Core DELETE, not db.delete(user): the ORM path -would SELECT every adventure, action and memory into Python purely to delete them, and the -foreign keys are ON DELETE CASCADE from users all the way down, so -the database does the whole graph in one statement. Nothing a guest owns is visible to -anyone else either — is_public is output-only, so shared content is exactly the -seeded scenarios, which have user_id NULL and never match the filter.

- -

3.2The shared demo key

- -

The demo lets people play with no signup and no API key, on a key the server pays for. That is a -spending surface, so it’s the most defended code in the project.

- -

One function makes the BYOK-vs-demo decision, and on the demo branch it pins two -things:

- -
    -
  • The model — to a whitelist. A caller-supplied override or a hand-edited - settings row can’t aim a server-funded key at an expensive model. Anything unrecognised falls - back to the first whitelisted model.
  • -
  • The endpoint — to the configured demo URL. Otherwise the key could be - redirected to a URL the user controls and harvested.
  • -
- -

Plus a daily per-user turn cap (default 20), checked before the player’s input is stored -so a capped player doesn’t get their message saved with no reply, and counted only after a -successful turn.

- -
- A real bug, recorded in a comment -

There’s a defensive check that raises if a demo config somehow carries a non-whitelisted - model. It tests using_demo, not - api_key == DEMO_API_KEY. Keying on the key value looks stricter but is wrong — the - demo key is an ordinary OpenRouter key, so a user can legitimately paste that same key into their - own settings as BYOK, and then every resolution raised, 500ing even GET /auth/me and - taking the whole SPA down. using_demo is what actually means “the server is paying”.

-
- -

3.3Secrets

- -

Everything derives from one server-side secret.

- -
- - - - - - - -
ThingMechanism
Passwordshashlib.scrypt, N=2¹⁴, r=8, p=1, per-password salt, constant-time compare. Stdlib, so no extra dependency.
Sessionsv1.<user_id>.<HMAC-SHA256>, no expiry — long-lived guest sessions are the point. A cookie can outlive a swept guest row; that resolves to a 401, which the frontend already turns into a fresh session.
Stored LLM API keysFernet encryption at rest, key derived from the secret, enc: prefix so legacy plaintext rows are recognisable and migratable.
-
- -

The secret auto-generates into a file next to the database for local installs (zero config), but -multi-user mode refuses to start without the env var — with an error that explains -why and gives the command to generate one. Hosted filesystems are ephemeral; a regenerated secret -on every deploy would silently log out every user and orphan their stored API keys.

- -

A rotated secret makes stored keys undecryptable. Decryption treats that as “unset” rather than -raising, so the user just re-enters their key instead of hitting a 500.

- -

3.4Abuse guards

- -
- - - - - - - - - - - - -
GuardLimit
Turn generation10 / min
Auth attempts (per IP)10 / 5 min
Guest creation (per IP)30 / 5 min
Script test runs30 / min
Connection test10 / min
Adventures / scenarios / scripts per user100 / 200 / 200
Actions per adventure5,000
Request body2 MB (20 MB on import)
-
- -

Rate limits are keyed per user when one is known — accounts survive IP changes — and per IP -otherwise, in fixed windows held in memory, with a pruning pass so the per-IP dict can’t grow -without bound. Import endpoints check bundle list lengths against the same caps live creation -enforces, otherwise the cap is trivially bypassed by uploading a file.

- -

Security headers on every response: nosniff, X-Frame-Options: DENY, -Referrer-Policy: same-origin, and a CSP allowing exactly what the SPA uses.

- -

3.5Deployment

- -

One Docker web service on Render, serving the SPA and the API same-origin, with Postgres on Neon.

- -

The Postgres decision was forced: Render’s free tier has no persistent disk, so a SQLite file -wouldn’t survive a deploy. The database lives off-box.

- -

Two things worth knowing about the free tier:

- -
    -
  • The service sleeps after ~15 minutes idle, and the first request then takes - 30–60 seconds.
  • -
  • /api/health deliberately doesn’t touch the database, so a - keep-warm pinger wakes the web service without waking the database. Waking a database around the - clock costs far more than the cold start is worth.
  • -
- -

CI runs the backend tests, the frontend lint and build, and a Docker image build on every push.

- -
- - -
-

Part 4

-

The web plumbing, briefly

-

The parts that are just how the web works, not decisions.

-
- -
- -

Frontend and backend are two programs. In development they’re two servers — -Vite on 5173 serving React, FastAPI on 8000 serving the API — and Vite proxies /api to -FastAPI so the browser thinks it’s all one origin, which avoids CORS entirely. In production -there’s one server: FastAPI serves the built React files as static assets from the same port.

- -

SPA routing. React Router handles URLs like /play/3 in the browser -without a round trip. But if you reload that URL, the browser asks the server for -/play/3, which isn’t a file. So the static-file handler catches the 404 and returns -index.html, letting React take over and read the URL itself. API routes are matched -before the static mount, so they’re unaffected.

- -

Sessions. A cookie is a small value the browser stores and automatically -attaches to every request to that site. Here it holds v1.<user_id>.<signature>. -The server doesn’t store sessions anywhere — it re-verifies the signature on each request, which is -why there’s no session table.

- -

The 401 retry. If the cookie is missing or stale, any API call returns 401. The -frontend catches that once, calls /api/auth/me — which mints a fresh guest session — -and retries the original request. So a returning visitor with an expired cookie never sees an -error.

- -

React, in one paragraph. A component is a function that returns a description -of some UI. useState holds a value; changing it re-renders the component. The -streaming turn is the clearest example: each SSE chunk appends to a state string, React re-renders, -and the text appears to type itself.

- -
- - -
-

Part 5

-

Results and limitations

-

What was measured, and what this design knowingly does not do.

-
- -
- -

5.1Measured results

- -
- - - - - - - - - - - - - -
Database egress per adventure load38.5 MB → 0.20 MB (~189×)
Prompt snapshot size~74 KB/turn, 94% of the DB
Turn read cost at turn 200839 KB → 129 KB, flat after ~turn 50
Cost of a branch~103 B; 20 forks load at 1.007× the same story flat
Length-hint phrasing174 → 246 words as a budget; 170 as a ceiling (n=5)
Backend tests440, LLM mocked, real QuickJS engine
Schema versions64
Sandbox limits16 MB, 2 s CPU, fresh context per run
Context defaultsauthor’s note at depth 3; cards ≤ 40% of elastic budget
Memory cadencememory / 6 turns, summary / 15 turns, top-5 retrieval
-
- -

Two of the tests encode a performance property rather than a behaviour: -test_egress.py asserts on the SQL the ORM emits, and -test_history_window.py asserts that the read cost stops growing with story -length.

- -

5.2Known limitations

- -

Deliberate trades for a single-user-first app that also happens to be hosted, listed so -nobody has to discover them the hard way.

- -
    -
  • Single process. The turn lock, the rate limiter and the summarization task - all assume one worker. A second worker would need the lock in the database — a row-level - advisory lock — and the rate limiter in Redis.
  • -
  • No vector index. Retrieval does cosine similarity in Python over the whole - bank. Fine at the 200-memory cap; at 10,000 it would want pgvector.
  • -
  • Prompt snapshots are heavy even after the egress fix — they’re deferred, not - smaller. Compressing them or expiring old ones is the real fix.
  • -
  • In-memory rate-limit windows reset on restart, so a restart grants a brief - extra allowance.
  • -
  • Background summarization is a fire-and-forget asyncio task, so it does not - survive a restart. At real load it belongs in a queue.
  • -
  • The demo key depends on a free-tier provider’s daily cap, which the app can - only detect after the fact by string-matching the 429 body.
  • -
  • The two memory marks are one pair on the adventure, not one per branch. - Switching lines makes the mark on the line being left unreadable from the new one, so that ground - is summarized again. It fails in the safe direction — redo, never skip — but switching back and - forth costs AI calls.
  • -
  • Story cards are adventure-wide, so a card invented on one branch shows on all - of them.
  • -
  • Editing an already-summarized turn leaves its memory stale. Replacing a turn - withdraws what was derived from it; editing one in place does not.
  • -
- -

5.3Cleanup backlog

- -

docs/self-review.md carries an open list of non-bugs — reuse, simplification and -efficiency items — kept deliberately separate from the correctness list, which is empty. The -largest ones:

- -
    -
  • Section.tokens is uncached, so the context gets tokenized two or three times a - turn.
  • -
  • onModelContext flattens system and story into one string before handing it to - user scripts; if a script modifies it, the structure is gone and everything ships as user - content. Passing structure through the hook would be better but would break AI Dungeon - compatibility, which is the point of the feature.
  • -
  • The import endpoints hand-coerce raw dicts instead of using Pydantic bundle schemas.
  • -
  • The legacy pre-tree columns (index, variants, - variant_index, and the two *_before snapshots) are still on - actions, unread, kept for one release so redeploying the previous build remains a way - out. Dropping them is a migration that rewrites every row, so it owes a - VACUUM FULL actions; after it.
  • -
- -
- -
-
-

AI D&D — design notes · - Source on GitHub · - Project page

-

Full source for every claim here is in the repo; the file paths are named inline.

-
-
- -
- - - - - diff --git a/docs/images/branch-map.jpg b/docs/images/branch-map.jpg deleted file mode 100644 index 369f801a425ea50a6e91ef7d71481add716b386b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36669 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 LX>SVo)b_ssI^lFg diff --git a/docs/images/branches-panel.jpg b/docs/images/branches-panel.jpg deleted file mode 100644 index 119de2c16eed53039014b8c900a2134274bde9e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105348 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

fe)|S-ir9am+v@Mt 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 PZm08>^&qyJpZot0iDp%i diff --git a/docs/images/insights.jpg b/docs/images/insights.jpg deleted file mode 100644 index a18b69e51409203ec72aceec4ec72f67d2a7c588..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160403 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 z 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

<&p& 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 z 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 diff --git a/docs/images/play-world-state.jpg b/docs/images/play-world-state.jpg deleted file mode 100644 index 61851832465c718ecc1544f84db6265ed28721f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96735 zcmeFZbyQr<)+gM!LkLb9cMY!HB)Ge4LK+AjT%!;q1RB@iZjCk`2;N9=ceh4@6NpWd z-22Y+%zNKE>s_yyQ_{a?f2{)Us41x`0npF@05sGe;MW2` z9)OFDjf0Jai-UuMhlh(#NKQmZKtM=ymyCp*j+TL*jur@HWaZ~zWaec70y)LGcm)JS zL_`=kB*79wAbw#Hp<5+rczAe(1ccN?MASk|KqjI8_VueBK#GgrgUyJ6#tc9wMZ+LP z`_&BqqClc!pxxsA=Y@uej)8@ZgNx$o-vOXuV4z`Q;bP(8U|^!5;oVAOVv*j(CSwvL zm(|s?#$ncXe-j&*Lm}i*y+%pJA}lHwKQ=C6_{1|IuY=W~hD|>AI7m$4gk4+_;#J#; zQc8=m_WSy$L6oa*m13iKqNFGT=x8WY=$M$;7`LdyVF>Dyk<04EVp=n^Cs$w|#1{x6wSC)3 zsEbN?$W5&Uf^U=J9`fRc3JRDC|2|P5B}F5>LrO}npdfpD$CeNF^FGr<&FzEf93{~$kDX9qjI1Ww1bvz>ntRWLj%;-3p%!)RAu25J<&fj z$BaS%cYr@iMH^IcBxP{*={StiFE+S|Wjp6S(pB`9ms=gLO2o;6MxUT@V7%j&w@`Yu zrA|-#eUJKBkM$WwaBMnL7eGZ4ODV<%;h^~7Ti$fIWDkP1cs*i26Y{R0q64kzK&C*E zb-lc~ps~x$kDfl?TVB-%coOq)?Q3=g8VBI6fxD6TAqCck5rJEvKNkgn`uk1{8V>lC z2t{KFBlK~Ije!7Z&(07L=mABksr>mJ3$ND#M=GMBjWGa8P?DT@a?&x@$J2+M{o`Rx zsrG1%!{~znOJ^%_7}3n59>Lmo{7>jY2)U<;eXH7qI25n%@9k+5Qk*RPEv?Mg8>m&ULF?$M=3#;)zl zK8lWja?hyJ+>F2cGMHu7H(1Eta(~?0x^eY|qgg3Hwf9A-)i|Ole+Q?;*#<_6y>-aA zxwmglBrkB_NW*%%ljT^?eWJy^qH0$d^JsggtZO>kph4ZUICg|Za${d_)83N+;&dvo z1@-78Ve00qRhg;FmfQ?_S#@X@HsxDl@MM{W+pqW;bG^bbVr#BfDn^8E`D5f$Y7=)O zjg#c3BlK>*0BO<-B0e{#OgwG$U-PdBTKOvhinw78kB7O+nc9j1-`5aR4) zKe;N3mRVE14vPr?zI=aeZ4Ej_R2+$o4)ltA-s)%(MOxK&-0r6B*uBZaT#$;H90@!I ze)T>v`@mw(>N=QyZ!(}&ZMG+pp92b@Va<;eOs7H*Xh)hi%X?8GUwUmAn{Moz6~?GC zxv@riXHOnU@Bp#mn|O5ziM!hm6dtx8Xb^WR{C|y*eUC^nN^JTC(1CHQmFF@SC)!1% z7*5M;?WN+zcC_>h7c?DqR#3@JI2M1L_EOWQ{0eW%RSrcWbWThMo-o_lB{6Tooqx2i z$b>Kc0!%p~zPc}u*hy^Zl@+=0SyYMHX|TB4$EJ=Sct=(ir8x%MiV2$HY%wTyNSrm8 z4u5&%N;S-1U-};WRH0PxS+IOMi=R*W1p~#-?nOgd>My{^m!HtowQ^2ti=w(IgN4$v z+-L#4q%S0Pd9O>oD@zWhr0q?HGK+24X-l6VgKX_u_Z#;n(vMwn*eX65PxNklJ57PW z+sv@Fgrg~Dn-7LGbC=XD!J0hlrGRc0ezq=}5z0tUL;j>|K4Yl}nwKB%^NrXeWyV7G z>qmY8#=eRF)cFPID9O_E>9U&m1$ZL3{%mdaxFzAa4mJQ34D zs|3Napb^Hl_r5jbhg|n$ZJx*B3Gi^G918gWJvX2gZ3P=XK|yJA&r5R)yrM&?u)|bV zEwr%x(WWbvVV}rAzW{wB_*)(=HXQXe8B@v9UUE6jxs!XKwXW|#NvP8YuNq@BLc3C? z<^;zG`$b?sjy+Sr2xrzDFT*cD;Ef;ybGua%N`2)O(v#IZ&V2;ys%7L#5s;!4W5*$)ckh5C`US`B=%e3D07? z*WPAwJ2FPX11MCkkB}QmYdM#Cw>7+DbjkSHf0>v^3^ab<$i?SN0i7-RTNTEs@hj+5 zrBeBZ#KcrT*&h%*SSFRqO1uHIs zw@{zL%-YOPMwAP%wh>HKxlDo;wXAdO!=A^_Ffxu}f0KhaqzJ z6dRjrm8TJS^ky^IaRjOxh@TYt6og3w<_RLaA)m_^+rS%(Gm$oA@i8w{tIl>?Z96lQ z>c71Z(if4+#G@adk$&Gn@4hh-&2L4Z*fyp3VD#Rs@c0VnhhevAw6&S8*$*Tl@5e{S z7MyG=OGHLHh*&Vp$!i~|r&}l$ks_8t-X_$GEpwjnzMxgDmFRWP1lNFWViVn_&q zrLnq(DeJfTL0z3~Bw#gE^rPpxAA8p@Uv!YkI#jv)62a~z0e^kq*|Bd0HGk2(pzN7s zrWJ<_80t3L^mJRK_uEdGZVs3|5LB_cCv4~S8k8Oy=Z3Z_>CN8tnfXW)i^wI@ao7Il zUdfIVfytEwxMIXFOTyZuc~s{IgUB?}(Vr=@flN{7a06}aMoAie2}lCXGxITq!^|_k z9vO@)qZJ3TI=y{p^9d&|n1+knVZ%9oLKFDxrXmm*XHr|VEX>|2t-_yQB$>;;!tlPU z=!mk>FrVuyuM;3iap{GQIcyuBV_`+-n(HkzBTm|?h%kyJ38zG*-va0tl0K@g^@B`l zcjdKLYd(F%0g1oTXT>l7bgg85CFB+4SI!0^bl^C01lC2M^%_|xC2F=e0N zn<6t!wF%=T2$OopH=>_RZ%ab?{Ki+^WLIS^TP&RzYueI$r*#pmHy=#37K0w%RICf( z$Y5Dg4k!8+r|sxds59p~kM*rXoM93~{OyyVfN3A&Bt*D|UkI`|x4r`|mdYh1ycye5 zgCJPm7Mxq%jt@SNG+ZrtJ@>3)SK!UZ=%LB)?(TF)d5DGC55$-;>I5cxES~pRyak%_ z9qIDmyn^dg#YOdc1_twxOz}2JP6n1Yv5URAf{3tUH#4RNoh1jC2}La~!nzFFhyB1+ zCE+Y-2{5YXHeqy^8|NhvxF}*RotOuJ5^QtD@#y_O(YtY^^`DcInW+~$tQH44bZvr} zhBBZ)3w3Suu~l#hswDBY+skvr*Psr{*vtprWDM&Bn6fSI?E8WFM;#NDI5Y%8p9_JoioqOhs5e5ca(Z!lyED50e(_K*oxRC zvUr0eIs%v4Me|s{QF@GPW9D+@KKG#42vm_-Z?5-k%k1$kEnus2UV?)^nMQI9GhTSR zn{=4FmxYEKglTFG)bD0$B;&XZ%D>y1332v#bT7{z(wmGCG~>nui9W$XR#fR4O+Knt zVJR-~Io4ox=a;II1y&=9IfI6j*;rQ7k}~l_$7z&*$Y+UV3*li50WLlDpJbX30f(QX zx2LBkE;kR|t9Kk8)4XS4&Q4EIBX9)v+ZZ|OTrDHB?<0+5u-1RGOn_iSCc$ysKHvXQ6ig>E~$Zvr8L{zwE24{Q*H8b z!F&PLbdlK4ej%Ur6Vt0Ba|@n0y6pY}@Kt88)hzvqe?pV@m*l-K7g1#;MTOAEqvN~sq1lp8Sz*bR`6Vq2 zC7gDs$K0~vk=E@S)EmjbQwxnkBS=xCF9gfZu7R*J7oD<PgQ z`pZY;F)2Ce*i^}{Ar9jk(~Hp}H_2e~V#8_6vfQTT_5Qvxch>%VcaAJV$YxbhS2EvZi3VUEsFp*7}*=$JXyb}Jk((QPA}czfYdkl=jUg%Zz|H`DOv0ZM?i#- z*`~fk^eMj1(g@!n%ruK*`3`$e3lafYY95DH3nFLl7c6&smk7suL+e`KH1>i7ikWrl zOx;T=m)qE3`!cvt2f(zf$n_Uh4&ji3jZKA01=|IcZLNA=So;A>vWMJ%IRXm2I zv)BiwNxiR*1DB=0)8QA}Hyem>SP)|^cx=Nzz9K>_pG^P4$yW5JEs1YYUtGmMwmHw~ zJalp|3=FdsRU0Q49tC>?E|uAKyQ4ofe2d3-+p3jG?)LdeVO4uqyd zWF#y$)!x(>mO2iEO&X}!B<}FLo5c#ee)qOB_mFPBK8T|W6Sf3G3ijW#TD)9qKhZz$ z>Dt3F_DZG=BjR@l1W5}e)V;O=ER`-_8rfS)oZz*_x!%iS1(d(h}6C9svVZ(cP5IK!b}mV;~;$@ zYq6xq64FWX)iG{mkZyL6oX)u~QFf@4i=i|(Ho{*Au+xI|Xt%Z!^G_eleE9wp$&o=RXpD8G>1CU;kD-qW3De2LM#P1N81jS8`OPxKd_ z-T$QGEC|P}+fX2Gi`^2AUZ=qqKV_9wl0Ti|&n~{#R{8Mh6mOjE%l9A)qCWWfyOv)7 z#tddx)NBb```_3m|AJ~g#g|f8LgBeL=CF77>Zh!fLhCO;jQX1<%FI~tSDR${>MPfn zjrU^zaFta)w4UNY0{Fnc4RHLn?IzAVG0u*%@AWPaWjh(e1I4yN+2`CN^KFymeWsX~ zKeT*}`A@3ow@wuGQmS} zZ}t5rx-5VXC5Ym1sG_`ai|g-XNKyVE3jUrDs$#sis{ad$@3wwIS8cLV8m+g!`0rB+ z|15X+R?97A)O(N)0g>MN?H^C{rf!{j9~BSz>Zr(Z`Qy}oFn=5U57Gah`lr+WU-SgS z1^#%Au*ylnaNh;5k*)D@9s%2Etts+ZGUsl|Ys{cMR$uJyj*HBVNq{BkP^YyiEHo2; zF1Mgq^p0Rpc}lHP^+IGiAGQ8RJ_C3ouRwj%<7@<8NF29dr?pM^|2DCkP6ndI)h=e9 zz1S656KHVN_AhAyZx;|rXz(q>FFU$gPMW&(2H`n+V~69 z&4^Wr3}084T7yf0dQG64d{i7_uh>PKg$RyY=?MWp!10htd^esDx|U%0<3`2~nK8|>QC zFYrO~Vw*>L)tlNh<1yWZ*U%Afe0JyuQOT697%Cb$aA$jo0$xL2p$jj}*3l?jswINh z6DKOGE%{=F%#e6{dH!8=JYHb|wL_-wZ7R>?`g7riZQ_x2hmkRB^Z3uAEEDX*dUCLM@UaRC1C^C3a#@ls1Ac1wheuN-vO$hMju#i4s;nw>FPL zXFT#6CS8alB05qCiuu(nv~aqI-|fz1cYQLd26tiAvozG+`X3t*omyYYDm^RV$!Ml6S=VnUvr>FJ*ObA9oMifemB+!kg^bPQXAr3xHh1*oEIaxBb9~(NF++$>f z=nTY@mwx4Wijsd(_JRlClr@u1g7pc%8{d-NYE28Js;p6iy|`d(>xleTRmN3L6a zQnxnO1kxh@Dwms#(qq&h_*ueJMTRFYro{@5-S5powbu6w;w96-G zkJkDK(fYVZpWGJF&l_CiJ|e?M{%39fVB!580;d!gwb1q*X(NJyGdgSC#56WfWkx?P zLA4H;CzKWs{KokfY%PYowX0;yJ9B~ZCw~L|8|S~){5QGZaCA|1XL!n$C(Ka9 zx^kZbrw5|ezLQLPp`;8`+fFQ(<)R^!&_@YUcnehg9qNBp_)o9?9mNi1Qu-a)18$1> zP#ncFSnEstw9h@<|3x%(rZZauxD;f^jQNb!T10zkZ)kiyyFEw*lucO5>3cM(6rRCK zC=zQ8BK#b>EBKe>a3P+PXsmAz%_#f1wj(&V;%I1qO@ggMj1Pdv0n_k9f@QPCoN}*d zLR632cs(5%XMp<0QVk}uKt%rr+}01th?3`~TQI7k}Sr)A=EI`L2D<5u!Udqyu2pXFb}180&M zCN(}oEPer|e*v1j(q*hDs9^KHbQ$G^_2w9lAzZW9Y)(o|>Hcy4v1|PnBILD(&UK^u zJ>-ew*amXb07T5HsKci4%w)rUkCI{crKV(2sN$ESbNI38movkOiDqG=C%FQhOS%#g zJ8XGF)ChFH3b27L35S7j_$<4(XUd*PlwwZ)2Ix5DVuKSZ<8(t70_wbv+T-wavv*YM zNz`~}-_pIz_VrHHVIlH`+=#21tFS3~+~RDAZK#jZv^3OX;3_hlK_EVXf+o#pHo zphnO?+&U@OsE6=boN~HoqyuXffO0+!zgb{HUz1n1J2biKwaT_orSsOfGyd*RX2x?ocuzy(F> zem6u?!zay>-;th)s$?!qYpFP=cB+Rg2kI-NlK-xve z*&g7;hd>*|A2F&)seG^L*N~!!6w8*v-_O_8R^jS-__zov033@`*4ctC*KJMT_o^uFiO$nxch@_}N14Pd+RE1?U{SGQX5V zXTFr?P=<&}hrGCfN^)()Rx1m5xP9=GS8=n1_NrKxC?3jFBxtKTij{-hzRu=gyqgJLd4-nTbzW_h1pZBa%2~c4N zNoLANTzzwYRcl#qHl!@;0S$;?4;|6yNckVuC| z0!9OkqRLA18xJkLE$P(R3X7A?S&2L!;=u2a^s7d;Iz=gnLjSVM#DQdC&=8tM2k zpAzl^@f&<|l3Mz)A|xJRSkO9wo{2 zQK|4vv`CpTPhA{p95V&6Jxb#qcq%&n%sNYD^#~9?LeB%0*rK*ZIGp^1Y4i*ttV+0D zY9vBcxXMoK716(bs0*5cwZ*IL%NU=wYh9h8|V z)URGrbbseto)QT>MBCY?bP8T;&PVP^8)(afHl;7P=!X_Finp~kQtfvmu|=!t-{8E> zJ_E{1lg9M&T%?}hJ2KW+V)#}KP-EQ%)3Zyp3#!Jl!Og@ocwlFc^64di;h2ka zmU?)3?ZgKc5$A}6bE7!gQR#y|$pNHG-DwZ}@}7%6zaYUB$huaEDFJ8G9k7Q$Rfwlh zdL!Ld@aO#_nzVGMuKHhq9J^4#!95~pcJXxQai9-b*cFOpc{}?+cV8Vr`_$bia(o$A$Zg0oUlb&|_ z=PoreUgDp1k4;riE4>wip{J!-+;pcH~Qj z#n=rU64vNTWnsrPmER{sFs@3RN~}%IW;olO49Ku{J}IQ9F!_)_Y4|e*d~tUVq)cT) z7}!BjO&2<&Eo@Uc&yAxO9LI$jr*<++YX;!>u3+s2Gj?Lrn%8^|i<9EE$1XK{TK$FJ z)9fC#l-WZdnhcNUfR~5w8f@@IJO7wH2^tRdDkXm) zJ%dM7+5)$c|HHHsj|@0mD`d2H6d@rbLraSxdq9w|HU&*zJ6ZV!AhMEH(h}?Ge0&=0 z60%|@mD_dDXN4rNY`=b7*&^6U@IpFFC3PGd9yRfN`MKZfH>&8IO1SQ|I+d*Z``s~v z>+Y5&NnpCeMEc1==Z;Iy3%Us>r?Opcb6rhIr?k}>accYDZ9c*4G4gap3jZ?YJ#F*I23_4Y}sGj`m&n(nyBVea6^J+R~X+p~E z;aEd;)(c^Eb`~^fhd)?;YQB0tnp5;jl?JP{zCLYbER}4{hg$u0S6c~)1ZLiFxYV4M zIi<=6)-PFaP?a~%7m*%j3=@t9LMjQePUMcqo}PLk%X$dCles1N>SS-Y?RTbNLpZz+ zd^cS*jL?UbOdk0zSiQN4@jcQ{{O#j7?#={2aTZ&9DCbOhJxn=^St^RNvy0<$9SR`k zEQ^>Fy_H@7oj=%_{eW4}Jj#YRU|M z*uBWQHs1M0W-l&>v!BFPaWs@$B@_j2|_pbKMKx*>xJ z!O2lYrZn-7jUvdK3YuQIjYYBHxVDHS@%rjB)YQtz^PIQMG7oJYJ6@u#C&zY1As$yG zA!(47y7D?V{-%WI=Vw&vWSMTGYmx4(9$*K{Dq|^psir8yHizPkyM}u5G9@_<8_9%_ zFe^MuDOh8USBtDDh(dbcWF@|IF6EX(?NvnI>%v zNYn1#C+W)%yn71Vxt3@|Ji@{_N_t}|F-7-4#ixOYZLT*9or+j9vplH%8Cf%D_8xX> zbt}T}3hGDQiv#oJ`Qqtxvexk%T(cd_Z^m0~oe*tV)Jm$GNe&jxY%UpQt1%Fd`W&Z++ z9Ok~C|5AOnn0I=wsCKbJWXfRO^a$<|$LJ0W1|UdI?tiGvHmQ%dEkDy+EQ#8V$fZOB zV@Y4g>wJf>HqCYHLBLB?w(pKfs{k*L*vqNpDL<2*$TGh;n3RyzE5U^R|Ji_ zdMDN+P2@MDeMPn^17}tETxn%FaHx3-*vFj@Z+(%xk$Idi6lbBt`BB55bb2il z5Dwc}V&kV61LOlZ*y4Ap5$pJ1vUak@1aXt{bD-Z52R?U?2&>EPXolwC!q_a$l*QR# za=Z!Z=H79Woe8}5=Ap6-hL(OKqDwuy;MYK*+W1jXD`#dL49 zz=fOpS(_D?zsZ(jRfDi-pd*sgo;fE)miI!Rve(M*H7;f*CcUK-OtVUplBW@t8JF1> zSI90_Y8LsB#ckL_a;GfRuS}e;w%Ubc94=1kR+Ljx=3)pMQ$m3A1w`tGh9y={`nMY| z0_AP&hL#R42hvrDJ6U>0ZNa10ze-IzT@%-f()0(*fL9E+6vE5E{R7g&zodLv7|q=ycIHCA%l3!-z=LgqxshrA4G{hDtR_< zGVb}Rn78zk-U_@re;c*nsK04q2~N)m+JgJsa3{qRp4CMIR%5gsxA~)j)+kDU0c0OZ zTkV!?J4X`&8egLVbn=out)c&X+-O=;-SSypLw((OHEsXmL@_-EGJVT)t}0m&+>>Z1 zBBHCKHd~lk_+<7%Ivns?`;P3^bmfXz=Za^J^PMh(8G@FwnoBs*9CGyw?DWKvvwnMq zeQfE7u)IY_too!`s4p+J!pVfz?CYXRe@92Z$;U)Qv<){=QYp6X_>{5O`hW>Cr_6nO z)NT4rena#$2TN^`>gG*-8W+;iz22Zpm4`-e-eGY^AQ`QQR+(b4aG}M$!jq2@AWuc}0G-BU)Y|Q!R$RUM!v8#w(X)Z@*3gib%LmIr7Rx4W~{cDK_ zXnM>avJjC-k)Xn-+FU>G zV6Y*GZi+yXiILx)x+Y!Ji3ZWamgXZn0KBed>!U(nq+hR1B|3b2WRrHS=;NZf4lRId z7Q{?cXXSZ5L|fF(T=vJo`>srK&#zYXX&Sp!1+*ltN~Pbyc6uD#9=wm{nOwA>e%O3% zta3x0xySTg;le|%FGJ_x$CK$}g3o*iSKYTOb1MhmrYHiQ9elZbfs7bdWB!*L&4P2= zLaNwv?-@+I2XiP>PB%h*E$u3&_d5EFrDWq$#1F@jFZCS2Y$ zw>e5ycpW?8J*r1LT(GYwtU!#Z;P4;Qv#UC0(FJ~TYG$1gCb^HRH z4<~4t|6~+Mq{5F#qcxL!l;Ys+im=OzE6q(qgfw~Ewy7FxYg4@7rLfCrW{H#A{wQ9O z508v~#6XeoLD`&d9NKj7_8iQd2^eRNBz_3+JR8_#9w~|rcJQUC6NSaCTM~)HEaG{c ze{p&*N?(m&{AQH2lF2(}mY97x?1)GH z9;=@pk&{36+8l!rTs<&jdd7TjEhsKY*)cH&$4*Ytk)QSWV3xo3aATUWMO;Gahf_FX($epT5*l#56dE#SECzxZi&%7^e!hDv+f}e_}V7tY$uW zX7aeUnNtyTH`VNzuDo$4by(^frLYx8Qa!Ywx`#C1=^h!&BrS%PvXHjSb7L@>#&atE zh0C;pZz_Yk1^Kr6TDjA8^Fa5=*s=$riVc#c0XI5|l;2)`8kHR@lRO+2UN2#eo#>TT zK5*fOxg5`E8cH9YI~SJ>H<^B%6!Kf8iUAKMgx5g@BMr#J7DuK?O!cb6x0u?D$DTYW0hI`wxz5eoX&}sY+~L%if`T$sf)49|#SN^Sdj6J{l3+S1GUhP3S?@$CFLtoctfD470}c z8zWHp$z~(Rl*dU0al;OgqttT}4n9FDU<<$3%EVJcz_1e2++8`yM33xyZK4i)pBJqb z(@(3AGfP|r4F-D3JXr*U?&H0tuSdsrR97t#h-iuq%O*Oz8}=$){8+&;Xxuh1>QT#^ z#BS5>-;-C5YkyB(Q9f<_(>o(jTPyVbxfK-OfEhRD$0OtKiJ_Jss?PaP4yE)|H;Q2uvIc(M3so(7Sbs9DEddS zU;%v|!a^O}L>k?MhuowTf7wLyuMZIF%}`26c;Yb0F}BT7pCmPspu5QpvV2?#=l9@q zmXf5~{w^ydJ@hqbhxvb{u!b<$4l$-nM%XVkqD!&Srz13GSF}0p%r-(Z+d5&kt=iAJ zsU+{RhbiBxV^{Y7bjM=T*m?}LUwj=ecKMb`_P?hHG$usL`5dqiVZ_;`%pdTa-dg}A zgJUYilvIR(X!{Nlo!Rfm+Ce-U_z;~NQ-=SwO8JqfIWB#f$;^5yp=3hB&xuM}Vm=#@4f1+no z&mK9LbTCF;KQM@-D4~3|>4lM|BtNeV)B58jvso{XTc_Kg>VY+^@ZX?Ji&G0)>VYTy zOhyW(sa5~_+$bFdvRw3zk;zc$jziPbXifGAh2ls56A&CnJe-g5HP~uu%eZ#7H79#+ z_v#{9y-dBpQ;~vc;tp1$&?gRJkRKX?4`{Rlk6jv?cN}tCI6OAzF6QK%KLM2X$H{FZ zOCkU^s572eJIO`!3q7LGDd)KZ3oIwc>NUOV_&hC{0^a~|?z#am-dR~|1oB=vd9;@= zR&jU!tDVyL-iVF`3*V_Uwvx^d>99=JGdFjD z#if;82sJ2)P8^+YEh+0-*ro^aR{wql80r12TdoWGe5H{4R_FIAUA2F1cSFr&bJNlu zv28NcCM*e$GlZQesFOB7&3{0$UgR~R@{*y)^hT;-cg<}#-it4=^=ELNv7NuIkvh=? znCa^)@_!UD>iQ!qu5li=_FdOYGh%l^e1bS9vHpi_kT0P~{9NYC9$wTYzgxEcA6ZiK zzv(blLJYD%qACIx<;_qFR@#i{4PI~y-a5^~COy=_2>})A?@v`XpG#8k>isdQ`v+yg zQ$nFhZJ&9fT#2wMD>3>&Wmky8T$)eA^e6C1DnA0F2qAchew-nK+Nmp5s? zH5KAVOH|^H2Wy|po$MVC>$D4Cg-Mw?S!R+ZW@`srkiTPU(A7aL!Oy8HYV#?||15VG zX+q3WCRd-R>~1=xT*jf!vtg2czLP}D@*B|DpZeC9u?<}7?D8deO!oC6-5#%w&1*9j zvqi#{Z{5Q!^5;g1x%EBlq^~X_I9cmYuxX91bP3P2#*QMrxvjVT zZ)qdWV7|Z7h96uinorCIq`i6TM2TrgdxhNR192k8bTQ#|LKB}m`=9tj<^$4+aj&uw ze+;Gl#){eiJ@|L}le|sSVllFGcefxZEjHwA04J>-TTAsAxgC@g{<{JG0Oui>{U`m~ z|2(KeAt`_O|JUgiR5%q!aB-x@Dj;N(8$~1S``tkRj5_DC{F3a_xTFd^#^T7elfLm8 zk*`6)M1ogK$3M9WWxk=J2X&lfNbt8K9ez8~aKQz-=Vx$Y1qkH;lN!;Qtm)kfVr-Onb*N2hM_|q}YP9&$p537k}j` ztFB>n`{wvlJJeZ@k-yPCg$S=a_{0*oeL+M-&b;+MoE=I0JQ<7~@K=wNlm549S3-;- ze~zjK0?hy7=P6{ztO^(E?wb0jNjZ6)C|etTyb3T(kz- zlDg+{mQv03SR?$hy`tY%XH$NW?ZX6a)IGa@JaToa?3>c^$Wg7}F zo1VP+kE*?S`FDid$r6h{-1+wl5bx^*3)4nKwnMq?JDj*I@TRXYc5HqgBZL#RCI}-E8)|xJp~x76i@AD6S4tk96M4Jp{?88=j^7_H1(;k4 z2LJeJk>9^Bg3~gGk4zCGaWV&v*hg?`_&v~R>Xv|heofh0+2DN9)@9>OL1Z9mXrP%< zxgt6w>3jF2h;<_g7tNhdTAiE6r){WP=v7W%b)U!r^3HJY5!iw5N1iX!(|`>3U* z4GxnAbPg_f7}SY6hMsp+O|==RV=0(>JBdTZ;mGnBI;F6 zm|mWHXFb3PBZT0)wwUXB)Tz2egeFBKIYJYK8K=Dm{9{WHD#z5u2^MxEbFbJAW_$-9 zrIk)->W$?QwGW#JBDamLzfqvp*@%il1Ky`XKliE)E#wW(t`g(%?ZgE%x@~`4Ds$qf zpO=YDTT!&s=x-M z?{hbF!>i^8jiEkW+nUNvlhi|}t#{=&U9SNbkf3S;mpUu;BazE+$Mw5Q){*#4W~UbZ zz&aR$xI(6;G3M~FH+4E$Z)|j;c^09J2eFZw{Zc*?1_~mP$*n0{NacVVEO0p|4R8m% zGZ$PGduYzMx$5mXQsu*@f?V=pM>tTNUr>=zXcB7r$4;H^@8)N7a$xbJ zrrLRX#34U7ZIADHY>zPELhC3bR7C=Syym{*Ye+iiycrZNRhy|&7Bzi0ziE&7T7`#! z)rC_PB5!{g=TcA_QCJuemubI2iY!_t(!>DV*=ck;?o@=H9WF@Y#H$e`oQv931f;U8 zC<@E+6RoAguQM^CBhHftMdR(oLdTp+liGL^F2f`5Dw#IBCP}tS+)*7SF|i_Es`D5# zX$voI(3zP)9dSo5sgN#;4N^)bj7G1>_*5=FApL1ftiw=Mdu)A8EavlLn`e@*RW5Jh z^3CDSE=`xxw!MdX+XQbboZKz3Btiw;tQ688A)xo5^gKji`T2RL103*7fng>{)vaBguC#sp0D|(HicXV<&||917_rw z?CeI2UA9UkEqS;Oqlp4T@i5VaH+uXBH0v{yc-~@9J&Q}L_nAxqvX1wg2LXv&ExxYC zfzv6kKW0y9KJqFPdv6`LgBw&N{YA2|V~eo^B#CO`Ce!MqT`?=3M1e}!@`UJmMViVI z0S8CqGrJ5;jE`yyp0iGhu*3v!DoRC5O%--B)kj)1tc^H6Og|p4`rY-rZok-8SJoN7+=!;1@;u#MV;DUu$jdE=n~2qBm6KD~o_4B3 z1$x=k+Y&k{K4{$`U2|?AVPe&YrYW8DoXO=!aI{a%NdatJSxDk$TG@uz+w+kk?SeNe2p8$a~Jb7D|6Ft!q0M$k&{lGvb}rTocD#EdHT&!E6Iz z4DfvIhQ4+%mAD`e7x5_^Y+m^orW(0tj~O4W!&B~4klb>S>y+3wg=rAL%Jt zbmN(j{T$zRU0v8W}gx^>Lq68Se3bmo%g9~%0@XeWQ^>6 zhoa{8RZ(8-!Wg$_##iQs4E;XAlKUspua$)Q=ks4bDJrev35GRB6;*kzflI8i`F(X6 zG#o#E#LmWLki?;t4G_ulN3lMFek1AneDTbbXS+WK2#7q4^hOkv`E#9b(PPl_7yy6G zd(-VxZd`rvNQFDQZc-n68jx>W#oJ_YE2Hq-r-ngqugorVD54XhCR%qK>pK6y+Mk)C zq{&^KPuHM0k$i%x3_rA2bDSB6kW5~Rq^f;vQzI zD*3Gty1j51cG{drc#Et^K{YA;Dnc*TpWvNmw>C;DfqvEL!PP(&PlZ25ai zd3(}ROXWRSXAMmNzCXW*elN`m-O*o2@KsJ}f5)m7v6NNNU|G|mbQ3kbz2lkRFfJ&~ zSLo*~!8!EV74qS(Ya>EssxO~-wwjQGNlG?QwJCC(HJH`S zw{q6zN@jm$s58o0duNIGO-5)zOIRb=G2v7|?PjRcKqFCEdA2m;)WXD!nE-}-;2MPZKj5TrXsqo~{Y)BF1e}75o}_D2JZN;t#VRjl;S427dYd^Zf-Vi*;ldQ{p(80?nVBM z)3iE4${MeK*8xa~!3bMO&PXX29rWHRrENu0QGd7$z(@=t*N>o-l@C1CqnXhS>)4GO zd_d9tS7TSM2k!}ESgrg+d0ah*DM$X9f%EqXJYHq|*Pb|eb{7bMY2N4oE@6G`L-C}+ zz$tk$cN2UaC~u{AUQQhs#g?THTavreXD!v5KVdyK2SpEiaM(YFN*1~%@i9FuZ-2)e zz$%1Wci<<-*Nj>W*k-=odps=#E#;Dp)mU4SKMCqnmn9FIElT%jhE&WDTs<3kMuxq@ zO}SDOyBTNW@v@zjJ6{~-!b_Sh+&|tL?mayg7nM_voJ9O6+}w#WpkXQ-7PkgDH-L?F zG>w0_>oWQ&-4$d9*nHVc(){|ZNPpMV-E(kqj(kzzdF}B`4sB}9)Yt@=j|TV@+fAd- zP@Zx4wf6>RpzC&J@v4YSYaMo+``n6PbbmMK~aNh=+5`5Luj;ePRHS7ev@NfN>fjCBO6f$k3-y^RE?Zx@O(VqHU#4_g*?`Z$ zlRTop7LYQ>OAnRHZx^mUUvrxgFyhi(Alg;=sODd~{}>3z#a=YI%?d~_oB6iqGat~- zZv%f=fSkYIAJe=S8o}B*zeGi^LX)=ixZPsRxXSHe&JKHncLFGnx4EizC0iEY`Q1P4RK; zV~;>J;JsrcK~>+C<$fhfgL5be@$%e=v0CQ`X+h7aJ_r;wva3T-bs1MJG`3&I$*0>1H|vb`V9)dW>>fEB3*f)wP16{2Dfy}3>)MIBz6VCfkqwre zH`3}=i`B>8blcyyo;Vsd>v6#=eZ=WT8>YioyCp~NoqYoT^sx4N~5k?leb_hl@p5I+cl~f9{`VDe(!Nz9g04}fFwse z+MWOjvpPmzjUNX5O$D-B*>R0ywi5f^0tvLbOA;by+H9)w z{mu!%D^Mik>FIpcbPF*5gktUjEb? zRH%^~aDP!}rPNH)jy>*JYkB=`vmULQ=GN9!iGG`U>}Qe6excLxw5yU-%%H+Q=ht|f zAd^)vV&R^E+HTUU`Vs$w4<%O20SGH-ep zz_`LNIT7*f<9Hi$%QI_*sbB}!hTRq!Ic%hI7K6(+NQ9)8wCCAB?BG=5{x$h{4$}Yl z0nZkz%I(3Urqf3VEFS8zJo7qXa#wWlaCVmrX z>Qa-HZyr2NNp$->TQ!bwXE`jc$`}=y@F$mXf1TVvq&mtHXyrhx#vkkeQqml2-A{lN z7aym_aj-`FJD1|J62Fe~H1-S(&=_lpS?Pff0)kpbeoX{{H*dDeQ&p1aAGT=eQDZd1 zUner8GNmfp?z0u-zC9!!D`}th3dacHWfWH<$tt&`*apfr^jtY{oC?*rCIVXjdC?&2 zS;tN=tx&sAg{c0X+9Qv^^V=|wbb2jY(Lh}p@B=z`F1r$p?>m5 zU~O9Gh#tTMRjk`&5SzpZw^KM6K4)UhwXuVr%s1SEV}Vu!9_C_PtQi@XN)MS&nz9tu zzVL2Tnyk~?!aVQGb-KoGU#GU(nz|4Vw! z8xhr>!#(XNr9^)cwk?`wM9?9ytP3y>MgHbGnw^+QkHZ!}!N*1DF+pXCdU;jCUtf|X z*KbW2nA9EF2*4W&XqQ(a$+PpT7ry&R2lJO6uEC#+W}QJKZX>g=Bc$gc@}^vB#eNao(ZW9`SJjO*G=oPQADs`Els`O2crQHU9cl<13wMl!rTmmd2dyik{Jl$F> zb=Q>_l9mIc*GBDQ4m2JO6{S(G0#{wJNSC`ugGj%pFk#%yz=nVeq3vUnlT%r$7j%U*Wdt4Svw+|uZFCjJ`it|s7kE# zZ=a(b&qw8F$!P%&SgAlfDaqrSLgS@+hS7b$SWYp+6mV?3Q`5SQ>joBx(!U3lCf;!k zxU<|LF`d{qbSnQyM#*?nvd6-hLOOq^{v?GrN8K1O#nOE2tYYa!)J(jm&8wr2SJ-sa zT<<*zHLLepZ?aG|91yLu>butugS?_I#;q4#{DVp%kuAfwj0fz_om~}$#9Wx>@4Gg^ z2ITI&m%8gjS06reB=)SvC($Ws@%DyPmeq|B_Qnu!% z*gSXDR|Xfj=k6j)r{;NLq*T*54tU`k*nqqS%1OIzc;sA(tVMpG2V0_XVS^HvtJq4_EZ_rrBAd z?H^QTG&IO(dOeX0F)FL&lBt<|PA}5x2Sh`m4Wl}WVMcF}h~ku;?yg+=$Vq+mDdD5Y zFl8wtL=ICCUWf3KJ6n$VVS4<5$U`CfsB%xz?hCr`{(SwVa^wbMMO0p4-F@SbmsjGm z9)2-Q-u9Sff81>4exmKKOb@wvO}~rj&65~s1Rd!Pa4BfpK`;exx8p*BSPdUm)Iy`)N|(P#Z37r3=Npl|i`wPQdHh`_1wO(CL5DGX0e&?(E` zRs_~XzSFPQuDtFpTk?we+C8K?0u?HgCfvf>J-u5U$Zx1T)nfvS=(9PCdpYeCw4C>1 z&-LN(XSKOn=Pr_)TLFeCnN#+DpREI@rKu&D`q5d&>XC897toGm{8Q8Y%E~8BxPb2^ z3cvV^J|`?vT-6K}ALd-7wk*Z3lr07z>*n3@#5>bDOr^pv->gHMV^RKUi(REEf}wNh zuir)R;g?zN$qH0po@iGl&x{~86)Sny$IIOV`t1T@Bm;~+Q!I>B;>-<%-fTk>6^9WS zT6H0jbcp@g$Gh3jZK3rL?AELbMwaG5nX>;OcRI5{=RK+{qzs%_;P>UzBxn^xWYVWWhYY)YKRhud zf9SAZ$r$y-pxO(HR5LWMI{79p2nB>c%(`q%x4!oDQ13Pvf=wwZpbDa@U^ZpH|DS20 ze;{O^pw?|%PX2nY(4 z4yIa-xnnazUUy)&kX9mHi0kM5q;O)+kgg=370*Cra;b;U(TH=Bp*mnhQ*-U}ma5x+ zin+DYp)Ouoxu7QkET;!(VzuW<(zd!5`KP7*U;5Mk{Bx<2ok17BCsn`_PA3JJq?i39 z$3hiw?q^uK3cI-GKM_F3phKw<;?qxwdqC)1@Ih}#GQ-s!og{5--hs2JkVz?tXLg_M zIuXf|fAwVeLj2!wq_}T&_JeO0R5|nKg!qVzT?RukYC&I?u>UZBx>1mDq~E&=%@OW} z+l``Df~i#dnN1xCcGBE-#bfrR4~*2_h_JuoBL-ha6-K-kUkxMHiND$GJ>;LnIW5Npv{|4nzq6N$qPmR!IaH#36m z`Fp43?C-;Lp6)s~*}5PFUZ}h{l+hmsfmBK=xKS4}J&>D$PKWnR!Rmf{H`B;Ma4NnP z6V^h&@!u4oCSxPf%);7I4-977z*xhad>W;FzA&l1Xs?=Z0CM?V=YR8(En#}VfN$rE zxMcpdbJ8y?QJY#G#Z42Y11zG}J5Pc-o-u#f?N*)Apl0_Izmcw%AZY0!%2`Jw%CrYZ z9NAFy_1zM*A3=Q4Gkei6OxMI@$Eef(Kjrq08~fZlEtX+8#Sy|^eNs-ubVFJ%I@D0UP3t*wde^b^|GpSK8wvOS>F`Fj%GO{EMSB>?&_>Z4ThAJj*T!8(ORq7f7M z=8w?S+eJivcVd+r@OySN?fcIhJ&Y&`(2CqVHSSUSfdjyIvj)%VdJ+>$8p zflnVq5mjH>jj(f*>}+@4pv6Tmj!cAcb)eaF&^KECWV@!>GO{9tQRvu`eB9kMrwd79 zTwl6t;!#;#P*RRTxt5m6H>d4gt%G7H-~{&n-NIB={3U@BWT)8t(8|+>>#FvDW=;S7 z`J(w}ta!?))3ZmxtN%oH8xnmsd#>(iQvf|~P4o4XFXHaFIP!Q0?;LzIGlJOpU- zpKFTeQHWY?jyTTTubCi^)xNZ~xsT?hI$^4r_w0m>K@XcQYJ7a)+-Y+i>+W|nC#pEP z>fiw_f&x|;co#Q>G=hcJ*MI_syB_&vV;TfBA*NWj|J#O~LacH!D>Txfg{8mj2v_@H zx5^@52d-taEMU)9thCAgp$ml=m>qOU^QoRJ4=gA15R`(Hf2V=Izdr<7AhHNq(3 z(XNtgTVrWMEJx?+3M{n%-ubI z1%K)tUZIb(nVxQoUgafGi#lMS(~YR&Ag#_&fq_A3AOkL|qZRNIHuu4Wc;du|L2oOh)GuCU1kNd;=e;Ou5>^-O)-Eyu^B9x0?9FQdQ_ zfsZdwGDml$^wj#?2D&LY^3Pf}zXOAA6*h)3URmS*of6AoQgP~SYag(XziSXTU|DDP zYiMbT!13fpw~EVO`i@ZdflJNeDP{~-iC*REE}o@%ga!(r1^M_RX)$`qg5Y5_r%I&& zJK=T%2vBFprM5U>5B}(3K*VZkVN*&E0OVihHA`wVpo4gzPaPzdI7h9iuZHpp6h0~< z`9sycai-*=drD`9hO^;EGa zIMtx-tm%1)v2s%%c(kr+?O+IG9X*JHnA6d=DOkJ~F*8+6k|Ax!EuT~^;AKCshENVcmQ@5D5@#S*BRa-?2}HM7C9?W3=$N3C&qnh;LO;Od z{*}rk_qM1^7tcVc)J6%BMtc@Oe6gPg+sYl``yet!xU;@B(5M&?N?9r9=9hX1>KPQH z9{w_IJvytR`a-@SiJx>U_kSNTL#`;PD&01V`CQ*hW(dkcHt2SabcR#~f=m4|sD**( zmn@4aRYGw2co&t`_#knTgt)^RzH{UnHtrHcWmG*%k7jC<9>>0SA#sy+^|%18(>})! z(*ca1iO3FKT$^H0KP2;R_DB}3!gB&@yIeeq7b$zwVw>mGXKzf@Iaaey)-FHxy;1M7 z_YQRmZ&|M%w1gFvy58vWmPa#(G^b7djH}TKCAq1_y-to9rNYK{L&s0IX`_>e#X5rr ztR_u7aN&H4?J(w~B$Lq-jg2_lM;Ui6 zi})&%DEQuv-ShdU$jc=vhUu}>ZCZW4)KN`nw1zN2F= z9br%=!SMNJwCz`17QJnT;H z|9VVU2hXyW&ZLBYYC`iv>HXEH&W&{}`RuIDc$Po|{Pcx+VoX)i21|l6+A)C!e4v{w ziLV(ojE8unt3At#D?I#xWiwiiPuz)?niMK6I%h#dL8wMH6+8P@v>Hhe;KE$U-om)x z83d2M9St`50mDHVH zV7iq$wi5{( zoh*KSpX02%S99Z0jIMn>r6HjjafvRc#C1JQk;6MbXj2%BH3PFoLL!(x&IZ&@p4pLv zVY%4G3H=!(k^H;83ky0WR@K2$g?~_qpHvfWEgx%C7v4(AYeM(S3(4rxM)*^>ngjR_ zTCM2T*xm=lK*Q42vYApkZRE=&w`-j}Hy3(oG+uYu-(;VHh;O}h%Pms)_+gM%+dw*) z0faJExB4MH26n>Fi9;!SJw3<4x&54AY$KNCrYj)mu)m%=(F9SD+ic?6;vteov{-by ze%-XUyABPsgPdgeBLMWO-^!*aS!GB-<j>tGA&9UpI*IL((85Av zDln(zqU5WAfzo;a6~w1f%O8^;X2>%3 z+lxN#{^K%=oPTM5&2k*o6-mWkK!=6{a)}} z7}XbbV@oOgtVwW5a2alvC9IaQsOn<&ZU)HZ6;W`7KHM-N)^)x1ql3w)M7e}i0W&tL zz)CP0on)s;%TcY62m@HDCWKm* zD{}op^H+fc9Uc6lqDHTB5mBXjUVr1zd*Btk*;m6pwbW9reTdl8V1X{zKuE5X&$}X? zIJGpYFEtFgWaw)^OGAD*&|wC@ySNu@*I5ur#Yn?fg`TfiWxP8^AU$Y%>*4a79OLuX zs!K|W^TJdEZxkhhp+sLc017c_AVqxFuX8W4;_alAo_+7b=jy_RUCV5SUm$y&S(+L#LE z5dt=1DFM%MRTJ)2{+ImBOw2E3Z9!&EYA65=L%Lmq=V5|_CQDlt9b3|PhBvxJ4Enkt|}lqzCHDtsFInLN#oREvK8@jmpp& z!*;d-MJ46c3cj7@C85zLj*E&-46)}_$L9l-I@w98YT-J)*0yikZekJ4zKz1d)Va|; znDE!x=8cCYCW4?^2p^|^n_|MJ9*!e1)kEL#Y_R$1VVU7Tn>ksjOLS6W-sF6Zp5tu; z%DEXgkWIipsByOHekVRAOu1-@t{OXiF}lKwK?sP~td8}wvXTsEJto*A z_ltVm6|xEIA3vbN46N6MqBiRxw_(kx4hM{POVd(W z%eJc$b(Fq1y{Xw)pT67*uzH>S&PAd%duDf5%Zfy5`*KMs;2_jEN1gk?Q;$lfrsyyk zLaVR>wj=~E7b-x$#>U>_kS&Z@(#Sp6?Bn5-Fmr0V*)U?E#k3YKwGJQ1oyxX3stNuw zkrh-TUR!6y^khA8+oO5hq}}N;CPv-$(WWrQZX-gCRRdmcP{S$RzF3LGVkpKK!a5oN zc{x58*owY1EeFGWfXU;dNux?^9mxfM%x_45IeTbnFV8qtcBh}4Vm@56+&(=uYOEEjP`8-BIqX?H`FNWvn3fu;*^Fp;)=Q`s$(yimMpTTp1BW`4}9;7vuP zaYuS_^W2%|=|D>1i!sR3oG!7eSa+4S)#a5i-+HgeH2L>k61W^AQVIv+M)|R8RWTZt z9M8yjJ)B}>n^Sbwl2kysQ9E(I&;#F)CSz28sBb4J{C1=jc>_^!WFVLq%w#-mP1@P# z=;P$7nX4ym@7RGw`-XxpYOZ)&#)ae2$tT%DJuoDH8B~8O6OeFDI4izk35EFhA!Q{^ za&IBiBVc$>*z5pI;!nTYpGdu??egGikom1w-G-`ESIQzIJ*4dCPkS&O-d%DHYq~Kr0ZC%LWa)m*(%+olzDjN7?yN_pmK@_;ky6)%up7R@wbvhG@h9~)jH8BW^o z%TjwgT^b{poVh1whcL*|gUSLOaHY+5J6qQpO-*$mQZ8k448N1fYj+SYUB58tuucqX z)_3<0gfl1cN18b$={q_B5*cE?0A%AS-V_b9S3PSWsyf2b9JRv_OR5%nfErp%VXGTB zZonaJnlZZ5C4c6%|HkRrCVu(;)+#cY>`YDoA?EC-oKC3|?MD4)RbQu-70+|EBjYYK zf7H>Bey}Yf_)z78LJ(cmd_%qIOg?U67F(r|Wav`jp&ulxe##AUa(SaZmLZg36j={( zA}P(eRdM{{oZgHd+wwdVJ7l=BJlEs$AZkpg#?<-UQsQm5RZd&CzPQHP^m$qX7)o(fX4o#6bSZtg@2l$ zr(+~qaxFvu!}IOiwpHVYj2dAYHioGWqMdK>fHj`<&;7_Uo!$9!>S`qVC96!c!dN_0 zmVB%2mBd{50@`L=-K3+Iy$TiYp#pUuKjoj|P7=Tv0}y-UqZ2ik8#St!j4`jml8n3t zMN^&DG1Dt5{B=KLHj(J0#_V_H{)dG9U89-_tFhzYCZoc7DgevoxjQs7y93En_3psp zq81GcaVM}B%bmndHt)s@oqs<-Fyx_d%hpZ8$`9l=Hn+jz@b>G~~Gl${*Y zOMo8D0N+wug5dk;Ej3_sW#hQ*Ac(6TzXxO1`H-$S8XL7jy-V91wjfn-L%_yUNo*%B zBX6ikURm`mieB%fezSE!(F{amFg>(_r8F+;#R|I#&`*Y7@H+ALE{T7V8u>7-bXMx| zle6NC3;ivrci*O%k3r^UibCf*Kl#PO-!O#?$@r=O**%VH46FeGYBr6NY-e%TdeHiN zo2H^OtbNPTUA5K)v#?UJY8D)Xh0C3#1f5XZ96vI*N>{}?WWr$}s-l-KWU`k;_NN?| z)-6p#Jh#8781;2QVza~ ze^6`Dm>cZsBQd_|=x|iQYaRgBAP@7#qG^w2T3?fF7CWlKLb(ZsH~&;^mdI ztEQZnb_LS^pn5Ur4IcC6a0wIWD{0ueI-g(a003KZM4ZFyzF>G#SGbymjyBhD5*ORWYV>4IH|YZ*lL zjW$hu2ollJlm)=FH%yn#ewUQUE#a1eUM;CmOqQ1qt`idbg9;%g7H3p|n@^nnN^V^1 za;px%nwVUjx{u=!z*2XqM-JWH_{CEDE@kwA|02sxu!5OZ6)@y@hmCbCLSZw8O{l?8 zqqY3CTMrsm9&4!RV1DV8axb8Q|G#h`&O$uc&yQq&tH`uKZw1JmT#q)#hnBl zP$c7-8nhk!bcA9eZ_lhma-o1=p!2A;F1v`Tg}h<6lCOTY5}#iLIetxV_kTBc-33E?85OFF$X@y;zbS7ZcX*^ zY7Wm2xtlgeCx{LCM6=BqC1n1`pp0b`qk9KxmpO5tVe@b zKKJt%&t+?wT91~(3NmEHsh$jl4lt5CScDoNFr@&E3=+XjU(9cuh?jinco6IAvm}UY zr*j%?2)v7)S47P!Rua!c3$E$nc_QN*I?tL;*0K)l2VzDh=DjLgto4|Q3XS(fRBB@y zQ|{1SWT?I+(R;={IlY%ot|p1w0>c7B+%zq*XivdnrQoLSDs7)(FS<`kOyN!f0!E7c z2clfVLGxo9u!|t>MA^bexu{Ua?jqJCB$$d``jOdk@hjswFQF2bIvUqo<8h z?p(EivX;34yr2kgEC|@J?zZ_9wMQ^e$XfDusg<;>D|ZTbTAbqg))m{VgzqUO`sH^4 z2H`IQ6e2X2rzq!`U!>+dbjR9J+d=vG(b=m}_`G3Eh;95vX+|GkKqD2K+$a+M$#K7; z)!qmKJoaO`0T1N#*?QQqi6mCosr0_Y{%sJPG?&M#`ESzgzZB5u{Fkru8DB<2QWZZY zvckF|do^Bw(R4Z?A!meunUCF!T9W(1{$kWIu(S1d@Yq3z*l@w2Mb8+tGHD|cnIaP< zLf4KMsLhIA?V=9aG!o8tGEUx5;;?(a8ncOEFeCM!^=>D~OK!R|WZk{XAD)Hw7qlLG zrR1!6uj|T_j(D8YHn77EpBAOo{#hU9B+H<{s&}V<)7;9^w0WqgG~?WI6g1c~e87BR zTWGWG`&xa(iS7xjPm@BhK%;-armp)EzTkN+#M^KUz4YnA`u3G=6x z#_qytlA#H3m-bmZSAQG?xNL1a&($_!|??1MnA-&%kXzGJ&n__Kd4X^|GN_1{}12# z?{%Pm_4U6hk>rXrmiJwiF>fTEJ^Ird3o#TJ6GxGY`|Dq1(fAw{2{&!y{fA>XcN9XUp{x@~vw(#xQ zWWJ>(m1Va(|5AGYR@nb7N7i4m`2P*&`0tbly-@%sXbt;+jV=ENkMd`5_Z#S<2_U$; zk$$sYR0poJol~c(bkcHjto|+JvcY@+PEFN6=(Iw3I`r55uv7~+Yzc>c?>Z|yFau!c zeSXN7jn&Pb4fI+{=U4W{G7K4EFCV9x8-H62R= z$cn?wPS?B!FvP&pOo);M^HG4qk2U6Pp^wsZv{oy5a-+uytB|rsOf*gB^!r3ksJj;M z6QpbtTO%XpPgs8~{tcCg2ryE%XS@;_BAsSbsCOyH$fCb;E=od+UpHOIW>`jFSkjGr z4#PJuXpbiw6Z?03)qYyMGYq97yv#$svfW%-vB$6&@DHrS7- zNj$644&t|1q8Hl+A>d|Sj{Hh1JmD>TMorEq8ufm3e7Q?QkI7UqA$#4pZeEd@v(9*f z!9uUq{o(gPFH_-i4a58$f!A<+a@t8|?WIk4)(pRWca0Fs6`w1tyiB9x3m(SB&E}&M zL>(d@)3j(IE7YDyHtKyXMitL7x@((t*qvYLCc45N`nH-|Z*qLU2;V)bpv$Mq@aEAU zRCCVC!9$^X(o_`>5H&{Bk;wcwzN@ADs(h1EJeSzA__89ASI)*ye9!uKg&cLGV)EB* z`t7V5#%}L446_X5lFDxuTks3qJ06owy%&>FEQH#4uSnduU3%iLCfz6Y%KkwSkTsv# zZ~WZOw2YJv_f@grY*jzRSeI&eIq0M~_~o(-V`6%O)vPDhzinbVp@xAw>N`L2P&R4A z*jP^Grlqy_%yy|X`xqgHPXsY z-+uko6QLV>S@|Wc4;7}0fyuGT(kBis6C;s`uqA}; z)yX+rOL}J3Y7ej7(l05|l0fd}7hRnl9BczZ6FXIbOy(7q$K#gcgK?(DTN0VZ8*N3o zyQ~56mr}BJqz25MzSD>gzJ1=VN233_GsokiyWFvc)Bpyqhm>r>`8gC{gRfG!suyYA zguJ4W*r-z^elu=TBe{qNQ8>a)5Nm%s94hMhz2;Hnkb?JE)))0VyG$y(63Ujcni^7C z^Guyvn+8`4LDU72!%=w95fOZEZXg%1gj46Ci9euP12R#9NwuXi?>m=BZ=0xQnnbq% zJH!T8dq_VbcS!vAx|mvrm)0eZLiR+AbHZ0}BQAkS};o|G_4%b*k) zHk(uuS*zV_j(Q1`-?tu@#2XQMle?YUfx5cT2v}#&0~Z;555&W7d>Ib?yjsr zv_5agnlZrV2&~7>^EjmJ5!hlnTd!3#vQP`P90kz4!Mn3(J7#638iSNrd(QhjqT$P0 z?B%k3mKEv^d5W9U%hwH}Pp%Zc(S?QRlA}#ah8lgwA?PnldM@l~3H}rfKc%+18Y|yidqf;cbO3GN@JsS}g{%N5(klK|qbkRag@qBY)@D1s=HQm(7NFcA6xk*N- z-c_=xiMdxCQkn1SqU>If=h}MHGxlutAb*N*QB;nEt#RfT(D*$lP>n&vA8s_<)F$o7 zs(npU7>+Zn`I^w#{h2{s!TVN~l!ZU{^l~w?!f%*sk^#FiB*r6q=})p#pg5{k`gKRjxbA^GTG(8f*OT=o|1l*&ofFolBZMZums?*G)SF zxsi^u`}^@ULK00%#(cw>DM@Uu5D3I$0q2}ntNM&cV}i0RJg1kLbbg3I7E{>Z{pi)v zr``gpwoS06H_fg3&c~-{jZfZP8Qyn~lkpeaKthupj?=5CJJ6#y^TCtn`9z{XKn|<> z8f*Gzfdgk|eVO$9bF0RAg)Y_D)vHFabUdMjv-W*<4)>;-BNTrDmaCaN11HW3PyB^I zb{^&4n|PC$W8CUXm6t4 zAmf+754++c7re4d)N6YdDl*C+UPWSf%>828YUOy&O>PSh9R)2>qNCM=OQA#8$QG7k z7Yt)Q?8r-u@I&aqF^oR6H4hI2v(N;CEn6Ml6#t|d+6up5M4u20UfTsw?YUN2fO_5T zR+$(erm&w-^`b+E9u@R7B}H%0GQ&>Ke!CZyMd`1=n()4KCzy^Tx5dBS;QwO^H zO%hPre|N0;PkW|+mid{o`)RUYg!}ftB_peNnwBd=4Q}z-G;7&x3r)&!Yyv1CZ`>K; zXW`)Y_wMm+;P0xUp1NDBx{(-u|8VyrlhFw)3hjVxlZz2AciXWO>SH~L3PX*Igzeif z0Q#32`_LmQxheWYUB9=+dQHILLw_8xlas?@teqD@NSI&+IvX1&$1`Js9K9ao9i*y; z1b+)|D{R&YB7tALr{~r&Rl{^+f_8=z?bfSZzSq9{O3B_H_YC#a-Rsxc?UkIhhJC_q zR}+)lY`m{$e*d#DCsW`Ls;7zHO1_H`wtnRvAALD^*_N}>{Rh=z!EcG*2LIJgXjLMK z?f>U*A938=yHo|d{iZ60FmxNO<3;W9^1untAQ?p-n;=6xcy%J0JOxQPIbc$>gP zn5F;YrD9g|PNT<$yM`^_d=za@((h)uZVNEDS{1z8Ar`F_l17LtvkMt;uu$}|@UznG zsZ9tHGZ0BG&3)wmPD%OqugeD}hbDs*@7i~IKh1xp%pE@+p^UXJi;*CKkDzGomaVAm ztC)x6wh^C{;yHpSH113 zq5f079KK`giHfZD(nV#>&)-yZU213qDHAiewe zI>lqZUQqmuXtvGpY4c3P!FW~m!d*RH&OFxfI$V>@uR88XR~MJf3Ec<9>s`!6Gk?xy~^t-BvJ zFRI1gqj^i)Q-zgA6iGe__OJiFi4l8(n~-(3IqWup(%*;-aCTS&>{4q0#c<0q!T)!g%SuI z1VO6w9za2A=pDtS>y56pTx;*M&-I@3{`ju%2V~^QoMVhR+8pC4_nnaUIQ_p0rw?k% zjWuPet||8YzK zBnk_(tN8=BS5Lv04)g;FcNGhcOCzEYG5SOLLqecIW&Mxq9%D5sKk##Z+^Ihy{{L{( zH22#>3M^7?&JWbu;+WdD9xUW9&kspCyNkfl55(Ue5?%kRh<}LqzRvj@q3l4f=bCNwG3H_x{mvk(4kNDru z|LMB_r;g%Bn83e!z_U@8{jK5nnUbr6rp57_e_Dgq{Wt20f4kw2yZ#EP8NR7zPRx0$ z_a@S-tE@(OxfAUxgrJjuA$Xt6_Uo{J`EM((4=?k+-g>Tjs@xLDe6Q*m8{*{T>`e|i z{5zH|@98~^)+piB?>GU!1flvOC zGkgmV{5wwj?>O_Xu8guCUwp&Xg!SXSduqS3PW@+9VIND*6MLC9HZaeBWu5nToh`vs zi)Wp6pTmROJMR4>XT87c%s0Xw^elb$;Z67S^pd}`e)g-*0mWMGlUMzp$p6>P!58n* zYPjsQY+$C1DfPh8Ab=?HiEzpbXJz;Wv-{~^=9C5v1KsvD-1dzN(O8V(>DhxBEMTZv zo1R&l;hiwGrNC)+*8d)v@0W@q`+W2q;VkjS92;Kr^l@PX@1HRrf2~N^uRoL)k8=FE zZ#=z*{j<7YWzXfQ6M^;KjP-7-6OpRJGTT!eq>dk8@3uYn z&VNU12ESBw%Uix9qyWQwo6Lq^UN`Qh4a$^?5Eu5lo+hXO+tggu=*@lmBl$^lRj)a{ zS+}pjOq}vcI6EeUK`j(PJJQ=3SNh3v^+!4=CMZIj^76Vl*)50Wlx95$k4H(|Er*uB zldagK_jimMJ^L{BLu2Y|iB2qkllwvH@|#xer>}sEZ6%6PeyU_Tb_L6mSi&t)!!5@-~0?o=>MQk3LJv8g~ zr0%~njC`+799==2@$+Jq-pL>uY~#S*VKc)PQnDOVupB{n2RVN5HjDX3JEUPtcU#7$ zwCk}|()0a}(YAUev-q_{T!k6EF zCalR-L8T9B40Lw%W4KW$E1`U4oURoj3g!&6Pm5;jUNU3lvA&($kG8kq@q0jr=~Fe& z!wx;K zF*@s*Oz?sFm+GW1{pmQMiy9^ilz3$d?}Cw8yQ>+8?V5KtqAsV_10bkngffnDy_eLy zgDsif1UEL&|HIo^2c;~txVY2J*LTKTtEPQ5^0&_zwol{$BW@kmt&tu8Sx#Td_D>zY z?M5H@BrWk-E+R7#v7jz-@niP(v>eAG!@8nBmS0Y|kHvAjpT7CD}sML%h` z-Sd}1r`0QV@CXbXF*{_fGj)K=?POmU>{;8G&ZFrW1Dcc>Jy#==+orKD|7W)+B)LVa z$Xu+Kj%Hs-h3txQ)F}A4#0kZgmpk!Rz)m^9iyWF&I3zqtM+T@da1#Ru8AhNoSoe*r zA7SX`F(8I#wq?4>bm-1YkAbf7eb+grCk4w0PQYzlJquopXmm07hDQLU5RzQ}vVFxP zSWoBanwt7ZVOquz+R5e)#WUzCz4T`a9_Cj?brX-oKBcj-X5F;MU2UFSUObY0@Egq* zXY$DahSL+WgpE6U|GM*|M+It5$xK0Dr;va9S(0ax+={;!m-=yL-&1LR4Ye6`W8iS0 zh^WXOA}sS52RQ-+<{1ZkqZqY0d)d2?>%^Y3nHz=Vi90c?KpcT9PSb<0|X-D8waatxAdpvjlktW1;O!2 zVg0CLr%7RoAlzf)B{8Q_t~MQ=#%jq5VvZaQegx!prKVZ2$gT61jev}rbb7}{6=p?N z7hze3f;T$r5uZEF_p0!e#sP?r7wfefqAAo}C1X7|fR6^N-)-3$}N# zElySAEre_;L1UV-FI6W8EqWff<%xK%_HOv2_IVqz_@Jg0j_Wlrr!4y}Gu?sOQ4?;( z7Z?eT0s5h1XA9;XU-in>{}7zlL4RrY&*(ho?DMhnDq6_1mhY&r-Z1t*LT0(B*+~?L z!*u@L!t*O;56gm4fd3=(@g>$WdFNkuR{yrqB96vVcw;a7YxBg^YNsT>ojvCr5_lP6 zvTu5e@~_BX8iMQkzuu@3Smy74E&kE31HeBf`u|Wy5z8(R&CX8lrv0{?kL*R-HBlUF zY*)$fwZhICnyta{i14CG@evm1?HuLvTpk{D{guPuTtUI_IFAmupKvLBx&VDc{?-;a z_35_hKYr0F4bBZ@ynQC{J>o=P%O-L0-r%lQbci!1kH<1H>Ko~sf(Mx=KHHAZ$J?&x z26bc-9=g2YK|EkPulaT3%YS6|gzNFoa$pUgqh>pQ-AVO#cEPWy=GQKUPyT;Z5rgvf z04X1le7(u`v>Y)>anb&1@3`AeUwlng4@@Wgw$E%zeC~^H#GNkG^B-Mu{~@*hJ5Bh$ zzGo@ZA*#jsF_ZAv4KF(w%Ld}63s=T(v<(XX1H*W=``kn9F>&N->uMGi`eQK0{xv|b zn=6j?>KR_+JJn_B8op+`T3a(`RSB`)j2BJ!b!z`ll}Ij&8x9 zuWl?!%Rf#SZ2zj;!vFK&`d797uV3%tG#@=82Hp8PA0h=TqvfqzgiUuNdB&tY`h>p# z7$?vwUD_LhL!WsP97k_6g=Ym3OI+t$?Ce1{q<_A6S~5pu1ZrduAWJS3Q{S^9K09{P zyq{AIwCp!8+bl{@lFu6IpLn-*(+71$A`K$D`YbS=EssR`4P2y^GDSC|MqZ!sJF9DD z=394v8E}*3wBZFe_Dz`(G^!0dzq25Ct2%zS$R?8;zB{RwL+TNHgCIdsFyKM-B4(q! zU8hHTH@uwNrlco$DvZ(l_<2;hcl9EZqmP2C3>o#2l9nEULk@{VL*C^qm z^%0bBMHyhh%!H|^Uhw~BNxw!y zYKIIA4D9C84_upEk?#5?z!5s>4`3;*vJ|*Ei%d>RZ}aFE9rf0TqcqndY*>NlWiuGe zZ$%)WWHQ;2>T=1gN2VlfErWPX*X#EUTuMYKY(xl&(qc?483QPn)+sp3y)C@+DUo%# zJjKXVSq=zOew$*hX?+f4H6o?@jJ#=#j6aC1;S(DZcz%4&WANT6+<_?1DV-x%CapD# zXOPpS*GgjF1p&#r!EQ?7*$dpS(_jYb&wy+SY%B_(qNDiDWFWk>BN>0Q+x{=-jP!+| zPyLPit`S`$l{ntme4R5V+q@eY`2=PD7U!SHA|8EG0x`1t1$l|j9K;U4q=xm z7a>sYb4gsQ`4*9hg!?rRMQ>*Eh${^BHX~9gE5|Ln!wv3`+y_ad0oEHz96H`4#}^$H zB_p4H?q}$kN8t<+ND|v{DFdb`@VnUb5vqVW*Ain#Kl(&6-o0xt5XaDj@P{Y3+2G&& zZXR_roxOq9Y0z?Idy(EH8}82OOyXyzs719x+nPm$Jj8PRS@2~G#fMj|ta{tEPRxNZ z2^ECAQL6o-@!X0z9@4TzBLLY4CB_!)_7iFzx7vF+Yq;Ry4Q(7B!_qnQs?HHUw#v%qKmmgpe};IaFuN^i!@(TUMoRJU#~7$ULn zOza#fJ(1Gu594zYXYv7+mXfDAa(s{;u##YL5`?ISPRBZ+c)o)RD=;LTPhGIIqXzz%huRSC#-$l`ap~ zV}@N)F_T@3UZj|~yrl&v!V34J5oeNckVK_R>!gyA7Hv$PJhKEmg_WqInlmMC#Z#Iv zFm1kOLfM%??5WEX35N&V7A;FpK6wDB>-FUt*kV#}X3tXLuyu{tNEB9{UF~wfw~B|4(QlYyW~K!npYj^3B!w@F|5C%}ow!@d&bjK)&Gvi8m38)rPx1cRU;VO)Q{(ueAhX z!YgmZIiQt~466MCcYEsw&BQtuy%qZ!gp!-;5I8tAny#4DIJ$cQ6QvQ~K zFD&wSija8o9LXp^60Sjom;)xTbl52}EAsAIaH@BT>M8%8yZA9a*>RupMvwH2oKNGs zI$={bXicexE;>f7z%g#cMp^PSbPRMMP@HYzW}Dii>bRpYO6p&jK-tA^^Mb>&YW;`@ z4{$ZJ3NtTtiRhDea<(gh9JqNzAX0SPV0QELIR18C7_g5e8y4m!CP(3gX8M=;t*(_C zXSYQr8qzz@<;~i|K7f4)#n0SWcYvqO=~QEpLc_7AOfnWOO|G0Y=Pu>c4JYml09Thp z&f6lCYD!H#P>(v{^K?WeYBEG*5J)*Lr-^sRDVL+WEOm_>cZ&jlD|($sbR^rZ+Q)v` z{@Ms29xh4_@O)gvlmW39^0 zERGyW>U-kXl)}rq97ZKr*OLcz+n?;y1l`pD7eykFa#F5t3SJUaP=MKZ9{}`zMg;h7$%|<05q%r0 zI;TIStBW-3=`4eUs|^fb{dQ#>@ZJRii&_=r?2GafwOa<=Bh#vG_0fu?6zv1z67r>u z9`s<5cM&u?>N_pDdQ1zVR7K7Oso&Sa>(i2V?sh|zv&KA%u)w(rqOH%1o$9F87{Sv@OEWnRrKVC-*aY`vx9`H0DZ5*U0R0od_>|%}I!uLa z)c^Fw;HRaVS{3@JRs%XD;XR=v}m;<_sA#c zJx&3Q-K&JXftC=sh+KYlo*(oRnr^gW*>~^qkk%r5y}KiJ5%H=0oq`~HLH)Q;7uY7U zM&h@C9Zx^9<6XUo+#E@bxaJwd2N@%3q)mtL_(_@h%)L_m+3~ z$YxN1wxVt&!TRs=Z}zvbcnN{FZ03VM;Hx$AMGeZ_OLEmg|e9bSeKR-8FA3t>Vp1Hq6A77F;9 zp-}J2h9ex!Cs@$nZe8UvTW$-zgX5en1VM@Goe0X|@o3Tbs|}82+f?iC6$^J}R+m+7 zTZcVb1LS?`0x^d;blWM_H8Bn~=^d6^`+hq+ZdGcdlF6KblVjKR(QEYzsEKZ;%8&A# zO-gf;jU*tq`Aflu=HpRCFG4BW&3z5BwtFga(oMv0xJz7e>b?~XTfemJ1BhZk9H6{A zKstQhCi%Uu@T6S#fN|xVkTa$HnMo!8vUt#>_g27MoDVOr*nc|t{C$YRLIeGMRO0>n z@bkCu5B0#y|H~2WFIB^z$2PeX7Gtl1(P=NNhx9L%!Y@_pPvXKy(SPVL!p>>x&KrDu z$kW8G`$mC-x@J-m>an&LRc0{4RVb%#nJw(m@|sVi-z!Lt|EdPL{>>@F?gvL=g@Us! z@f6(@_#T`dD>W+KMcyffP0uGc;7b{=6kn)$r#p>$V#5}^c(ZBzGai}Hp2{UP1;3Ac zL7~NhM}#n_EQH1Exs15OmenCFEOQj&a;()a(~WO?AQ%DDR=askic6 zu7&Ii|J&DUGy;oYkh?DXQ%@!{MDWVt3`B`|J@1@KIuz76u{*$)QG~0!93G=$RruPw zhtc{zSuMzS^a@e3qh@Dny~@6)2HO-Sx}{4($Y7`!u+cbr&1@T^(3Gg zoa$v%GKFb(YKho3zuEM|l*&CXwOb>dqEur13aS!0qFJF@!yl^De~T;M_c73RFLV~6 zG+p*Wm8?U^@z=l7Wb%u$PBzlLsMZ{R zdRtCC?#D}mu70~_o?vr#0}3-iFZI*rI!M?#g}gIM)@h8L;y-Q` zQ?z13GH^LFlB^4BQW#!U0*>c2@Iez)CWuqzS)45qfNc`?B_CF&=Uq^P5z?Auk2w)G zl!>G8H67u+m3)afd^4%WrEaA{Ei5Unc&XPfL{K#;=bRvuU+yKrJz+Xf3RPZUvJS9$ z4;5|pVCWkBDTy{=Cp40tKE#8~CW^~Cj^^O_d*ON+y!vwUmq!CkI`&K? zub$&8ssANQFgB7y4hvdni`CHGE8|0(q6A z!nvL@i^Wy@x!~3*Ss1xzj40@dN>_uegSCL1EH-S8oGrc0P%TO=)0eWk1*-q9o8l9E zG}iz%jA{^FC*Xe?9s5%?{AYm9kIMeN2LH{AR`$2>pQ_xSI@$;pfZnv=sus@Pv>N$r;!;I?7_Meu0hHVKDA97sfx*KQP)^=#~*KG9QRT7j@7Dq7j}C{cQlj%q!`kIAO7YntsL2K==C8;Zy z(?_p-`QZOx6c3@6JeIX+p+!}al~4#md$%0My|{reuvl=@9}Cg>NO{{mfp(qc9;4R- z1&`OE|G`4g^sn%JPCpC;2xr@mHO~>w*u(H&n0^1+2VpvYxb4%VEjs6NVy9+tr{QjV zA&=euJ^&O^aXp~I&PDst7h6AW#mP@0p@a>(n=J`Ih?@CS7;!hjqia9y4nKT)WQF~K z?~BWmp&h9wElJ<&x?e^Hew8T`DO?m4Iopq)lH+`;xMLkeJT0Xd5OGs)MDM;eJW|E( zpZO;KBj48o&KY|WCGgsa*gxY5WJy?Yp@B)ajJIiWfvFvo&!E47qkMhZq!kH;4O=jq zCAmlX@|^btS0}LSo8q%KI2k8y;nJc5!8_~A*wQDha)oM}*P3vhv~A~F+GBgy5w*J0 z`svsGxl&Ojqql0?aC~xnfXc+L!0;Ts>nUVm*Y>Yj@TmAO`LqV1j&ev@=R#Y0oLiihENFtvcx#9mdHw|V#cJVedVVwMq)u~&S*=< zl@F7bMuzMDES>wHkbx})2a{X^Cra)%IlDd@J{8FMjY`t*Ykk|8eU&wVw*r!$yF9bD zn`-0r65{~cT&oP9al+R4m<=0Xxq<<5Bm@cOhmqjlR1*BvaQ|sX`lsxd(a4x4JB>hD z=O4ZI->+r=Nxk&1V6}{;|3$s@*G3f!T>BRurW6A+#rVSRh22?$&T?_wpq$}n6T|^P z-LBUmQD+l$!6*L8pmkMAkB_so2xSs}ceARr&^oO;{|JQB#4#(o`>DNi=n3mW+i!Fx zj_Z{^57a%8o;L^aJtZxY!-D7@y>i+vENL^{*{@dvH^1juZY(A#@Lwx?sCc6cWnbDa z2h+^tQX%gUoEml)J%h&DWX?})iNsri0g_{)z0EG``awLdo$oxTVh1g#3T~zyp{OS!wojb&0O zR7FnuIr= zI=FD5G?@d?q>5yuzXPsm)rp;sf|~J{iV71TnMPK@SOLUp(z8t1{L4oE7NcTdxIM$8z7aPR0S(lZk-=Gt%%1d<4+3tB-z(<-@ag+hWfu zFvuWMubMI&o3OQfv#b+Q2LEtAB}1mgm7MVcgStla^#Hz#L!{X9%Be;jHS!G7QxnCamM7QHpk$wE-M&~-Q4$8x z(c+T+#8cOwt!cUFa0q|qB0MCH_vxEuk68dQ9F(JhPiX%FaZ-?wpo*?VD}uTKQbmZPSOwg0!^d zCuCG0TV?Vr0LYajp#!1H>EYj zOG7{hMW%D^0Q%P-zbkRf({Z;sm1R3OPnaN}6F;a75vTej9=JU6g0rhF(xa+g_u2-} zfNz!%gcr;ETs*4Q+x=%nnuQOVC~RuxE69^k{aAZ28d2y1tZ?r2h!qEAntjbA+VmGF zex{AEU-?>m!I0;f6*sB5X$ZD<%aw4%y6{s3tT&4@7v@(jNb&*1hN3k9iBO4U!@2e5 zjRm(ohn~a^3A%%?1ZQg2FP1n%g1;u?+ov`%ypN(!uuzhO^t1BO0bf+t=@^;c-PtwM zapCt)^x??2L3&t_wYKlw#wm2aBP8#V23>H=!sVP$AApxp_NZ?UaCk4X#wDk`)9Kvj zianQ}d6P1!V4=UV6BXN2(ymxUKh>2akrO(vmwcwW4dz^AF0LrAOk*gjBU2tgrHRx! z`JYBA@9pkC;Vp%`e*YbZgRds*DQ@rVZ1WC;X&5K3N>Om>t5I>;NrZbv^HRk}n{vX= zsQiK1C8o`K`hjnB(%6Mt937JPrg7y8*Wjje1_z`Ia=%NWx}S(*DG180WOG7EXHn(7 z*Osgj5Z6kwUIGkK22E}{$+VwagIipDxJ0Wr4BXq#FbEWRSOuP{4QmTp4m|DSxjG;a zuatV7p_1Q66E&v+EI5SR7WIde;IBJ-#;tEL@MV87d4zU8(?RcKeQnswd!hkeYN?)G zBA>erf{g;C^^$kR*vD8#T%Gr)EV-}brnN#eICY-A40svYa@E|7msgn}Jr}y*nwtVX zysgv^in{eccVBWzUWvPjLod<((YfFk1=Kge;SI&Hj!OqwE104ICP7PF6CtTda`DwD zrG9DKwMnIdyCBP$r60*Uc&3KS6OR7&(4?bzZ6znswNBr2Q7m@DHEc_J2kde(D8&;x zo{=|vBT?WqOD1^knng5*tbW9{Irg&v1dAdCr6SF^I2{;VjmxP{++rcJ$TgFx2pDL} z4zj%J;tJ9d?^vuaU#jb}NfdpQ=vs)i3Wzj_EttqOR?R4hpx9P7mH?CE-8}#aINP&% ziK^MaS1H^T!QM01NE8gH%Fi8Ain#jCAamW8w+tS8+Z^eo-@O016v9XhvJiD;5TTZaN7(p{Jot0cX zKqzQZw6j)e7_65(U4G)SBc_EVJSvn_DM~1qG<)v~G54?_eN}sOgQ9u{`NqoChsjwc zb_$Yet>TaFr-~W2wx2)uDZEaWMIfSMN`i`}{W~SOWK5iW-+zO6^=u9&d_H zG2Hdj?NT|-+8$2KAz}%Jj08#|!itMpN3^&;L6m6Tz|UqUuJwzY;J8bE!2)5-@qjNP zH7c7YX!Vqt$$+IcB`E@~%1t%6NxZ8-|y)8MLOi0jD z3i7`-HhS9ZLxFN_3sSw>VS0ZN)%isMCsHC?C;G_?*`Q3g1j9Npfnok4UFLN}k_9w( z4-zp~Mj?ZA#D>l7%k89`o@UW0aNh7`JgC*e8#p$~QhN4g@V((~vG)v}$)dXB6B8~i zc{h$p4`ansm_Lj35$N3KAPp^3RJ-RwN4nQ=PW6sc|1b@YuY_xbT*)YVS_0-gFujim zu^;Ljlw!Q>ESPL9Ya1P3E30SDYCb)Bi~LsC)R7IEaxDKHXM(vr?;P>%-pzxDg{xb* zd#j(|ymjPQAh$Xud1`SFb~wUS#sUu<-jq03eDk!|YmC6K$$tAHP{l;2!L6iYvI`yA z%`DZh%fS|=y%%M3yOoH=*>({u`xRS7DFuCdj6U!CTnV`M(Y`#MPp`wqp<3tcf;;m1 zZseHErP5>eG~-=T4qrmiNX*Kf*C*LM>6B#5#hiV4<1431aib7%<^8?fhUyns^j<3- zVTaI*+omTlO-v=PZ_KBUezfSmsrI^W01ZU*aO4f#E{M_c24T!s9pKRduQ*iaOrB2D z4QC22vFsZ>M{4IAn{DszWv+pb;_&^vmRL*(rl9}i4LCX?86ll!VrF(5?Ri$+qNYWl z4iz`o&z^m31`}=uIpW7*opSXXjU4V?q&c-Z7!7I}kWs+yWNLDLB#cWU3Gy(~?P;8r zlEIT?Mk0`XTwr%~rq;Cf9QW*^Q6Lc>{itMm4)TLN@=3Yd^q$)ZiO)6wm)Z3QpOR*S zCE+&Drn6|m3Lo!%@7&sqMb$~`QO!cX97RGU0cx;#GJbdd_S+OZW#DT62xwcK zGBVadLxYp%8V^}cniSC2R12kPFqJqs%mzE98T~@TfZ7gPy1UF8tl7sRD7U}byB=hi z(a0Ec5xE?Hpy!?jwuvp3ytXNFALQNXAry5hwCnV`MbpGg_UVDxpgnYN_OZI?BOx-E zD}*DzuNIgm(aQ9~-4)#E8+(hUGk@ms0uy&^{=8Foef+LS4u&Z5hOQUC>2FyEOLtsP zI-SZkn;+Rn`8~$4*?6WMat)9PoYTlZ74r?dY!IcUGqI18Crg~5lAAWA@PZ8ZLyoZ6 zbYCs{X5plXQrGczo z_^*7O*1fFq+w=DsiX@D@r!={YYp8Bj7!VhMv zEvCE?u}WggO5~vh=&F z^X>8Bju_x<&}$eL-O+*^Y%>t)I)%OfByh5@yz{cC#uAw}EH2uWC-AxIRWOt`Gn?j? zHN^!JIXraIue7_D(u(1&AJ+YqrIk0#+@roS6@B)b z8-?WJfl;M&uCBa)QT^0DKyRsHm>IyGJi^o@(%jz0NrvMOk(L0aC*Cpq+EKdd4xKS) zuYyX*2l}&9eq6Q^TCF4|Ih9!> z(G9~cAwgoXhHr;HgzayIdY-;AofS#P?M=xu-5PAV2&fQv4R!hLo4~HR zRVA>xCjd8%9B@DGc*sjIYH*{~0nNswDye{G-rxwNYDk%1t9*gwUV~I#2-ELP8KDn4 zWTt0ai^`&>=o<$7$vIQX9HMlaSeCUF%<5oV5B)$SM~@HaXN-r7Z8<*GT= z>p>Qe-4InYaj$3MeKGqIj(vxt4Cg^K>z36gc6xG3=3jW7%tXn6SzcFkV%I=Uw}$hi z8(6l7MzwgT$y>Ue6Tx@~G$LUYSsaf|=i<5M1!lUAWR%i%x1RV`3_pXKez^!3X`L1n z1DhE>4xnd5FF!QHt4fl(-t2qzVqc;0%NdvANn}cqut3fdh%hZobGL5vi&sA#d&@!_ zsdV1N2Y9o=J0t1CMD`q+k`ec5JngREap(pEcuhEBs~NZx1E@HiVWUSx(HAvpgj49z z5hgC5!T`)lxsL)HO`!SYYWzJmn8)q_-hA_1MVp14zE;B`vmB|l^AjexcWd749?WFV z@YW%NZ+`jwBI-LZuR*ZeE8)&l5uKyx<4J$d2fQPF&h`@lG9_8amWwPRiw}3_+3ve# z!^0WJWyu+MGVtpBcPvWs;q$GCJfG^4gYs0(t5D;Ks0J(Rw))M_4i9)KCbd=S&axkh zB1=ZXDvMj~XGL2F={@@PR&3I#!WRcs4L-N$8n&JX6nak%5|oJyash?1RD5uZ8CVom zh?>GSTv^6uTyiH<#RA@_QSn;6p2)Cu3o?V6t|&Pj3~vy|R%3ji6$6)x)f0W_3L8Vx ztW6xo3n|&|E&#D~;9N?Jfti!DjSqRtS!$dkIBxU_$LC|NUK^e+D__nhLl5FNDEd_l zEWeJ-ZizB6so=3)%Te7)-yS3%Mtcbh2Y7BxH}9SJxrHcHWVqhJ(3yIQas-k#w1uv$ zRgP?ovAlczaY%vd0A|X&NDb9~z>TVttflMWkiDk(&=1QTK+pP_cYm>!(0q?39yz@^ zLL4c^c#*pTh>=S*Gm9o} z3~kDO*?o4ipZ~sIPkE$xa;ZAqH7y}H2fefw!I1CVqBA1JwAwc|#! zgs3%Gk8r~BA1z7eUvG<8o>POA6tw6eGkWm15zx&1`rxVz0!IeN~^nipjEHNZME`{?2$k8q6{)m?glbn(@ zdf-sUjMG~%U6N5pFP<~bGXOkxEii0v!FT7QT^UAywqgM8GO4y{gu_GeZBX%W6tD7y zN`u{crwQ9438t_DVg*g}IGMXS_o63Eb}PXtXOTfk6vl@r{YGQJTHy+m-(eN`8YPvI zpfVh6K%R%TwnKy64hyNoboearIG10r5Q9LVz95>ueIKsQ1lS@N)zY|WC|2!jsLti1 zmK`TL?}%Ge&vJJpZdZ&gPeHhI!kdneaaDF@TU>TP6W0a>k|{nvvP|oDgV&C`#uub# z?l?rw-BX6&Oq0D0$48+P3MFhC1VV;~%xU!X6x8I*0HxKE1Gh!pjvifn-)4Mh^!-*s zCm+%NdHf8WXBc#!olG9s_gUBhYm;{87`NL5 zGE1l48P*_~_uYZQPg;J7L{s zX5XZxy_>=WfFjEj=^4EQrx?~!`V(86bQTZ`JA4R>&eqOqCh@6zGS{GK(Gto1IU?;* z;tU`L92iRVjdka%1_MJwbM#=@?mL>tz!ypHkLN0-6>|W%a}gqoHga686XfVApoT^c z7KToZTysyR1z$|o)C(8TO`vY(PUAus81${>JZZ@(!bDJ`6~c*!b4_m8^97tEvtdz+ zcI4yJrq1T+N1ux_MhCo;<~&y>4?rv&4@f7w*ZU-M3tW**?PHOu+_OrCcOs*rSjcY| zZw3T@+a2A1HlgP#I&^*D&TYl*6d9u~veV}4hWBY7Sj>umoL|;G*WUYHcz&F6yA>dY zzIOHHql_q~mCN~+NE;_~{6V#*7RAhflt0(vfNxe1m>+&Szc|*TYczPdc-^-71TcF8 zy=%5-#HUg}OI%q=VD@opvAmc?{>&U&Vi;WhRHIF5E{R!-5WdD45YF>1P^P%H^Vp|; z9!O_ew!noX(okz<2M){m9=$k}SopRC*|nr{7K_aWAF4MuhMDTM*?K8D;b(;_5>kXF zu%H%GhB!6QjHDf+1$#a+tOaQd6))qnZVDxk5x^54JSZ+_NaUJ1r(C?^L;)kqPei|o z#f+=nv>KG^c-(wzm?(DFrEzFx@5Aigc^_le=9J;|pa!Rp$Y@D=xJplE*#;cvbZ5PN zgJKR{Wfbkq?~6w*D^rgwEgQfXABS~$;K}ZH^WG6F?6!Y1VUEa_l44T5REyy|xdg=# zZpq;mC6d`*z&R9PBu>&r(U^m4Vfk|pA==g-Fwcq-#gi|p*S|_QBO3R)K3w`$h)y@L zByy>HBY>lY_wzvxhEg8)XWKi-FVbGQFa=ZJf55HJ7&(=d#9S%S{Vajw;i`qbYi^^W zCB~WmoYL&z^|h*AbQfoWwpwRr94!|4O9`yf;J+0a)hzUGgea7Qfu$sN)IsQU;B(P( z-xlSp(`YgbPsW)b{}%wtzj-A6@l~O&ZpYfsYH0Qss%091{RX);RI%=jNiR-eodO!s zh{lkem9|=2;7GnIWP&$C9J>e3VF1e5K6EALOr()m(G71Q-0ppQ(c5{08u^|&en4NR zz&6jSjG105>oDaXWxT|; z3HC?(k}~+M%#}1Y4j;jeh}I~_F~M-%yOaBj4Y?A1o;^PtE`EB>As2nHw6IJ{+7^QE zHEv1$`AjC%EhW`J57zLo8@eyCa68m@e?C`q_+2ij5s~txOqY#-#7Z5upg-h)_1lkl zzW-N84nn#0Ym%|=ZvArTxa0haWlm(ZYn?^_%R;(P(LSkvxFy!;y|7Sn(yF0Ogcz|0 z-F7Ev&2w@n5%KKCV^Y&%iFz!rlJd}ar%w?w-ON_a!B(8G#2w|Q)uLhLQzkH-YF90C zLs;o9RDwOvchAqr7~}cKaoF?%GZXgB{6=jxO^xHWqpxmfzm98Jj^WTCXO?7Re>@Q@j*VYaVvBF? z#`TCr?ZvI}*GzpAXBu-_ZbER)s)Lrb&HAAhDI)78SK9G2zPNf}HmY^4^)7d?xV*^_CuWyNK3#>);$U z0C8Ww#99r@yJaMA0`@r+z>|Z z%cIy>v-=p=8}C{(rxR^wPW3Y__r7nh z;5c67QLp=9d>Lm%nbW|&8H!(9_~AB=uU`1G6;qX2*{(h0GF+`oJ)3=1VnDNEsE;mj zF{t%+5EM+O-Kgdn#R-$3P*t2Wi+C{lnExDa+eAH{9aR*p7M}22p=T6tA^uhZZSemfJ!85m(e%p}Ms?I@;H?DW#?@gx46m4g6 zjn~dS8on!G0YKY+ef%eDvf{$A7w3=N$ z6`gTnrrPzL zF!2qYp*}{Y`*(GbFy>ZSCEXyq^(n>mU1#C%IDxI5!+I6F$~z_;t`Lu|6})Jv>b@rC zoPe#UrlLgJj=Tn9f&Exjw(Qq$d_NaDpH>RXylleT1w%qq)!*NUH|iz_1|P{u!0vo7 zT~HIDv#DVKUDjyX%~v?jNUKMFA!!~_zlxkvqHU}dzqIxDHC|gNzM91nwAN}bAdVJ$ zZhKM=cVlPVB{3Yh)I68o82`L>A^xrHMrk82{z~D@aaoIMfm7d62Lb7~8rs;ONR$>Qt&w z_QseX6frO;0U)8@H?>Eb*)9ULtgpDs0g_ILZ(orEhHd9_Ja z4|D9#8L!cZ^i|P`?%{~;d06%1fNuJq!+D2?$VT$)FBJ9>uv$hF<*b5MTaW1nwZnA_ z?@!DzVxp;L;%WgEEo!&QN_`%4OX_|`-Kb~^dDW#zF5E3G(G(#!UARsL*0@B^3FiHx8fDAo!cMJ`J#L(R#-Q5bJ??FGt=YHef z`@Qe)4>+H0f>+UeI&dvE$Bp<_LJn*Na8vX_(42&@2hM_uYI zcBK#*X%Rb|+*o~{7rS#~K5{dgzziQke;H-h`wdj(m^?Y@$y1IEA7_@!D<)0BtgC$M zTyv-=Ec-~da%iN(qGz((Iwzl7J}Wnzx4W7UG{4|2ncT47u`cs(tEyFvj3jZ(%{B~= z7g1p$UqhSvpzxvyO~{j^_~pCPc3wZk@|mY3v5LUeY!`%OOM|%^|FX2%qML!9l0u;o z8#hns;LsL#zW*m)=N9J3Ry=FM<1{a4;@?2%IMm}PUfbS=wcLU_DD zDY*fvA>vQ*9tpU5I7C7;c-h{S^Uh#&RG*$|d+CY+$=;Gls^;EnZdw<~~7XhM|}HaE*Lmnv9`Tz)DO{^;nGY zM7HJdaO|@nb>tBTDi@FD`W8Ll!)ffo;*5`gt~g~N*7%{&SjgmKCUWtXFXSU3E1I){ zuKOq|$-ZgKQ7ewzl!{Ia2~9)3+Xn2J-sJZjE5OhltAvqyXs5~eL`9Jf(=&@)@CO_p z^zG>RtLSCmGnKGxgkq{!_lRN!`Q7}CnON)4Q`WWNRJUA#;;&#f>M-t8p_N?Np?wjV3I~$$>nQa+=Huy-q#(b3 z%7K11UHJ?{2JWuxJ}}O!0M`KrDCfP0_K@-oqnnd%7zH`cRt+kxDQQ9l4&S47_iBP1 z<`Bru@;4?q&cIrB2z13R77CS;Va3gG%z%Jx$!;2avZHZ0aCOjQwpaqk86R%kxovqn zH=W%2EzTRH`ROS??k88*g1*Td2zyl2(0C$S`JTV(pgD(gp}NCb`5sx z4|lAz*R!8Nj^yCSJ(#@8t2Gt}E?Z8Ek&Bffv}onATLghGzj-mNng4u&lFiRqWke~K z6T>ATZH1k_)qdwO$S~ZVN-;Z^nLV;~+J$&ML#b`^(13j}x-$;ty~v-paqQeAS2WUD zYEAU8q|p5V_bhVTtTi-`+uH#J(-1TUZ1v{_JUsS~<<#nYc6su%2-Ykr2^IW}_BXY^ zftajHH*1N_J0A@kGBJ38p}EqR7UP95KaVjAaU`NI_p3KOORg6GY+=2bdd_vr=+PU% zW~pxC;p?U}y0NYs{Ux!vBlio@XogC4x4*3BqcxsZTQPrvSEwkPI_2?@yv!nP3nqr- zn0VV+dUur9pu%`F8rWqBXC zMS2cm(himrzM!z~r`k>s>fCy6zr46WRnL;owMMb%w}e4Cl2d(Uo1-{^mU0AE%Tqnw zaIk*Fe6#*BT}5@LTH&BJpILS`! z0+n*6yL-h4>g2M{VwgueD}mtY2T1>lQ0{T7?DX-mi32HB^M$M#UU&0kg0M<7$6YP^ zp7-W!lS0WV{UzxGTKeDtjo7|Z=8&EO$aMR1__C{0ChfaP!Zy|l0hC53gKl|Y%8+u) zBg){KOLG&gqDKyM^I6m7?m?4fdzx{Mp7BrGm%Vrs1`}YU_|ADoCZRkVc@^f-EQM#L zcS3|?bNDKpl}>0r7>9<;H%KXTR>;g#t*^P;h60w2U9=)rcoOvJ** zoli|ymui&;XIdmaj9$uuqe(vjH(cU(PHn!-Z&(=HFIBWFBB#dSwUDA?9cE)Tdvju< z??pj{Il03u_9X9=_WIqE+}zD!fr2tvdN_y9)IEOCJpM*80C*u>#Rcso56p54%4B#t z21#MD>Z*R64@3&~s%7|h=|rx!O{$de#_6>Mou2vX^0Y_xX4n~#Pf#^@_VJ(v2d{r- z-&*ceL_Xe(6gJ-4)yc66xgL1Sv1Zgx#ABZW@sXvY)Y5Fj5=nfvRV_OJh>_M$IebsT zn$QJ(I$e(pw5+EebFMtivx28laMO~GS;U<_fDpRq+z3nH$;q>%?-ThlmL(ElB`i;q zhTnhJL!Yb77EJiQ{fL1_cVtHXB@v&X7bM#WlE!K=2BOD`qT;9X6V54`#4+hRa$b0z zJ3TadxWcbdaclfE^-!m%qQ>S9b=j!gWFAsr`({SW9`^E_cNzCv7u^t>UW6-zRI415 ze9+jhU~}EPrq&uB%d5OYPR{?iXXKs&1mtD$3NIG!mF|?MYv31C0?8;zuPOJ2gmbzc z8Hx-a$r8ggot=T1wtid7^8A+dbX%KPdykRFRcow~0j+uyn@VNN^6}~n{*>y0fu7?E z{Y!MN-m|v0?v3+dbXn7Hk}Qw06Vn-EN1fyBsry~*k&O+8GIn5xUY1DYag8qGTt&(< zCsw_MW$e?EzrqHVH~x^5`zQ9{1sk)CwfsD2P*Jya{@UYiHO)HqgI7g6e2j3+D-9@~ z<_VAP^3Qsw6M{x%5xuwIE;35B7^v)2>4siwMWSc@{m#vU_1(6)DfMC+DmIzM;(IN` z&jz*)O8miv%QiVH;_bVEIw)E~&tl0n$%6a<}>hhZ{2SH|=GTVhIh*+7Y%7^3X~@g_`1JtQUpGs(D{klpYsWu zSg-Z7S{qi)HC?$Uu1yQoEUgLYL5o527R@Juy;1@)JAX!B<)I4w26`)zZ7T6TX~_Nl zS5W~xsnlg0pdgbCXPx)1%+{OzKF2EMK*Kn1?PlTfy<@@kT4$n_^~22UfdJTc z^iqNP8KpL37R+`{OvXX?3fPeVC35u8M8eFkAitLaUi#R^07dPFHc>n1ncT2i%7B~l z$ljun>S%c1lI=cXxZ$ta4ibEzm%tjjA6 z*O+4XB3^)9eHy5VR<@|c9Im84jk5{WFWV{SliT-Fi^>rbX z_WqK4vP;^R@NlNM zD?$@yv3TruhP^T2*K+#X*lfQJMh)Nn%C3oTU_xAvuIHB#iIL zVWlC5tl;~kOOw7@3GX(YK2UP+^W#1=as|ybg|&er$AuyL%e0&I%#4ckY<`hxgZ`3p zkk;DEq6}lybq#szO!=F}GA5GvAV^}y$hBC`et#24uT3$ZzN}}ZX*D13AVmx-0qW5< z&EVV@LAzX^-Pw1(DSq><9ep2KSFbr}tr#@y_X?r4>sFRKF2^qwr@rWJs$5y8+NiGE zC{^OdY@;QtcpN13?13m`9*qlLQ+9NwK^1qVdf)23dl(-MT%gN-nU=N*2obvoXZ>#{ z+M6_yzl08uzzzCiiQ|Vn!T96Icfs^C>DvEx_Wn8F|BC&Oepx1U?Ef+Or{wAVP%wSp zV&oeL@eTAPC@cSuonyzc@`=LRfP2l+trF`H8VqOPyOeSfFad z=6h#@rc%8GUM8y+UWgf2gm3WF@HlQ^5TvUZO5cd*HZS_&#zdS|HI5a0%{twQj{SOFZzg998hF%@1|ei$+c6{${ShbF z!G|e53g?+iqhw$7#Ku)}S&!}5Xi0iBg4umjt4WKTzjC)hnabK$l{Y`n)Gvd#RjXkd z&mn+~biH9|Ha%42O`lrFde(MdCj|v1i;qvTxApGs6s~h6ih=$^I2yVah(>$J6UAM{mW3j)mrJ*Xif5sm;`6J}IFsWEr07A_&vE zFa3mbKuUiWE;P@c55zQ;sSNj~4cR1;xtd~hg9zdsT4ghpFoaZx2E^3I)56j^Nkh7) z!MkKa_?|??nk03#lUq?&=>u9GcXW7gJ3Ulyo5qsEic3{+Jzg>`9G6ogqTdzC56%mU zmL1pCCRw9r!;)Ot068?Qumn5p%});KF_2UX-W9{+j^KnOWv6=LJ3P+~lDe8g}LTk(Qlv}r!Ot__-H4wt|1UQFbBfZ^X3VGr)XNe`I~8VtvsFz?9+;p z5=>FY)g$zd0V=8$ya#6Fwo>duB*aGsFqD;P_FbNVVd+-f&ovp80>$%gt{8dlV_l;i z3S8Ij|0#cKIR8Td46L{Q6&d3UsJ;}+mcQC@%5&^fC0 z81aFG%^H@Ia(fNt_T*!0tv8a4E|HhOr4J%}K{U1~# z*R3LwEE!+)e@%ZsggB#4`+7~%vuTM)L!e_qKvp&o*(f>6i)n`Tz^;8BQ;}*|$qTTj z|2jPZ%|P%t^oZ?iy4Uf28DX-6Yp|3vk_Tf7=c(s%FS7+OKe%jBuWO&n?4)imrivKs z4e38yrMNm%_A=Y%i1F8X!iQc)hbCPj0wsoB_B>m744HH)V*X`R7+$ab@lk0fW9Rf2 zXRD|<DlTTI13eT{$eR7J1Q zftOgoTkTg_4?E#mFnZ{9%R4j}Y8lbNQ!sV6w7Nf7Co()G*fFxR8>P_0RB*+*OfvGj zPJO&L-kSnnWM=*MouZ~KsI^%5f-M&y4zz|&CWr^5kX|kSsr4BZBFRHuX-T@ihQ7uz za!e7q_4A*Nm-hSR>QQU=U-c*6`^={1cod7?V-3GdKEjb_;m~_22}AeawmiK0vz=#e z=4KAf-j`o}fSs&{+H0phn>IzJL5$>B2J#_rI*{x@&qRvcoondJyCtfdXtg^XBm_(c%;0zHN;ruS^qk7}^y4j){#hdY*VPZVM7_D4juPDkaYI+yz-?<-uJVie+<&kqT|3 zGGTSL%X@}gi#v;z6ExX5<@W^D#Jh~>iO&r(wd=n!r^N|71?7~-99rLa+)|%q zo$)m!8cXQYE?xNW2S`8-%JA+c?5c5v%uN;H5PXkV?)9ZC6SiI$F%S^Fo)B9j0!{CZ z0Bh1oH=(RK?EpNcP5e(QUfV2rjR^ORYugdk-M4f3d(3M{tWIgBb!~1n&h6N9jP!!G z9?_%M#oZG2)o^&UU!(Q_WjL5mBKqV&J-Nijnmv`ru5}q%#SWHxETbktA*ODKL{4m_l8VTK3J zHw`P1qC*dB>-SzxMC;5qh4FQ>(1xV%3j?`I!7@TnTqP~uj#*UgfP8v{HQ14g&_(zK zj)(J$-A~J5@ahWA%i1h)I00th#<}57>P$o-mzm8r^?aR7wm9QWxx^o7;V`Gn2< zSDeJk)$?R8p)UfXZ@S?r!$L=V*s>EV+mCK24YuR%6Nlx#7+eP9qa{KQJWMpC>?X`} zeesN(uB*P?B}XBc0s#Tj4k&W4@Bf?u{^WhVNE zNo>65J^8xDxAzkL%jC>i*-Xn9;<{d`t$%g!#$TjnZa4L64s2d-556SXRqU?&2C9+& zqTeX|f4(v*9Y$uJYAik(EpxCqpb)l>ldhoz%1^&HDmh`Q8!-8ZI_K8MgV?Evu7ZkR z^~s**Wj-)X^0InlM{3ww3XY*;B>NzxpZ;#h7&Aty!Ne~&MR{ln!(vjYmP=40h7_zF z1#(>o&$i6=Jt@^!Qv=hvuDm_2jWQ7`EBi7^chz@Nol@4}5tI2UeO_SlwozCQ`8j&< zu84JkRB5(mcsb1LocJ3^{5FC=HKB{2-30s=^MR0i4GGTG*F$<$y|{j{r1T#-5d>5R z=vd%zX)G9j9&>@36EZj#@xbJ%sYPsS1-Y9o2tx%pzhV$`>`9_(Yulzu9kg7(zsC28 zgAPUK7kZN@BFQF?(xyI^ToxK4T(~B)`-6mij$Zn&f%yj@$h;!atZlT8a5;|>=S=op zIomp(eao}05`c9v1x-wA&VT1>!yJkZ9GPE*KwOGg%?iCN6FINNxWKtEAZ$1uKxI+WDf(J zYyVql`>&buPZ@G!ERzmk9*T!Gw`~HvLv+QQf?2nSH7SSMw!ic}Wa{)t{TO58=m8Xm zKCW~tWSoeY0=zpJCs=hne3imWqCu?4i>*S2`~4VEDC-edf2f=MqeH`w7i>6|z*a?D znDhi7;~CFK1bB25z z1`l7=foKp=qn&O};P0aPKuIts(9o$8!>x=glwWn;s!__h6Uw--_?^tc@@ zDM1TIF}?S{ekv3x4emPejZP*Yek*>=hz~G7*$dwITk}`PP|n0&k$`JYf6f5@E|`Mi zraoufxhK^O9Q*s;pJ!?dUq5+A-v9Sz-}tlH-G4OuDaXzofP@O@CBQcT zx{4C?doNkLcxRx;lzf~$`KxxCHZIE-RR-!1Hmwvk1|?9CdS>Jd5Fs@cU(iRTT=|=> z@oV}b<2njC@?G1$U&?Ssw}aF5_g&ZHFqF8&W5C6H>M!*w%f6W(+1L~f!C_w2{f*z% zSWV;(r$nUmjgroySKezK1t|)dV_`?Onlk57tDqI#7MMcWve_*Qx#mRg*)$V#2Nwr) zkXHrHt=+%t9Cy}v4y<;i|50?Sb*-4c8cO^{)Z`cE8RHTlqupK>Qy*sjDi!iwoWn5> zu;=|^U{mY_riOih=I#F4A4NSIDz;Bh!0#ZM%BolFMVx(Iq>KbG;ZWvtz#Z@S(BodOJ)lwIr zI0{t?iwSFZdze);>;E^7PNTEx7VmMc`c0kmW$ z`XAdqzog?op|t^Yt&6l@eKKv7pxg!eX?fOGQ`e_ybY3=?z?YAMr1aOF4 zf5;?$JxB@tU($_WU@M{10LW?a(}PK1SLeqr@!l34 zEFdT;Ab_Xpsk-&!){U_IEt)%7cy?rX^v-kKfOmUmrb~ock7+F0GoRKwV-TKv{x51o z9C2?La)0{4FO9u1u~BKP4;s|^)YfL}GbU01))0`M0p>7uNT^w(h2NE9dgB}8W~slf zKoUvj<8S|Pk%te7{=OCRy7@0RdMk1AvY+nt+De%JZ!UPF^YLl$u`kZci)CU4)Bkfv z0lpnzF06KHTTQBw|#EssFDGX>l4OKTq$ zFFW16=Ss+xcvCt$jKuo0NgA?G%NUwsW}Kfn(>6{@k#xgv_Oiz{t$Et!u{Vy6k%R-C zs%?6!v@|j_R2DLeSGRPvmk{t#Q8PGpU^=`!@x^gl!!#(K4|HS9{fvOESzH`Qb|=f$ zr>v^Rt09WBXvxYct;I>&KjJ~646I(tm%JlUA9}PNTZL%65l!AVazGcz7zlQC+7+)L zy2&H~j0F<&C6lz|@lLhIh_2Jv`F%qVKx-O=kunOoe{zo^x)Mj=%=@e#WB-Jb=+S-? zfgxh0wP6!8IDRo07vIxME_&qQX;JqY4DB`_%vw$xVTA8Vz?MytMogiduaWb~T4hM$ z;W8&y0H0NKjj{#?{Asum*1#g0s^ayz|vu{8*iHQb_bqJ zE6lt5Nh+4nB5BvR7)lV`FS09?JL&Vh|GMGQe3i5m?SBy{e_wSMEyu)qk71MT8*D9# zovNc{PplBuyR2Fu3FMUhF8=;l%>Qeyl>k__WdBh}0dnLY5SH%-=wJUX2$p*IKo9nC z27bz;=)Z}lKjqP19x?z^(O-zP$()9DB68zp@5`?eURdn~-Gd@Aqt$J3EJyG`WYew6 zT$$)<< zvWKt+uhkxf);w#AvcXt!Zbd1&m&>sjn&$ha_e*2l2%Q#A`U1C3$u{NZxcmU!f>E7* zNTBkm$;a}MDGUbOBd#^mko0CWgxvC+tMLtgmlU_4sAfMdRQNH^8Fq&|b6|GCbF-&O zu)4iY#TraL6K`9)13$>qyObmK3A_X2^quDH0Bk;OYRt9j!nrh9W^3DtCMh`Hi*fS& zD-~n!LYaD}aqMPWsT6L$?@lixLD)AC)ow|4$*A%q6T7m+H;{xz!c;fJT9l^jm_A&akQR_yG+K1P zAVP)opiK}>#bKT#3y2n2u2b*Ca9)R75TpcjQ*^%eO~R~^Q)IOhiW2CzFW1bAP@;Il zd^o4jJg5YF5}~yJu#6)?-XN6 z{A6&I36CRYAL!;MXP4@fSCbWbWEF< z59Iw1Ct}Gg;`^2dIX#6Nf`iRLH(+^7&g|L}CKEVBtwAX&QuJKq7H$zL6A#GcFyU^g zfVt|LRXN3qjI)t*JAHObM&Xv51#Oh;0)-SNw9AeTEG-w-_{umZy@In^t6IP`kt=)e z@d?KV(e5;}&#hZ>kQR$ zs-MiW6X~=m{IIS!p@!7YLW288}y>eND?Bnj(ZOgmPC=3&9zi zSmwjhOE_8z%+~3mpRf>}ioDJ0R~v{nvb(WH=Z4yNx<|IBHsbS3*FnoV4V@~>pZ<=C zDg9qHv+v3hPRUnP<~I<}H;}DrfS?|);qHu1$Luw^n zbnRfPi$_PRa+5R_d55X!EJr*RzoD?nV|_+2cK8YbRHnTb$;RVlem;<#0aS%~8iUoo z+;8Dtl&Ej9HwcR#w1@x$zPLKw72zqPHu>c$$}*J2`;ucbg0UlJrfpNDLJr-%^9rtW zo->T3?IPCbq^6r`AHak`(Bp0Qu8Qhc)hc&BnLUkj zO75-dnx(v5An-)DSAzgipT2Kn(fRsC@-NQVVUvNNuqveJ*a|^bB&6VETWBspU@N2?QZDeY;Zi4!6iC= z{@%-Qg=^Z2=i8YRfz0$A0cJ^Xvl;JAp41aXbp?J{`gmG?%|~auqvLML_nEvgewf~- zYkOIs2eVTPj7>qGKfImif=Wu_3k;0dRaFcxra`MS7Fq6?Rjrxa$j3K^^w9w2Dxv1JsJ*;w&iH^UJ29I0+^w%1`-cX`kn zj3=OVu7J{@r18*byUGrskC;$ph}~1nP}X5$qASd|wUQ7&2}iHY{cU+rb+oD9lY%_g&o4S&KEr}~)=Z~$9D14)sdhWT`oVIedE=-*yq zL<=V&0au25$ZE9;O|J8HxTE?u<=M4wPz&bZM2q2r_(2X33k5}sT8+>NmuYLkN2?ab zfVL|WknHIl{VHL(Xyuh6K~rwN+wRKcD|Vf3;QhyNNgm!4LF?0GizJ6riJK~Jg-8XC zN>4+DER+hqdvPLB|7r#1=2k{bM#?aqNl>_~3qf_xHRohjlQ7$5>65}ZNRM4-AY)mw zLjb7qEyQjSZqc-{l~r3~&0hLwf7L8IJ^Tv2?NEbI>%plH3g&8ryRXx!n_|_9>aPoN zWRI0y3$PWK;?(v*2vKy|&I@XE9r1_D!F0P;V$GdJhg@;-g=NfAM>MVGKQCcRZ{t}U z$+PZ@Q&Hvx*GP&sE;K=pdMnhXHb=B@LKMo?b>8H})m;k148HP&Yq;5(^HYY0sZaySa^pKkf*69g2JWI z&*VM3t8*sq?5hS_Fu$?qQa7ucp&xhvZq2OR&bRf4o#=|JWh45qm5c;vW zCvoQjS^A;bzWznf`Gk*il>! zNIH=J(<|(s`=eSR4m6A|d`-cUR46YE*azev&{I}10hs{-i__gCz79&Rl&*Iwynr{U z58zE&7$*;SlWMteH>^10J5k>lsSGIgUpsY`Z{`txl))@6t{eSNpoE`q|Nmgh#VbhY z$uL64W3{v0YS#+8c#`ae-|#;$Li`uoQnAeod1hhebQ18>}1?LlxGz_%wduK2Q9!JzvC5*3jlr!IHc= zT+45$Z*p^Plk0ve?=!sz>_T!_Hd^yF&+|C-48mdj=Gne<%Q=~T^V8jejScM4d$=M; zOWDT-N3y3TQct)C8~dYC;vWWy?Vrp_f`#3*-?Q#`ajWjk4cxki{Q}nAx&!I2X^rns+DWa?mj1tiQ9u*?J`coo6Kw3&&%*oO1`Dy-oKM=QS9Rr3E37Ru!UFCDUv}1!=|~NU9E(RcH)jH;rxp0ctVo>jh4cWsdJ6 zZ&*M>3M_F4FufNdnsE`rIVn>#-^XtGph~-j3XzZ)r}XSdC?S&enwqY43uS$Le z!oR>M(vUCXk{e&7u-w+Ld&13;|B>J$&E*~QZs}SRZkHSZXB0&jE{s~Z`jw6}JB@vh z40dT6oZ|d(hEMpTHAGvZbv}LN9vV{3>Vva}jQaG)qiuGEJ)T)YhX#HyTS(0aZ}G5| z<3kf}WY2Xe=ry4b{`RfqeEf{8v?|DoldwD+#ncHh3ArD_teUh+DRQl2&!r_ohm}_KbftoTOPNp)}xlF$%0jCYYLiNyDU^r;|O2t zxdJ}u&GtNZ&X}lak>Hwzr*KkVf89p&G{X75!xOT>xyW82&X~YJ^V=eNp{73_9 zyMRKHJeQ%H7P1c{Z>@`XfGS!gH5-uR6m-jbHldy8M8qYKTq=;=c);r#L#H>_wq{F1 zFzg%b?j;>mp2@1)3G1G)w3Sm+w_r{RPYzQlVIoKI6a?RA7f_HM4#qXXLAsRkZrEp~ z5MA>lD#i%aY!YFq7godcJQoo!s!rP+dgDRIY7V`lm4S1{ z8u%LS(_lMQ>kOYW94R&op}u=l^cbbmRHA(l{!3w<*$xa7akUYmEar5+eQn0p^O%x2 zZH;z#ApWokq|V(@oCH=!B%wIdf?HUa1R9-%j@w&uEFQzmp~@W&{ zZ^Q1yZ&?{E>flInro$e7cM}N6>wl~^_1sa_5eVZfYMX6ck$ce2SVWTtP+-4%40tm- zTB*W2vT`Zys$v;YSDggK@gqFHZc7axy#p`mMVRxx2*g=Q;P z4{$>Fw52HGmsN&jZs=?q@ibrd>c9>FK(Uf_>~igKTPGoC*gk0-2dPCorZi0l)YX=-|_$o#iY z;RvZKEVLVz!zJ*8GH&Pfs%1asKy>%Hrw%1QUM8!cTcJaHu6T>h$jM0|t4tl+xpkSF zr<-#XJ$^CXZc#Yr$^MmUjUYK=`m!zLRJR8;jxI|u{u}47<+BGz1?EYA!{y!#SgQB- zZLuxOVt2#_;4keK*M!L=FAej`1Vzgp14EAamgQ&M zdmIJrAOt-E4Xr3?mUdE!!XO*Y8?J?dL)~_n6mB_*KTF={B%I6dFM~8V%@SS`>3gMo zSFGF_+beE@L_7OA-1Id^8}*$_C>4jHEY$&Rx)Np8Mh7k(dXLwKasJ%mA5-&oA-h}3 zCGaoc1zvokeHY-I_JnR1bMH_-|FI$1?L#4-8!i z_I*X!-@MmwoO6U5BoL#CTeBLwPsScTZH492xD(OCMe)O-6i znKlm|52-3reh5~L5JZ!1U%KeXSn(H%q$&YP+JtaoO4)dx+MyHA4V>V+VQ0%cv2xO2 z5M$XDdOea@OL9??yFM23l(krnW@+v>P$h#t4)K63Gpmyo#09_wyu;(!>b*m$uW5qi0{ z1O5~6r9FCg#$^#@Y*pbuGJ>T#g#q5OWNX3<_w3K@S>z8F_{45KqM1B?o>hQU>G=FC z(?M&rJw%EP3npy-WIpA$LiXF2`M;K+nn!;uLy^RojN0VgmA5;OxjRnns)Z_p3D7b_ z{|D~KTje4VMkXa#Ix}*EP6@NwagzMIgW;SUq1GCKe|Rk`+4W`$E5sY#&|jT0F3?ac zI;Sl1I#WCazz}7x-^EY+ckh>UerdNE3C$B}I&^M4*}A_TcP{g*B)8Cp=)8MCf0Vj$ z?O)!5@!}T3rl(Dt_oXbUUkrZDem{g%ojksxBCX3%1{#bYl4d^!Z7z9+5Z;%H`W)_F zeDpfrXdy~n>ET_k%JiGGPu@Kae5Uk}AyDE99AI?&wfi^#PWgu;*&UjFmcD@(U7?$I zg?{>e5EpL??Jf$-s^W_crC$)n#Q+o^0At)px#D-vPVb-A&aXvtz+%}JimcAjEg(Kt zsZN>EzA+E5g;n_pGh&P_q0mG`SZZX?kh(P%CaFeU6Mm}2A+X!Ty_EO(o2`~HIXYn^ z9q>mU3$7kut+}?cS>M}OlV&+@j-fC=6$%@*a*~pIPI9Wxho60>LmMF*TY83OTchqT z8FLTiL2PknZX(e^h?II>{0c-n4zTs)7e&7&e3`TFL7)u&8Zq@?#5)jI57YA23S0N{*u{& zt=rM%dZBisr?)~KL}}jQbHU^YL^?CJn6%bQBihJKG$J>wq>zx(!5S zGLz4bVsXEnwZF11ORZI+=u|stEht=eG^~m&8<23YSbm3`=Y6Y(k1q-`G6@h;-%6Qy zl3?UK`glZgjRh-v0aw34b^slk*>%i_BA~;SUXz+(s)Z zC|_0}OP}8Ek-4v-m*+5F`!c-Xt@DJP=#50#r%`%_!+x`Cz4&0e4LvmN6OTHD{sb+D zCgI8_>DLNSP7}RtZ>*Y#5@m3*Gd!m$4x#KT!)?~}q+7GqJiIlpHR|4HWL79lXa(Ud z${RzP!yzF(;aG$U1!ygv5;v5NtK}>oRc_MM42bZSOdLzSH!zft;^hv%ckKz*R8EY| zr!&hQ}E|Yjh-BZ9$e`?*2%A41;bis zHPPc5>6#N}UC##9-0NcMH8u;T%RPd_i+iI{dIMkS`9HUeZJ@7=)kB%?P$=pb->sIZ z&3LObt@7Sw_~Qm!kDH&F%LHV(o;L7aXny}pp^I?TB+fa7`Rf7%!K2*|`)w1Oln^X( zEqm#ua18})I+2mFZ4#}AlObxsa1r(G;K--8^P-nN^>U0iUNrxSFd}yUJguxz+o&KD zekX^5LBdJS+$1cz?OGGLm{`?~-tc`F{ft5?xs2S}LDhB@DThIDc!SpJ!k|CC7#{xI z*fsq0gQ&WFp?WDukI>XYxa#fHwKx_v&%|~wFE*HY)JT)<^XTm96;4LJK$K&+{mbxe zQTBeVJI9!?aO~)X$X4dIW(dA@@!JKMmJ?MEnLeq-$F5a}xU8qM3v|UmH*FWQ3Wv^= z1()?{c(GAVx9s*SFs);V$UF~!GOsy{MIF9R6dGIF5r2)?+>}{gh0F^#qFj<>mJmF9 z@9M@}Lm_E9^$n!t3CC2)AhcMP+Ab?IKX*bnE{{-0+@`v&bw;1cIFrV$1-ATbt|`k_ ztsFk*UMwSTSFEVKA&+4=tuME7m5j;G;kEbIIV`rtsq3B^t@?CJwvM3=JseTsUG;pG z$0}3F@FnTfYTo#EG=}M@-5n=8d9_2fs!GmYQEj~H1=j8frZ}yavI`mUWd@m{oSUI4 zw)t)X#@ff$*sjm=($?q;d&C&uT5@c1X15RGRc zb<)NyhKUUE3@82c=P$n(UnjSB~OA^;DyE`9N?bX3Z;ap>uQb0YA(@>a@$C zA-h*yw^KI+r937Ch5e0nW3p)wG0<{77)7(tb)2%#nE)I0hOYMOIPsY~PLTtB!&9vN z4_5|w3Kw2c*snsC7GNCv`uLhVMxFS3UrmPQJ!dqYnU^1nJWkF^sn^bQ>PSgnhVeC< z$7E#OhRTW873{Ih499lDN5{-rmf%(~UaW*z9lKAxM9uKpg?jSt`c<`Tv)eH=FGh4l zGVHM8vfCND`b9J_&}~-RaO$(&jKZO8kRh6qq0}|DFnN*q5%)50!i=69#zPO?5kcaQ z(OX9zqtj&cVz%=-=o2NF{J~F8qdN5I7HR6X?a~X`an9Rb>Kf5v`suTSV)c&BB|Hh$q0y1%M@4Ib!EW93-e^vrYi%#h)<4ak z$n`DRkkc_+$hs~&&ZiuV{3H})F4Bk`uF%H7><@7vOU`wmkZzQtdzNCth-X}VPYRd& ztw#9RSPo^e#u}@^iYW!Xy9;&U^%uuGV7CJlk$R!^vnkzEJYiS(I*QXkL%S}BW+BSd z)O2C!st`%A;WS0l*a}Zmm|5dhf`+rY>?2MqBED6ef{YIfPI#~RcZyH%eFamw?k;Nt zO&B;=eda<=ZTG3lXyQ4B38Xxcc6dhT%?L)?;;g(5w^xu6AMz42VrQ-2d}C^{&7~5D zA!i}>I*nuyX0#0m}qIQ++m%tZDG8?x!R8ax!hE{NP%W&ao58 z-D7@aQvl7ZZ-1F?4jT#SHizW6$!p5abaq-kC;q3w3!!L@%#lEEA|JJ?z4;bfdV^GN z=CswqLg!Ksh>o3f4i$T9c&9-3{%e6GS6mO&7F5*H|u$3 zeYnx@(_+LmT{Xr%#Y$cb1U_To1zL;ywEo zU_ygx9xoXNdu=A`13ptL(uc|6q%z8IV#afSTD0HNgEmo(Dvy_!S1;h%Rvk*aD9cf0 zhOoP^V+DXcq%POX0vwXI52NY^gzA7{GA{#RxJ_`}VJ@m!M!{fTuZ)OMvUHtM=%(lh zJ1v@*0 z*s&O%Dra?F-=mR1gI2Z&m^Iyt4@7F&PGmjuKTuX9hXxc*v3Yq8uLpVZa^g$wpp_PT zssR(2G)sBl!KP&quawqGl3{@|Ddu7YEPtWcrzw}E$ucbU%Wi|!*y{5VtA@D{0sa54 zy|<2vvRnIrM*&gklo&!v8tF!n2I*!PLb_w?;ThCt|%WbZuzlYLBYLw$DI7kg4|DpLaBGA zBB&Xu2k3d0v(0Rkdr_=1?29-2(K+qrER~c3Rv4F?__4FN)+J^C4%aTe( zQfHv!E|UfQ+4X#ciYzi1eCx#^x3Jef-_d-C=i`hNUQ*6Wc~rj>kdB( zDzIA?fH|ywN-5r+q01{LZ*ItGn9vzeRnVf7pHP$ECI%*&5iGPFU`skQJLNWYAZzGk zx2f5;aJBc0h4Gow%v^c6Ps@W!I>&2+ps_0%aDZD6EtVkdKwk;jaf=qK-AiJmeDh$T z`73592F%A51>=}^T;u9y&C3#)I-O-{RQs~MX&3|KU!SZ%4YsYsg&fGmS^870*X}tO z-(SYRc2dl1d-@d%6WeJ!E8+NXTIg06N$QO-9gOwFjW9c$am_w&p#eaPoW^d}otQcB zH>;@|>7hsTQHht%5)BscbK<_w0nt3Mdyr^86g&=N=ge!#88$x<2ZIZ#wxo5|yj z*Uj2i=}_F23s(^V;cECf6-)N+gisk?UQ>@r$2qA}XWKg3DmwHNm4K(i6vFtaic@0> zk|wfJC{)Szb9r4K4N0EE8|nzJeEw=f+W`ndb|pZf_lP2@x$F5yVTo%eoH|pc4jESc zp>0+L8A#*eA`oY~vTT1Fk;nNeyUgp=71%g{w*9SIJ9#>LO`eD$&f^oRCl)W}MtHy2 zN`wQer{7xq8e&`5j^`pHrBBKVE=rOHcB?IzOXj$C^zHq=EIK;LJ`sJ0eH@ex_{7z_ zgL*m7(cv^b*OZ1tiKo{LL3gZ=zlXBajZo}z9|ank3T78PH_k+?ktTTTx-NAyb3x{3 zi(zD?nQvb$jyxmdx3hZ!t(efRoh6cAl93FwM2x2#N|tp`Em<8xe6uS@w6>#kHN{x% z7B_0cwh6^WKbFn2%z;$v%r^@&i=u`kVy6XUiZ%GI%fLj*wk}Iz~%S-d*PNYamJ|Ls6V{|J=#YJGeov zo6Lt3b**IEkF|JBuNgTc!Q%B*P)sA1XL4V?I0-vv`Xc@Ae7u)JHl|%}I zHl8a(IQpDvq7IJS(WtjOrgVJ0pXMoJyM*R%2aJIB?x@KmfC#+x+! z-#^5IJo`R|fvs*JdXK^sN6wS$WU&QS7MN6qP|%&9#og~>Zp_Uby>spPmW}I?Tj%pT zy@}4TpB;?O8rrh)FgGyTo|MR;g}MbVwSF27{3+i3Ge8;?_dL>Qmm8l(ksp4G!4~r7 zxn8bwUhNc-U`Ty?QI2WvOEgdKmqZcsFVaCCfuHi<#D7=*TU_;q+bgBa=YzqV$Xy4O zJcuAv&DDWA3T9cR`*+1fRr@&(aI~dGt`pi$wpJIk6~45|NOZ%(dqW`V05w=5w@q5T z*q$>xP#f5v7Yv6U>1f}%^_*VtwTS3FfZMJfP38Q{SlanJf0#!l?y;RZYasHeWqSzsiJ??xbifS?YY~VC&iUSt^ zf(4!%ee(-$2jjAFDIc$Uwp|u%Qm^8kF*zqhO)n%0JXe=8WL(PPdNs#(_O;l!Y(b*( z3&f`$H)cu7?ZnFeRhknXY{(^8d81yb`b1}6@rl36(ng&cbyN)yND}=d+hR`Ku)=WF zQ?(41H)mQhJwCwkoO_avJouVq25LHq5v{yYdWgQX6{$Fm%c2*EeLE%yfEXuyzhAnl zuK$gBeTP%>WM-LH3m`@`X59VZ?lpfL-HQoLHPyLI8Ls<92%8QrrpggQ|A;{X^9Ke8 z^awh^5UkZ6W;pAfFuUF6O0b@MPaBhGMO+%E#q4@Rb55!cbHBu{N#C^8CLUQ4yv(LC zHi3X7R9Mk<)gAcI#ag+Xw&%loc|oRCmA1Cpbc;OkSSUiRPlU0P2Mn$e0@Vm2yBCrl zvzH#n@|7M)2q=-OyxzfG-gu44S~%w~pr<^q<2k?4W9tO;ynZi8B50Q0((>A*8u3RT zmE63R0sS#&(&6AIqx&<2?i0PiB1Bs{N!fD_xisyw)dyNbZ{hHlG9u&hNgwxDMZVri zH9|A>$Zc(hEQjr4E31SknS3!fZ8usWdT(q9N`uKKYuW^?^@cL`ZLvjNB+hd!tP5d@ zMG7sNxS{ui*o?>5D|04jC7eM;dMEUoseq-{pjt-4HDup%J0L2Dma|vFt$X z+030}-L!O4pgxp~iDq#FScb6S*?T3>+SnlVay5mtyS7Zrp7_B2=J6GHQN*FHP?3?y zN4U~(z@bQGgDd7TD&0QYHc*@;#XjYICU>M_tG|g7w;kn?kvyN)N86lC%j3@lh|HRO z!M32T+^%5>SHS){@|8?>Vvep_@00wyWii)xWiP?>kY3c5;Uhb%x;|+xOE+@$x0bol zoIc$IcM%!#kE%66O0Jvn>uwmpFN?TU0?VJA5n|GejL}RDz?6RdvB}rQ>rKxgCQE8| zl$ieH^Saj>!FIN?!Tg%^(S~TY`&-UEUCln@o{1*yyB3TV15c4LH(UdBn{+>jgB3g* z9^YW6W*WcQ+DhYRB(x` z6}l7=W|JN-5H0Lb-IlzBvV?;N*%4q8YpxEH^`rLV)gORIUlRuhFQOFWH^ee-E?`>r zf?M6TdN_+f+8&wBLWrsq3t;8?9nDRz&>C6-^KO8o*+Kub=tdoH8!zcm;BP!`j;5Ou zk;ek*)r?Nq)EH2HuPKkyqgSUhsb=~iG%;h8;owL8_(N$Recx0#Iuf#F7< z3aunr;cmilZE03PW?^XQ`|GzDASRH3Aw~|dH-h7ldF+10PL?DC7xifQ4CL}*RDv9n z-gapcj%8_NOK2WkA*wd6{Zk)J24S;Px#jHnoxt)&!!pt-kBTZ~`-l?->W;K%!Q5;S z8efTv_{x=e{=8Z)u8S<52+YF^uidg+~j}hwX9Qf#1esmbVp^0+htSMM)%U+BsB{I}T zN27D2-U9KEQiV*@gRx?X8E$~8nUOq@;FCutf2{1Gz_bWj74a#Iw)w2>a;6t~ws*^M z&JN*i3Bg}!y%>eWI_2l5lzB<3;>JBb3RR$1NkzQ}tm;?H;w8ZHjtrgWaCANRZ?mz} z6WfPjWhdKFdZ~%^aMvVfuEv7OYK@(up$W%iI`80>n|O1PM{AqjwLxqvu3LHTyfrg;A6&o z|2mt>_dv-4(NE>$276NnLPp|XCs}8&=b!AB_QA6;qWijNxpsCzprj75G(SEau!IYu z9xm|+Pg!@?aYoS=D*!py`)T1C89`7xKf=U|w~M8Ek?oTqSQ1&qu7G;{IcNwvyB0?D zk~9{aIN}i;RC_q@z?khNFe!z7bJ*AXeS4k#ENE&kxCpuEl9F}XOfd02d{uqQ7HAx( zj4#IHq+g|4@cxyqQ_+aY*PI06E?sJwT<_%8w7V^~BYGn4eI{xtv!)=uEoCFBdiN?#*k0ht6*bbIV>Kp_+Tv>AvDJ5Gl8dV_n)KSF7Nn-=qDrp7CENG$$Paih)0mP|E&DE>K z$_))oIQIeeQmi)mi(8aAZC!J%S^&$9Q)9`zq0S~sB9z!5PqknSBY5)o3$43B8qRTp zS+mrv<0ZK2tE-35R{gHqq!_D>hZ-+Jq}7)~>O||r5$$!mik&)e>2`7^_4ITXbSHU( z5U8TX<-jpkMR=XyVYU5axZvw0hR zjM3f%^%O`eHOVeiXqH-71{5613ZWY+rg-8@w{|e`18ygC3Wpb36~ZTs`+XuD;f={F zQH-zeQQU_4anvCFb3OUDSa)j^2o+A&La`-FPh##9Cyqc$bI~4o<_#q!o|zbAK;C~a z#JGmVgysW!`+Z5~qA5dW2{-*L0_cAYBmH`9k6nYR$=1!%6PFTePi^EK${V6<^=P$$ zI$cd&jmgH8suX+Q`KT@dD;VWIBS*Ev?8R#yq3I8tXE#6D7g65ivB<#Z7NKyn8GzT7=rK{4knm}*I!n>xszuAXlv5A~jqIpW0ij4YV0$Wx zKBtkvM~o#EPel%|s%3*QyKZ!Hs&_0&2ysvG;V%94Mx8g*HG(yQo~}mr3;p`xAFVg% zPDr9|pqOASohd;6p+~3F#bjwGUnP~m_RT$l#(83Kn&D4;d)uNmnS!Q8{HFUC5!F4_ z{<^w&0z*Ye;}8iRkIp*jm>N8h9blhSXXT2fQ6_T6c>VsUrjBm^4HdRkkC*rh0KHzv zI->{{7`2sU^z+SxlAz9Re;{CGS4UIMxopNYqtHsw3QJW|<{*zP69U{hY-MBy#aRml zvuwe;AZ9CFlW_b1aZ+GB*lPzl`gR3Loefi5QMkJ*dcs~_y|9`MuTt>@v03<9tb5?& zIs~<0y>As=flZ>7*)x&)i%cF~7gQ;AHC99UA>)MNih$B|>BUAK>G| za^bObWKs4LgS@lr!N~1+uGND@b^#LXUO$Ic&4cxU&h)P{JVLUI^3Yyn^Qq!$;L&pVJL}Jt z{Kko(SCj?(+Jz=JvU}Y(df?fwBNxGCD=z46bHdUU_}u2G{b*fucI~;ma(~&7M_mhx z-n;`_8e8WRTVVUokBKzaq_7HHupUZPY~@Hibdjf`w%({-brZE?IBCEm5I<5_Sfk&8 zfltR-GvZ+6e~a-NwkX-Up>q{z$e$|#mSxK{c@t;>@Z9PqZ=|*g99B?p$to<4&E|HJ zP>Ttgdx(XlV=_KpHKwUj#wV|_z03ErTkm#2<-I3n@&PC{T+i`c=#3#!Cv zk|4EDsZ1l*saLC`HF6Q`^jgMLK~sf|>czc?K{uNEyPnR8;mKrqR5(^E2@@R`);ewN zHmB`?K9>h@{M#J5({tNb8*?OjW5$z?>qKwN^>(KZgP|umzz(e5@s-BUrUzKb57#cL`AiDNz+&H z6z)t{gfG76HJlA$!FEWg?ZjdD>d?@4hdqi zwclK?GAf3?9hH-)y3wLnIgFX6-i?jx#T-o$xki??;SN3202OT|)6IAgimtR&VPjPx zI(&*EswU2xhak)~|Q2F;2H709zJlBt8IoQmtS2{x*KI2WnuxY0T ze$&zLo3GU`mZQ(jD&=jxB z*Ci(w=Ogdlfz9!~ok1eyr$x*rZ};4~`PHz0?Ww5~&sq3)%qwg7d6mo62SIelWr7#) zegM+9rjFlvKPopzmaS;mlqS^j6TM)XV%}q%_9^-Sc$1cKtn&lVef!G%(TPxP|F zCEIym&ATPWFIR@}`di;nm+EUzx9R>&dF#AkjzCWRn_2Pqe^F-c)AHd5li&6CD*sHD z{%6MjE8_RT`CsX%+aVt;xlfg)lJrUa)1-3;W5h8J`#=0Pe7TUhS3|3>we|Iq&GDx) zcuO(AEaR50&=t-Jy)HjfI^KTee*d+sNf~gEfdO0dgs#C3^b17o+DHATu^pd~z^EH! zty;k4@z%2&>5C&+Pi!B4lvK=J=qU*7C;e~TM9?^H-Yh*Do6fsYX#aqhfGGvl7~W)+ zgalPoLD&3%3E{K zfa`&na^V(weUJI+icE}lf})~MY*Mn-wKY{B)ka&+)6}w>Ix0-=qiS1W)!J%G0nz?O zG*bvkA5(U;6cO(CN)DNBc>$K=&p<9Pk#ui;*|IcypxYT`PuV6yaXaqQ2POM?lh|jq z!`m^AHFFN{QLNKyJ_yVx{3YO>ej-+XychLj5oJOgr7CQ@Ge!{V+}KtjHtMeEzU2p( z0~6CuRxp3+GLNsX%Y>l2c#S})8njgqe(gI^!Np8JkrACrUJ0D$)hxT8Lo*Yj67O1HoLpO3qo~pcX#R+QrNPrcBwOBhZT75Eo@rr+Y zci6uOI$ovt0r>JD>AL*(NVwb9zA2;Zq(2S3UIl+~QeRHf38vt(7_rzCYP>OM=SJme z6C{yTTc>1Sw<|z00cQL3e*W=TBiujL*$D`{sH|M;kZ|`QfxYF4%6d#tFz+l-;kY!^ z=Ubjr=t;IXK15jzv!f(h#*bN5oa$LG-c8dP1#$29NR6~awiH%Z9#3hhN^I!KJm(V; z#t2%*)-V;4%{=B%dy^km1PaXCR+90^Dfg6&(KoFn zT%c2dKyqAEExBGv;J&PPh!>9m1nrp8%4VJ@&m~PbhE8a<`D9rhE)Lclbh(F-{RTNp4Sgec530GvNUy@AK^H=hWq-rYdP5~oQ##}AUN67e;H2JT&yk=M;oWXz4 z2r#9S6Qnbqn~!tztq0i)`JQe=>BE)nEj{*ZF^IzJUJAAz9zDbva(uk%lr{&_<}lAM zDGa@*g|zFG?kZz`k2aV>(XOo$aReQosdC|W8?f4y$J4OmWUr+|AmP5b3&4Ptj_Jmu zDWjr2adR1UO`DXW6;5sK+_8xt53%W!YZXxEvyq>|maIMYnzu+h_#t)=!rgC1@9fx9Xi#@z$P~)} zc0gkps`64>%BODtt#Ek_?d%Ys{AG(xW|zxRZJv59z=e;FLQ)j%n!7DSax?L}UI|Z(wSC)C)GHVu=Qo zu1&hRmjI_s>At@&23&ne_0X+to3S*YnKbjHzyCnjuuc_zB*RIj342NB9$veL%_wg= zh-Hn~bPhhLIw3o=zau3sQ)sHe!5H7Bp>Tb5hFI*|p0RH^l)+xAuu5av0GSfD3C%+` zBJhsh+!=R-7#6-WWm>jJ>*v3P82d%!ruRX0B?J56Sx9ljd2CAy8`D_6$}9OS#bIB_ zMxT#>URX=5o;n>orBTs&R7UIT3#Ld6Q=t3J);2MfAPjw=e}H}|e3SGmTJ%5Dk3FAP zO%bpiFE6ZUa3j^B+tcQ=DXAQ5pjU9q-RiHRl6fFNVXfd!A=ycER0oUCTS2&h1g(VY z&It$gT}?&HYU3@J_(P;vWN0IiE{%jo(8vA7*s_PHfqTzQsnBaC=cKCY$J1SDyd{`e z61Xm=i>9C68*B;qZiqSp7d3uKt0kN0(6z3Y(X|hy+)Ov65L9^X52E)@Ss95IJjhX| z?XmL%iu)@adrqq3(A>CJ5dZl7O~xT^AaMF}VA{m8Zdz}C+o~idyWDeCG%u7R%3XW1 z_TBfft`FHq(%tG|#4WOsqZf-#5c{puUHFo%Reo`}hfoWO$e~x1>e@4#L(rbE?PHHR zf6gWe)_F(La!>H%V1s~h8rqK!FcBpJ<*=-jXqO6Y!Nj55V52P~FP-_EP}VJWRJalw z@IG)Q-RQHJkyskkf?KdAMn%P02$^1*&>eFMM9e>h`6aVwCZ^;S6;>u2jN?=sJ`_Q1n{uGn zq^vK4Z*E(^oxc~@XeVYIx^`<6XHHtw%kBBoHRX41=Yq&eoRHdaJ>w8LacWbXsLBYO z2!MDr#z4Bh;1QEnSl#=A?1HMr%)m*RurB(HP2Tb3<0i4Xa_nnw_eIK`Oigik<_$Gf zd@~{vkVs@I5=DZuK921}xCiY20%vNyKm+$F6-lJ`F+1Ub!4h#*){Rxd%}}?2Yk15b z^gDt!LekK(aJw2Le1TmIH_16pu-m>@<_R~w?EsUQyl;}lz2eC8($I7L>N?v!fk*e0 zTh})e8XSTs=nth<$r_t;_>637=XWQOTgpjDrFk60nKG!C_zVc}S+-U8L;*j*RJgZKnW2VqZ^O#x}U>QoT}$D#gcO^Ml^mq)26+{(%Gj zHBpEAfF7d*a_ZK~g)Z-hIcS|Q8A_e@b-_ioZr>=@&~w+|&w^8N_;1)CQNw0#cHVpo zFzgDLl43*kjHs8o!*XFvFk3#aj$r=X!QrH6T1wMLP92sMTbJ>II^7dQWIILp&PlyY z(-zpVM>oxS%uVT;8oGv=;yUA|GYwzSDP}hhnO1?opyLohLC=5#DGI2io1(#t?+_k0 zy7MLcs+zHoE4cpZ)d>lAioF%6Jzjv=I0IZx6oy&J;+R+LDAM(r0 zD6gGV-a?8s#GZHVCzNC313ZI$SDaJIY`q@!sH;EAf|n-Q-75(2rkFFH5x={K)q1e% z1EXrYTLi_L@3-Tu725sC4GjqdE#+lPy%uR9<%2PxJM``I8 zMR~vj?-80O$;ikAe8<*!R-A5N3PexD0j}{yb%jb@?^hwf2`wd!K4be?#ub8lUdE%V zt|ESo`^A0Ql6anYn02a%I&HCyaYJ1Njf-R|6*a4)kN!pz127Tvv8~zFz~mBtB)x$AIX)!E9Xqlg<+k|-grvJz#7w+svOwh#G{|HglGof9 z=4lnhKk+=m`FJxWx`?u13n?UI>|jTqh?r>1FUu}<&fiEAi20aAJC5DpE_A~SN4FoI zp)skYqe7ShH_Dq(eGX~1=%dp|+zGzg?i<6|klv0Ab-S4SeVY0tg=>KUPAOi(ROrhL z$+W0j4{lkAVE@ukb_Swx!zu9bSE)HE_*QpwR3YUgt%VmT#;usKwH#Wgs!%$AYs*mD z5vha2kHxaDK-TCz_-Rg21hR1EdM})Ex%rJV53JbA-n_|(xiAl@gu7c%23;9pJh2R0 z%Yj{6sNOTBlg->dJrobn22o3EEMti-X&h&2%<-oe%)l&?yY+MD{8Ge*(#i^p%5y`T zW2J$Dq-3QK?rS-Oxagxr$4m!SKq+z9EcCJaxm>Lirto}#UYHzt#`X&eHOjE-?JK=O zsb9Ep$7FZI=ctHG9R&r2t%q=*DM}|*KQQq9mNYabNIQYHnkkuES69+k(k87r;4Kf% z6Q#Ooi%EQIqNfh${Uz)4?n%Vzx%9E6UM8Nx_gtbV5#F{TMic&mx|tmglp=aS=JH=U zLJ>F~iTtiLQ%cM-91dv|4kwn%Gko&3C_0C*Ywv4}%k>F*mGiYe*e2^UPb)2w(LgL1 zu8-q~COM3Qd(@>Qk_I&e>CEdFGkM1scdB;wvz;omC8s?3ZkZn`dickDBlfEpZ?W*H zY8kYX*)Fhelw~j++V1wO|qbxL4b1PxEBo;@)8nuzuy^Do^v{ekB8rzLSOxy7(U#JnSMCvG4RyY~Z-pywm2PIc9!{EqVi-oe1DB-T>f z=q|HS$1yFuYM;hB>OEO3r_*xpU5u{A7Au3GzjTUsfFv$mJ6)e}$7tKPyWij;oIfkc z^0P{0i~Gm-pWS@M^npMIdq3<@nY8-nNF@Jkj=uMh#LCO)!&W2fP~mRNkVc%R)%>xr z(RhL1JEKDGhVO5!=Op_7Y_JhM7a#X5qKD{bC4U94MvTk$h4pVkgkuy>`TM-Ako}K> zp6FToPj9JzO_E=IXuyDb^jnwy7Q8C{XYh(ha`e9pBKdzGai7Tp7XZM z*BAZKmVJ4LRjQ2cpuea7FM5PU!{%o;^qO$>DRi)1jnr?A?6o4o|knj~gLGB+26u;vWw3aMnLT4BaQ|;+lOEf+A^dgOJfz{^U zSQk15!NMP{qk8TIXS-$#eSLWh^YM*X%Wbt_BY9HAoofX*5?2-U&vAHJKScqw=pBG; zUvna75n`Q}>`uAeU_s1x>l*N8??B~q^(RRfQLhRWY%Gr%%JPt625H=zA%l>r9ZysY zY6y}W?a7atoI$fnUb_>7OPYfy{$a8Gryy?n$a|@^$i8ovFZK-3I1YIHKjrt1=_j90 zN*w@yv044anhu_LR^2&zizzN>{s&+b*>TOIbm=-}y7(|)MNaewAm46#m4CS!6fUds zc!lH4hc}FHq24lT-)N9HdXbJ0d}D6=gQEQ#1<~iYpE2-x=eKl;GtB6p=7kYnTbCXt zHuC1v7YqYQFYg%|mX3|h(z@b(AusCDm_Y#Rs1C5aezO<`#v{TksfcNNAq6z z4vsxDo_L}7JHZxPgZEjMPE#!Wj3=Im%q@)w<<mxMMP-X z6gxH5)LAS-Z~34QUNE_U!OA(Zzs6K|Ot zAT9`jLz-jnsCAQSQ6g0dwe?+vMO*n>3>9GAc$wT5Uz8aatirE|RocfC=+^q>ls-EZ zet#GfpNA$u!Km9liBC(QCMxV;w|#Cljma-s7}AY)tHy?*w+a?*J+Y_MtdCO?0iN4L z&P9$Uvr)b8vz%MvO+*1UDmJ3~>@D4|yDvF0rU!o@)l%np5&;W7&nGq*tC3e|L=d-wY&cgx_OJ zxD`(l-yfByq0HKEg_-sHA4}0f1DFHaw+hiuA&mwJ>n}L}E7Y6#t0Uh#a7#rqSI?>C zJIlZoU*x6NMf-6Q-7Nev_6)5Bqln64KtnX*IV&zua?tt;wx%*BV##?qpKX?TW54q` zQe-B8!W^Ule(zGcx3wM1>iZ0V3BHM7b?9h~d9%;mjeGAKZstBbPI#lF3Ze;=hg0!%ttNIP1~l_>ONo*9 zO$9B9T~wCd10c*U9yKx8<&htC2Y@GWs-%xtmk!svMFmLu>|`@!<w=@{4opb-=T(89r955qa#v&lE^7T%~umm#eaJp!;J_;I~+rLiUOZd4rAiM2{Nk zfcHgsxL&NjmSVR@9v>*E$qDkv<#Q%3Y#O5or;Rbf#Ts6;a#aaTX-`NQ`J~;feOtRq z9#mozi}8AA%R~jc^1eTb)>2kxjEL@phq6~?;_2fVA$lNP=)u%TsgSWS?t)UcJIhLraqI91No+vW-8e5GK z6IJ4A(*?09fQ3@)9Y($m6E81$h7bC>f=(*=Z>V55sO|$W0O-pY3gg~$V4ogCCh68Y zFnT^!VxM~XKRwa1iD2diHefwTUK!gv+Q4Y_3Sn%l<3{67tICi#RZVIQEAron!UlSu z*w9k+5H}b|hR722qpgG| zh=0n`eR^M5$FBz%UBWxF`c{wNNh%$gAlIk8$vIXI8f`f|UdxplblxE+6vEWN0$W1= zjW&9QN=9~Su}@uE2d1C1H_mI)2OwMs$D&S4RrnWWWi|ej5A1(*f&5qWRD4Skxz?-n zuAn=9GbBel55bhxu>4Sh#>ImFp%?vj;x8A_CCMDOF(Se%5cUcVVNQ8L+{0}x(zb3_ zx4BHCqguC%=D^pw>VneFg*k=U{)W2hoCFY=xmg1v6?F#wNq_yxQX~$f^nv`0#{3)2 zV{No}K0o#dBPX$U69)Loc-Of>^O3V!gRb<$EkA!C^DRb&nd|r~Ll~NLk!gP1# zPF|I^Oqh!+fbmj7y=7Eo1T=LXdhi~Kgb|LrkIm$>JM9}GE?;LQb~(l-8L?Rv_=XWo zj&9&F(E=;p5X3Q@QY-fx?M7EEp)zu^3lVl`p?>V%`}LYAV(|c-9a#@3a+l0W6^qN_ z)2De(&cbMrOsyA3fWblklrFL=^0wiY-3L<%HwDPR(n zoqe6gc<;H2q8mQ3dpJG?AU$PFMb0{}c9wmcYY?KEc_@}KF6NWlb4 zB+MIk{|tOweOcY14L%f+jss;;2UXqTTG z8!I>hdGXQFrH!i<(-ZT3z_h(`iuI%O&9#U)VPmz}kj&j(XWwYTnsUe*(H=a`+R~GQ z&G@UUTM(4Xfp69I+DC~yp?WOKf9zU6hyChD?o)nFCQDvIyrX$!M0LcA1#q#7(ss89 zBA3P1cM|R3G&DxZRDSMZn~M5dE46%kx0TL=i7=Z`>?cS4o$nbKaMK6&wgEmR?Tta;!AI4B^jHyNu?8i#{NuDqIm;! z6}$N}!JpX*AgP~AluioJ)Azql{MRxvM<6ZL5hn>ZpTA|6iOY$?6@KWm)P zyhi~^4YUF;$b9;4B}{nnAX(K?Q_kOwAp1Q2XWTyo1!LvNb~~k_*R_8pnU*8fult$n zXPhCarpbRu>Ho5#JNN%D-PB_5+P!VihyMVLpQ=H@|Ah)rHtgy^_5zfMWxW=pMcUWR-$q2}NnMlFt-%+P0Rh12g&iY4(9XBZ=P-HS+WzX~yK zH=FKfN*fXpC~e>6K!zggG|cj|EoPhGViX{?Sy1ZA%4_`7GA-tZn{74T zjYi~S+Kbznd3FO&VBUh+Q!fVE%+XxaF%`_38L{4U3|u$z!Oy(;X9~0!9q^5kwNL{l z-XUs~PaBuNqSR`_^>6+%zL6idqr8>7r|ZopKBtv-RHPoz-e!Ea#L&?AaUc!kka~af zYMSoSmLLYaLDaiw=D1ne+ z=P()t=H{@D=gYMWI0R+I@Z_42qU2HGc=<<0lBo$ES1VWC`B&_JZ}oJ}&rK{`s}vTK zH(qt65?9*NdkT$$l;@3@rl4I0gDfs-^5~bSXMr1EPyqk>uhYRg;mLZY@;#R;s{4JH zY{!nzmPkL~rjH}LWDF%h@9plvrM@yU`L~zD$!5+dFRAIyLWDc+9p{f#{s4TK_{R54 zxr1F=^_goT+N8*}AX2_bgbN?-^1jwX*iSy-2LRsk1F#cwvG@akqg>x}i+vGqDK`DV z$FyIMGCADprpKBs3Kk+3N*veGZtH`^5{rmHnN8#T8{gu7Jrd$o&GbGwA4i@405E-W zru%yJhuKPL>x89({aE2wF&=+BtB?wq)z}oP{vGnhM&oYV%0Vhw;y;P~di){=`-9l8 zC$kla0a+tsW!uh3-8mP5#p~4>T(VIk`)&(|#v>r6fGz__OZ;5lwmZV-oO_X-7yNe z?+E9!OQsF8ljytGidhLJo7TMV5jwzc^l1(_evN(u=<8E7+P=9)R1<{8)q3a|Us+1l zp+YAMLHQ(f%h%>yCjjoZopPEgb)D$J<)JiByHE+UrC|1WCg3mkA@e)$_k=Tm zj1kohz%oF?q($I+Q(=Il+gR5D!4S)s_oru(&vF&klWhHuA9Ot!ZyDcu{c=8zLpaBh zUt?HOh2cce8b`ZQ z|5MiS^B>v@67N6fC~Edx9pa@+2gLN+w|c2HR^Q`7iHRl`4$e7&6&w$4U*6)vE13m9 z{$%$(n#jEs7q1(#GM{jMvXZ56gBTPS?AN~JOqLg;Mb-b}vFIZ!ujnul-K00i@A44P zg~disvDh@%3pvR!>ks&)Zim;Iz=4CB^s2~X@E*h?+KF0`akJEse7=Q)a}y*|rM7}SRC8UgCw#c29J_5@ z<(P&m-1N$B-aRijI8j~(&yEYGM7!Sbruzs3aDWOqYB26mK9e89z^uGn46JzJU4gv`-o{ zF7ATK5WBw36H3yI4x3z$DieYsot4ld5BDBAqp2FCgD&Zr!(unu49#vxfM}vm=b&p4 zvteX)2S4L2X_Klq0$Eh0+^jvNO=XzgWHvF*kLK+;PdZKQJE`T1*`iqq_(QgjmKE}!K1KmU8&W({ zZ7NleA8hHsG@xyxjUJ-s_uFV+P>OPLP6U52P_^_MtM+FdGd1pAEr#K^6$Ua3`o>l& z&Rh$kM+J+^>nF`^gIi#T5!X8H#uN@H_xa?y3_Y}Ud8G%9SXZm}-el|LprEtCv z2PvloH8~`=6ax=6+XX4+uB}4@W%w1=#cK961~nQ92#fYy>zON&%}Qb5`pdVqJ!+Iy zmX9`v^MCldDraM>j_O624dAn_9;#}T;;u*B%?nv;`QD3y`6fy+Y4g=RW5xFM@dauH z$D&Y1?(aD_#A9sDP}@ugv{@CQyM|ce=9bX$Q5m3x9pu_N)heH4h;;EgP`w$8vFo|E2E0b8mGmQvXd#ZA4WBKEm~!td0L3<_ve%tFfe5Q4`id8 zKLD59NxKg|<4EiAqOpLr2&Ip5Xv^;E-@U(6bvoQIGcS=m$DC1Gkn3NZhkw75(vh*YveMP!Fs7$5NLfZXA7Y5xvT-4QwQOmg0y%Nkja?bOl z=8%O3^2WRs926N)LcLghyR5%gm-2P8vIS{&IpHDS&0AtY{hn$Rpz{#C9}#75kQ{*^ z2VcVJK<`%ULFvE#*nxVgRGJ-19m&Jmm})&3lxl^lZV=;RPkG&x5Z#xw@AzaH*q!J9 zioVzu3L|L&4_b=&=Dr8OAFd6#*>Ts#^NO)rI&Th78hn{M-KfLIj(pFfyK0x~v8tdB zU4{!am-TRCS_H?DSd<#tWRXm3Yx5s>CnwxD{|K8Aut~>fl+b5CQdd_nm@}Xblc5Ij zSE&{9COzYMQYqud)hL8AWf~tL5$L3te!hokzEJPB!x6_>j<6ZHJFv96K|8L+;1U}g zH?^gabK;j6i!-h_ri;Vd(G7xu zayX_Pi*e1()U5WjlWw^&PbTUCDxboy?^*~tE# z)`!|3fZHRM7D~hF8egYEjD8)!v}xO?$W+)6M~o^d@(*d2-AOz2-fvn#+qxBOq-hM7 zb!C-<8jb`f_q(qR;6$9R7;RQ24X(EssC+1pgkp7iL6#dn#q&s&QQD*pFWpd~a7OD9 zQyifB^z)J{h6-C7UXs`2`!M1tGvfqj-J~DGKNwyTyg-bK+Jy3HPE1q>GJ)F9rKyYo zcq3AxtA$w_)emXyXEQ@O;-2pzusJM>vdP?CS*|aR_S|2}k~&c$4w=@KQ*U&<``N*A zNu(a29?GZ3lbBp{*n@wZR1bW6Ey@$3OT=mrY|zOkBb3n-Ba^}>O-nn~l-r8?*uliY z(HT11t){d}MygECoX9ec)VVC?q;u#$f4fWLnUG^ykaEW_UcbcczG%N$Kqugo3k?Yt zGv_`SraX#}z}Cm5BlG<#Y~!g}Ab@atx?iEVLg{U1?XmG5sc%?%cB^iRu)Y_RIc!;G zjXV`sq6!NFUp}_>wJ?NyF!e62uY!vOJ>1YhdcPp_wKxjCp=-;dFCMRFZ@WdQDyieH zB(NJUE{1=9)CYHirzp`wm8=QIly#C3 zeC!8;uSe)`N{V+na&Q|WOlJBfawgYtue*vhCb>3iWM}2p$$!~oAmaVZxZ2bUu z$CZC)$-S-Vzi{OQ@E^u13KhP!+L+)Ss-sB(qJFUeU^>J}^yMUN2pW@>t3iW-)^Xk* z8o(m`7Z{*_r znwU`d0SKhq|L`qNmG1KRS;w_(q+ z$;8CXENH^D3wI@4*Ntr)`3pKl$u3Q~f|N+P-D9a3|45h2;};9Mr22Mu>P9c5_t;v( z#tGbXG=*aQ0||0yS93-P2*#fsum`i%yqfVc*xFXg! zip}rB#seaM#ua2aF`RxUlxhi7WiOdIKiARI{$AUd8>S+Vbj(>Q&rNs#nR_e#T`*peyu$S^uw_{?(NDbB=PV zY5*5;hsFA~4Yh_Ze3Lnyxng1X4q^8d7>Ww<+XoM&dh8FAWds?i-JtuE0_MZElRdO! z(~`DpPha9Cj5-~fTdr-Xdu8}KlGXOcygW3_Q$W9hd~y{pTASTgLm$p#7(^W z;{06&MU9$!gPO~)*!;dx9=DV4!Ws4hFh_GmA!5Fm=d$Bn=;x0uqAikzEs{lF@zeMG z&ZXMD)(6mzw?3~lopaLEk!#%A#Fx5`&TvR`u&ZG$S%~(0*|7cb#9z1enVOiGpU)x< zlZ}4b31V+z7B)iX9q-oP`hZ(Y`SiHaX9ZT@U=_b6%qlB47W;>=_{!nw9}-{sLj%sm zKC>KHBm-AQ7@Pr5?n!u7_?tSn5@n+VlRn=YAy{q=_UETZl%I_Tjp{k;aA^-^q06;?g0l(J)asW(p zbPRMfObiSREG$fHToOE792{KAN5ljqv{dwTv{W=S3@p5l8JM`4X=vC*Ik@=+goTCa zAB#zW1SNTegasdzAYox);o{&@;NejSGSV;#{;z+(`v63k$V2E1C`e2IWFjOKBBbAg z02+jo$S6n;{{Hiago2EUhK_-W=+z_yASe_x6l^q9WCR8vc;H4QLVLuBPAs64ETd~p z!XzkU;~AP#)Ich$=e3K$9M&&4OU5EBa^XEs&dLVV_epEK6b1Vh&mr_tAq@Tb{nHj= zmJs^T&=Gz7L0HP^y?xwIgtAkBDo+p=OhhP+_5Y;FvY!a~7uE|@d7N9; zA4G(FI~f=93J*{LGrIjC<3c-%4bhE;8Rm{?`~M+1Q<^|pUO-n;vAOVE%?cLz*)vm4QC&|@|i!B#RZsg5+d z2OmeqsZVt;bcq?QVJy#$p+UJr8dV)L?0!x$fPBpEO;kR*?3I?qjS1J`*(y0(%-`E) z?iq!f{|2-PZ*O2k((+w&F0SMXjkhMad~g`ULlS(6HZfd?^qJi3_>vfQneiJ?P_45& zY%&?xp&!JjqavapRP9utuSJ<+MDfkR3HexeX71@3ny1N(8Ip(7I@vC+R&=FHVzZ*X4W?_;Q2QhsG)f0MLUOfEdzV;lPtvUzv~E>&Ps_GLN8`0?YZbPm#>Fi>2UO zz54i~;@x+3l;42W#MM}in$IVwiskc&*|HU-SHHM*F#GX@UW$J@a1HXaB_lF^QYlqB zSaz^xeDH1IRS^-PIU4t(#MGqy_f|@Jx8Vd<=F)7FKu0_KSvlx_SKAD4?0q%(hlt zxpGgBRB<+Cz~f{Fofz1TUCef8kADLSTh``|(L=p7!oGRy&5y5MfIeDzLfK?;X-efBhX)(`%}AbIMRC|V5{c#$dtJe9a6Us z6uVAntvyjYAsmg8*9|JT2?++`VpOh2?MC5yWH>8}BZoOMiAp@e?m-+S8=;sz{bWpV zz5OBU$QfDMCTM<&?pw2bSkgG1u*+E9p-xF;OdeaDNBp>SdUEcDLvZ~>6?WTf&MppbC zH{wT*czlDrObZ%eNkz&sU9er>_8wbjc8+byPvqlU{ge5CfV2>^`wq;?x1wQZ zzVPhN-l2@Xen3Q$4QlXG-o^`6nvJ(pvngf1hiFlM4>dp90h8jce#5%N^oXgNr!1;t zAd`sZVcK%ueL;HkemK{=^5b@Ip1{EFRd;1tP_E?*!^4F|2`Lp7B`uTDS7!W}>UdKS z;=4~`SDl~tQ$gHBd+q+o;iOVPCsTAXxnc%nItPqtjOILXjwjJo``dKYcP~W8zLe>I z703Kw9Cg-9=8-FQu?Gs$P}|m=`$ki4wjtBrvs5>2ovb@IaI#ytv9R1paWz9#S&HWy zt0K-H@B>^hB(G#~cm0Ip%C*|99H*+JxU@Pe-c>|q_z~Tl8B)Uxbz;mto-2tFI%%^A zNxMC6PdurwOb4t|7ljg-e)^EV;;l+`) z_Y+^ftsmVPu<*Sw#qpF%uc#g5cX_5-_3TCtg=2>>%S0=WHj>)gG)PMU%>XM(Epm2N zhLka`H{VrqenAhVG$LGSl;mAQ1aMx zgP2neIqLNRrU#zS222vb0}AB@mKCC_q5u9Q0|Hib?E0{2ot6d%LW z0dgYbGqg$_-b+QO@-iKAuMBOBf28Eip+D1XccLe@s;Df>PAIwn%YP`fZpz-ZA-5$6 zdWYIEjn8eG zo>cXQ#K$M0W&q{H+S@X+=d-g_(*{b5v@~Z-K&7V%jo`DYX8~%}-w1pJqTpFpuF$!Y z1}*N`C4I#^j}7NV3VkW*otFBZ=G|Al&uj2n=G#Vw)Kf3P)|#;FY)rd^B;{L94jjwE zj^xG!(X)<5SG_SNra88RuF=gr?U%yKt?tAyotlA(x z*YRh5KyIwXdg0Eb!4H^-n{oH|i#BS%x6(J_Qt{q2nBJbqm{O5UllBlzeO6M9=TF1Z z$<;WPxs)7*#>@>P3}>cjM1I6x!|b= zejRuL`RVXI@HPjxepapU-)aai=nLq6rj)nihGiJ!w&4rrxqq6~hzVGhV`jEXj6b)_ z7~PvgsU03^vFf@l`SQfU!|&7HsQk-z`5A>g>)PhaBrWqFy2%z5kn~ z<^OaE`hUkq@eZ*v^+qgFb@dygVv1!?b~2ml%@kT{%43`<<|tTtvX!qRqs&@FjVCC^ zIStrs+~{8a1^`DS-;4*BN&Wil-?PyKG~JHRkf0jU6pHF?xxRe*Ei_lDe-KZryAC?{ zk>VC)D;fuG5jzyndNx61gn|34{|Uoki{p?NkeCshG1X{%;lNWUI|&$ zFb!TU)NMZmC)%fTFu+Iw>^t{a^2e&VKRYnLI2R1XPnEn0d0{H>WRz_nM$MEFYF{X~F?-QrR&(CU%cm9uTP7z;7C_u*VIgG+V}L z*-ysF^64x{d!=p&>E7&N1ukpInX*F3=#@0|_{u&^W@+6zw#_=(Qj85l(7?^$`pM_k zp>%sUGz5qeuI>5ffg)AeYADlsmIuTKA^#jB>Pams2Afo{F@t zoKH1h1Vgqg(NtM+aIYxxh|=Y#^K$FkSGw{#ZmS&zAubHA#0FX{JiF8ftZ#N&l97lA z$<&iCl~MVcGyYJl-@)78om0)_+CKAT53W|5md!0QV&TY%ZvehM|#Nb{mL0B7H zleUr}osdc%pd>apWlgwUNl#63o&U4f`1-|DA3}10X4fT5~ z^u?=ewLz+OVv!>;>m(LF5 z;Bs+tp_XEmZ@vowR3!JtmL|2gZH8cvRWjMHF<*I^Ngo|< z)HKtQ)Q|_eI~|$GPB-|gdCxy?PM*;y~eTnNJ4uw(Yevf(jbZ`f!?r$(s?OY?{Ci0abF=BH!~y$AX?I{s#E^?G;R@ zQzvKe^y7aq{8*k~bW~fBoboP^*^(V>7IGm3mc3;+T!E0z}3rc_ddZPN&#XkgguY1<} zF-;`k6PLN_noGB8T>$lf7@K#_Oj_AmpxMTh(?k%mGWltJa$@vhBAGwS!>|S`g;2@#jZPY z+N*Y>YRVWJq6KL`LvX8Oth5YQlf;&L)q;FcYKpWmElZ;J*1o^k@Ik?&hKHg7HA%~& zkHT=f*+(h1TkE1^MFQZ(EVM%ik`+#W+W`RTUMkSKuj@sa9i}FJ?TW zH85r~wiIh)ot`B7pl@ZLXrgH+YA$xL~yTXWVAPbX&-Cp+Km0Kw@cV=Hx=#8BVbyWUMQ)7U{GdlBRNi7wtz68J@AQpgba|a_CL- zb3gky1*dCdWC$F^4GVNWeALgim)uP^LY91qBlgjy%E5JhVT!x<+TvS^QhOB<+HhS- zry*6eN{))A_wmi$#xKt8Aps;DYtls~wz#64Wg~pDmRlZqRkAyMrPqPLU*`Q{ZN}4x z+(*;>TgeOY)Qn#r+@Ka4kQOs&@Pe1y^#eDkR)%e(x;1w?KdyJr<+SrJuX1+#diGO%T!f z(EeBF-zfOMbGrVmYv#XEo!tI&-!>OT9{?~k<^N1kGL1avd%mgn2U!1&k>j6KAWPGLl%S7X^H=SiSmJ9yiHFV{Mt%+@a@U|KP zJWzj-KVYToZ|NM=v8#KfQ~GKk-CWCm&69a#Lpj`IJcfa*0Vcyd)*PaFY`?6b7^$Hzk_jXz5bjc@ z+Co%V6c~{LHLiQU`ThZB<&^J!mET`OoNiyVRh9Ph8)D%A<3MC&+oN{ax9lF0y&7z@ zBA?Lvs5p=;B>v?Pc$x3+{nrm&O7NfeJrMplqLt_%zbJB%Wu7k_!nZZ2ANVs5r+x!c zS{ZiTe}=zb4EXskiyB_P0lB{c7O>fi-++*}mMRJ_KjjhpQe75XUYH#PeP6_1-JpD# zan5BjyXI74VfincdS>sApDY(c=?T7_-XvQGJbtZ2eR(tv@4F+3RV~L{{dxBraFt1b zqQqQ#X<_>y2L2P1kc=P4_t(J0h!-KXF)#&E^*9*xTEi3fHHVw$^oVwq(I2iq;3K8r z6dCnN(5KU!lNUOh`YUBl_1kLq34f7#Hd)2+f4GAPBv(D*e-r(SdI+uShp=iUc~FGV zfrv~5FBDBk0e1foTYssOQJ=N$eTY2KxreBm`Fk(!-#Ul>-uZXMu7B_Rr=;Q^TPsE} zJ%7CUV+u&d^!AT4?xhcIzKW{;x1rhnll9%7d_b_i_>(M$2v#+H1nb1ZEZiXmq&xHv z>dhYxqZU%rhi1M10VVtIO39e2Ux+^B8VF12yb4->kbj7FkV^j%4V2$TTtAxw@ZBE- zA@Sy)qHq3)*8Nko?(|=xbpI}T4gO2VKSXDB{veg4k2WlnOt)%dXKnxM(pTHujHBAw zLw`=7lYK_=3y=A?OQc3kz0?jOf_%Kx%gdU$@+50XZ~d z;m|%Ls*!ZDPGR!$w@RDAe;-Sw&3_)+KPHq}1PZ*lY~+w9vU{+KSW-M1 z*FiPrM(}dT%m0xg{f9h){V%SVzf?XjDI_BlvZfZ}x+46EPk9i1a1*HZ$DfElN)dJy zQjJxO|7cZ683!9{H^y=_KG2ALJ_Jh+YZ9UvvG0el@K^I_7PG!GMu0b0-Y>|w5bmJrQjk?BJx z-YqnlT!{m^s>UyT9;#x8u%np?$l;iae0Ht#FP=T8C282vc}`{?I>25nfME#U!!0R; zzp2BuiW}dU=HY&lnw)xymky%q-)%o6@#?>Yqsb%<>9DBV=^zCcEsF*M$q_ZX(5pd< zo-V1E*0&Z^KiqmdscIBabf|aY>te_Xlt9;cL4Lf~LCj(MKBi(Cvg1(>`T532c||2X zanioS8NO%KJHBf4f(qBKe}anrxWelvqIBmCCb)IsI$c7FCL@v{vdt1|9&U2Gn-{e9 z<{GH&J?h${qeH^7eC!QWCOA)hBO@SSY{Yhr%!jiUp~HhnIyaR`2Eh8mp+d2g(ZM~z|% zXSZNo^N6X6aIWy#vhFKU-f8fRowDNxTi!w@9-{%_ql@+!%)D)gq02+5U z@zMk;zT&a^73wN!(Q0ps@?h5TvLur<;d7?LyB(g@n>ARRGvJrKd-7xZ zO`<^Cp_ozM6Yo0Y?do6oI&%h%fmIL&IHm0U8cb4PekAp96h~86<@`*I1Ccww0gfK$ z=@oT@mZ*!wvmtnrjg4jKoi2{TZ3YGMoy!afIIo&lXwj*aBKAObi+m-6Hr{LFNt^pP z<~CSvkuj~3r+j4-O%&+-9;)b_#Tg_O{mgk~n3;^cc#nm?o9)|r?~tVv)eC3VA<-&> zN_-#b_Ie^x*dFA8dH~3so_Qvx01Umlw%F2$hwrc4UOJ3Ly=OiVJu1$NOUW1g^}0`N zO$m|^QyW_nR*p8Zt@|w|U`YBh&K4^`kjTmmub0eVBwpcq?L4Wkfu~`((wVu&F8s;^Evy31_?Cza}u?%5&BE9BvoUrtm1oCMYvUxtbl-}#Tjd+H<#U&X|HEc6hz5t zw^GzV=BFgwlYDm(^jwqSo+^feixrYR^*MWIsi7Nf4A=pj1%?%fKHU7o;kJW&q@)xP zBwpHDV4Hxr_r%3K?Z7K##$_7v^#1xuX3DkO`VWUSF=0iANxibO48GkPOY;fYtf^NY zdgIl83dLWy%bmJ7*flAC=7T_;n8&{f$0p3chg@{eLQu_L>FO^Vwl+~XrzFodlz}o; zh(7aGK5kuVG$Xl)>J9TFz_s;o!E&iqdjGV$|U1xqDW&yUp=S<#Y^$$x3W3f zZ`vGnbi$HXM-&tV5XoCy9DKR3GzY}4C`>WgrBk8!2BV}*-9QRjx#4K$k@kxHV3cuv zla*OV8#zm(O1=or^k4LBm{sn>5)?R_fBBeW5n}yatTA)?i3&{H2K1WlJ`&FLIKCry zX3o1l)VgBF*I*~W&QcLxZ^qe%Hpt;|nmsFjrkarTONYdC!0bnz2g4%_#uv{j&eEq< z^r9PKZy%-IO77^lI@Dts7jQKa^@YEWG1e+zx=$Aho5^ae3rDxD9Q4*1v<~)6Cm&^JRt*CncScr16JcG>HO#mw{=bC>7()Wm2I`~HM}&DE}U1pLN@v>Ut1CeIBZ{C zVj68;!*u+bx2MR?IXv|G*exd9o$A+sRv0+ZwP9vrDUO>kX|>nK(i`S(tUlF#T<8NI zoFQ~SbgLd&v(=F)-Ul|BoTHjGF8|ojoxN(dR2yv7Lw&m*Y?6?WNJ^wDQZhd~Jb)A5 z4!Wy>wp_L6RI{vdW1y6*DMKjrbw6-eSHQG{BsBtO@R>zEVNntF15JYa$-9PH`S9U4 zMmGEtQ48&TV+v;KcB#{zd+^jF`*!oq>M84Wkry7 z7Ky(B7$fggV}Ap-z}vrw^g^we-Z`U_8*v72tgg%VON`Nm4R0UzvG(mUeKoajoH^U~ zI4KlSx3{HJCslwc$10;A!iK-$%(@OwT9Hu8G+k#_pK=Ote#p^l7Z6xDhlDaFV(7Z! zck|ixVh2w>xN3`3Tnx7lUP;WbN%TwrF+uhd<_@Kut`c$q=2UM_B8UYuMlGXsrB#RL zK{j~y8_UT4p|zs!C`VoTi0l_qxJGw%K#R3reBXC^NvyA7jK^A)-M38sj`xMAsV8v1 zxc@tK=+ZmF@&O2BTkqBM=xKLRJZ@U)9nF!cj_vg62FZnTZ+NV$1CrY90a)KC_LSdH zV3GQ!i6zdvMh;rrcRbP$edll?Ra#O&_a>|-Y4QZc3ceE+1FSdh8eQ{#^wDzact|51 z;mQ2!8m53oabaPF>-(5uYw8FV3`k+iuU>LX&7szi92dLnVssk}ba#?-d#o_tK$(k~0HL zF0Kn5OEb@GRl|jzJL-co9;EfAyXXcKVAx4Bk0dsqtzJ`?Sa@jUzEPg5QB&o&Iv)WH zIh$qaAc2IjS(PA|FTVmeQhwKc&I^_++M*}GfKX#{WoR0z|VA^w}^@`yG0t4|Oa z#%^Um@<_}I{oDr_&5|0cH&^`QRH|5|gZ@=-xAWj1!czl?Ih3}G-CDNp;%Nl65}u~- zkv3-TRcDhoipm=uy-puq7CkOaH<}*VZE*c~nTh9i zAJ&=(h+TZ4b#W#^xF#w=8a+3sJ3pp-gJaJa_RIJClg$jXv%2?SQEbNBH=LK1G|n6= z%+0QRO=eeaWF7Tt^wJ7vFD})^&PK}Z%_d8WYZ4ODRd8Itb=;|hjMRZ-Z<=)wz702kk^y#r~o_7NbX|zbmFXiZjTlXN1A*lWAa7m&)-Yx z2pg?WO+IbI1694aa1FYyc=7X4<2PXU(MX=cY;C{G#o7mFE_rBhYwMALRuS+=g7__; zr|D1O&;?{wlxP_t7Y5kZ%Z6ed=Efrk(&RdEkQgt3=#}#EgPxOW@FDrk{t7oxqez(7k4wu%4)mtN&NbgK+%>s7H{sZ$?Df6BmR{7J z=H}in6~)nW^$*6hYWcqb6aF- z{`Ui?7x$8?FI9T%L^S+wwx{ZxQ7xPO>af3=vNi|@$-w%B;Utf=xZZ3lP*bAuIYvsKUj`>C`Dlk$oE{ ze7)Ps&q^&k6#ZxM*5UksA5~a2MtYTV19oMWaDy`cjleIBq!se|+@l{pXBI?#B7-O% zcyA$(QRel}ry2EW!LkW3iYoRjnXVX7z-ac~ZpQR}ZA)hp!TNX=rW3(#X7E=ZA`0iK zGRQ<0woXVXE)x}-6}~Y7 ziNMWy@ls-;msITRw)P^#+wydy2f=O~@kgKH`#{3Y-{0<44YfXdjZzZY4r2rj9+yL( zmclZ{;_$EKO8Cft`wlsL(lcf|OJOQNfM2z?6nBu#sibO56K=mpC$qA}Q6p$$@5@no z?lkYDV%fkAjvm??nKfbN!2mlKls#K?dKqK*55 zPJ}Ut`drdoO6R<1cuKP@N7p~(8pNXp1E$`&QZfZO4GAZTHE!wBUF`4W5=fsHz73fR zkv!&Kb{|ssa@X-T$TY)d?d@7s_Nmfb45xU2rTO6}!e}Wsv+}vXnajq{OFOr0v<)rA zVwPt2!RAR?dIz zQ!eNISQduXB}x;;Z0|+G@Lkf(Ph9BKGIU>xZ5_f~0edP^FVR-1C13{U_H^%eS0Y{w zW>#^rIpO+*9Yn1bwWgDcK2GsqFJI?2!F8!7_KC~;DyukwyBa6|Bx)jrrsd28G%c!^ zpryg1#$;Q0lBGU1!$8&+D*D*mu%sVoi%spR6k!uiiu*c_6wHJnx$*(Z!PsfZ-*e|z z2L{p6M(sE1SbqhjHnhC;#DzCacswT)XcYhT#e_K2ZXIGSnKF4Q&M3JORDWlW_k5cY zU>0|SkJL#BSk~6hwAcXeHfIJq;uTgE;IquWm#{D=WIHgf1AHHy_2)m29H?l8nc$kZ zjD=r}7z~W)X6DYOo?dpN)j4BKEzr%8r^B5*s5Z=IdV5=vCTGT8WIu(yTK?}%6-s< zy0Q|*qJrc)E?R2Z+ouUkU+y(ouSA%3MrlN$?<5QmI59pWIJqG@yiJ1!U0g*eMCx|B zvl6+BHWE3A^2C4bxP6RbNw6-)Kkg97O8^@&rqP0Nwl?gZ+qQ3D*n2L|c|6`~_Iu&X z2@F`mbar;Jt%>v&_T}-;KC(D)QX)OQghrLQt=PC%5Nq#T>V_~G(-;I9z1W9nzC3Eb zXxyOY3AL4w3hg5Bsn>xmg27-FoZ!NS2=kJ78kreC4i6B>VSdQ4*+_m-NN;DwkMvlE zL_me1)yiyZnH*%P>)LLr5+^~ohCL}Cwxs$Sa9;mQ5syk7(3|+uI+Oc0E;xJU_XP+Ty!Oug=fG*H=ey{@(!Z$~VG#OZFlDBN@EmK!Y5= z(Ao^ea9e(-(*BBunDJfnnD)%>wA#(#ja5b0wG`-TzbF5BmK$s9>z(B34?8etle?&{ z2+!BvZu3@eksq&fD;C$8WWZJFd1-GUFCn#^Ep?Kg)kY%2I&<6_C^$}>E!C8yXh_Q9 zFO)~@vjyFc1DAVliR_o)exj%&C54p(RhLL|tp3Rht%dx`b0%iX!iehCzy!#T7QZyYU{Xi*| zeSWa#1<#FP>FK3u9+|8$rFA^qk1ZAYei|09?wvJDN{OQ#YvSM?T(o1g^q-* zsLyk;*H20(@OuqgW%5Fm1rg9yEuM8EX))kn9~3;!R8k^CRMYdnZ9tlsWBd zLqp^I;JfmL$svz3>8L&J-l`IKZ&iFkj3PkuGSMLyNldfS1LX7FQG^yLCIcH@RO2g9 z;%n#G@z%!<;umQ*b#C%82WGn%U_c+Gqu0tbepIh~+?*lG+=Sb?p2%-#OR{=KdyD75 z5xTI{(M*aLR+I*Hnxt`EZsW5bL2iK8(~#limkeE=RxK%+N)(4S$2*TwzsWz_Q235s z**9$7^kw`$yXpr9cBQYj+x=N`>9jirhGUmz*lr2Rn74pb@!KJRqkX}SAQo$J22#QCN_6ff>JFgBuH#}sSaQ3+} zif*gvPn8kVXtyWK19Z>v56!y22M6Qpkj$Ket#f(_*i-pb6fDy_N84er;UV3KZi z$zMsEV7fzkXf$lVg|XZu(_@-8H*@Il-o(gWfSGWP8XF}})u61yu;>igFJN3qR%Jt~Cg3L7qSC|Jmn2Q`J)>Lp)ABm^?qXc&# z5!fnO?=^dFc7$PFjMf(^8KOxMpSakPe6|;^4eYQr`6xaB*Q;tiYbA(sy}?mXpp_HA zu8}pd*5g7^8CP^Y-?!qk9;D+^l}hRJ6x@$6FkDON#a3<5T|z!mVS9RG3Ts2$#tzzC zSxPH|GyRBQv+}1Z9HO4X`+m~Pyq<{U*1%%h$Ux36R#mNVu>q!#w<^-fQ@=Vp^|M5( zs2aZb!B$T6JcN>XYgU^wU5~ov0zkgo@9Fm%IUKdBurT_|nAyF93V4~wLHVZ~M!RXE zNJY_1&V6J&_5w?A2TExTk%0m>7OM$9M!KKIWX8p(Tk9E)AHP z93%_PQx#kKgQNR7Fsmg?P|Ix^h|UXIKje^ul_RWPfWyl_zJyp!`q`L!+rH)E8PDs; zH=&9V52nW{i>Yz|gV7bgxwAl;ET%mvRp{bthx|SQA3ItNb5&%SN5iZ=0yTURAb4vg zn64qqxgz4cAq%rsGiQoO`VsOMW2FVcO~bQ7HHwMe0V`WuSbcQ`5cUSj%8Ru#@mT!+ zvMQ)TkhAIIZ_4!T>51s8fgc1!tut>2>+2adrtLz@KH)*H(SIm3FFt=10}CfvIYU8# zZyTx#h)%K0ktup(=Hg6DHBm!SQ9hg12-))U6h5a_BsPq~W;WsAG}We{WG|%c<>*{w>9J*>f*E7!SmL7b`D|pfJi)$SkLLRQS^Ib;r zQj3=npK0aQQ&-o85fAPe0JJs;;me06+ zi5x6OIjft|9+I058#bgbqCmPjdQ%qGK3B5KM|{z%*}MKpc(*I2{pR{@i%pG1v(_um zMa+%e(-!4N9CL-kZE&KX=d~|8*GCYA%M$qI$gZ#Nr^^2F1hFACgM1Ufp5WRB>c`Gun z9Dlva!@MMfSjI4NLQcb60esYR0d@dL^+uzW(1%^8oH<}b$e6P68AmJfk+8w;MfY9`rMnHXT8S=geimg*5Hyn9FYGlSiIdmv7-FV%mZUWpkF|Fy(o`Lpj z_%l9b9vyDRit^<`_kX3;t&g8(9e+8c;;b@3e9&-I>JeZY5fU4UKS?7N*0s1juWYG- zQ$ZW0r+V6c5;aKG-%T(NLKL?k9)4BhlGJv^{Wt0C8fKr0eFU^b)YT{ZyPFD#8hGC2 z6&;YY)A2b0Q_7vnN`kK!n$JrkWJr9@c$}=naUL05cR`-)E|M$+7|_az=uy(DU(a@w zDK^If=Ju8@mf-rU^z)hP=F$DE>K)Yjc)T>1*1dwp{nJM%cvCg44SXj(q{14(x;Fe# z)r{pyihv<%1roLtkg6clGrYd-1kL) z0ebjcU%~G6jP>W3#&mIGtI?Y#W~oQq7cz9;19(5S$~hF*))9hjynYdzJA|ZVmrdZa0GwdG(6Nx!asJL26W7Q1 zrTJC4dSGzhh{&9xgy7LA);WVq_xL?c4)Mzw9eqO^(tyO*^E0~oMCh`|;*KD)*jSSt z)BPdn{BMBTNm+3Or2rWiJf95%6VdH)#kM4pPioY933nLmVFwTS^3!+)BlmpehvS~7 zlgkL}p`=}QMa_hPJkg-82^bp96cZJv<-t%_zT}$tDT1tlrTABaxWhlYGir-hYx4_p z=n-{05{Wnv7+qg+Ne#ahI5+skV?IWGK+;XDS=Sc4AlxgbqNFBEx58A0KLZMLGuu;{ zk2%@xdTtmA4AX-1pJty)OHGNGodpIle@dc3npvSXLt85RRWNGav4&7~`k6K(<93lA>zb7@7Kj0dq`Q|JG`->1X{V|Uw zkv63kO{QM%2OJ~ct|ppgw)-!QCqnLQJ`Md^N=gy)y)9|pS{l>`Ny_+jR|ORdch#kl zt`1nSjh&4sF&fmIo0w%KM7>577-ct?9zADNAije46tH5WY%@7J6P%pX9J<-nPns;x z=_%LF1alm#3*jyocU)Bvri%EL=Opz2qqDn%tq@4)8#nTTs`c?oV}UX_|Gn60somo1 zr^`nb-sYJo{)97{B{=4R!Ujb=9~{?dWQ&8n4B@J@aV@nKP38_O+mlz#x0}Y{=MkAa zQqsqm@B(ACoUxCz)H%#Tp1p>E-vHdc)xZ+f&^g4{61nu$v1G53)%2)yZ$6u-c@rv} zVIFk~Q(LkENj9Y0a0&;D6a2xlF4=pw$s1bPyP0$ffrF~FRkh`HRfVN*i1cJ=Mbu8S z+G8Joqj9gGGBW7{2%FxPZ7lBbFGbbgQ;Emiz?K&JmK}4wu5#sMS__eU50g1Ds%qD( zVwT1IcPWhOvTE!VEKa6_YoRsVW^!%-)R$OCWJ1t*3+@fYgkEtnxfpP(bD1~GFZj!G z9IwtBl=?_M-e_&9RcHSYgxj$egc>QSEk`9s-EjMq|<;cql)8D<=1(Rqmsjo$EVg?2spyq5&1F0^| zT5WxGcJTUFRwD3fNQ-X7zGZo>eQT-R>8MDRg8FUi8|W=JTO0Z4Q^YZGj;FUABA#7D z{>I%BXz4XwkZ6tQy{|`{22(M5cfzYCI}{s5ZSHwRS4z}xK}DPxi<&FDggSeq?r-5A zxm-N1w6Z0ojjBomo#;0_SzqGM&DTb<9@wvoaXPK7b<^Wk)!89ur8NzjUG#DD&UumJ zj}ZAD5pm-9Ty}ev(^rKRS0*)F%s1v0Gd$(7s%JYm?R;^U+pzn3UT+uVE25wvnxdtT zL$fT+Yb@7@9HdNcVgD%76m{gdZwO*jR#uVFh@vIz9cfg5*#OCwxvecbhcQjM;|x)f z3bDc&?SwDUyZqA%;}szr8{695_pAhZY#R%mAVvqGh#?J9<7ywgSD)8Ds%A;ab-`l_#%N#`EbHV({$vjtyph#s&!&27@~P72b~B%ow0JA&NnW%Z zeojl6q`}F3N`BVT{+1<1`|Be03xj)?+Y^r(V2Z2abag)2JBck!#(__aoe2O_-8A|I z?6I9ez(D=VX#+bhX_|)Z)i@7iT_pQCT(>)h_33rpx~rl_aRy~de*6~=t%G^J%&{1w z#+N2CqD$}=E59tPocpIF~}^6?(Hm)l$+f&$#-da{WsxRYsn zvBDsHsU!8)!&aq(N%m}anMTG~rL856l(_XRK~VugHs;CZ)16CdV@lR6Wk0K3?_-OP zW5r+sHL{gn&MNq=GzI^Cdk*dbZdBuAk+O49XZ%U5>$kp@&D1J-&6;nBb?4@ll<@Z` zPO^_$YI^z>@^NCKpGL)mF)<{$8YtXz|A+GRU)5}722bq&UfBNAWAbns9#IZQ)X-T; zn6MPEwMpVtBNYBy4g9~6x7Tb2Y?VhbeZ7CyrJ{ea=FMH3`O!aijx{G*kRYd6PwolxFeO?ri{ zdaU2R#gEZGH-OS)rlIm1S}*965)LpVw0s|)q>`dO41h#YnIUe95Ph31{Rmy_-+;&& zg@c0vju@IIwUCj{`#LX9@%L-kl^4%~1?$*3cgg~M2r_98G|sI&7!D*mjotO3Y4T@q zd5{CW3LDjqv)#+7Y<}A8EuRQNEq=os`Jxl9o9W`-=xax8I74|#jsEzkZNQd_m!^${ zM*KPC1IXB`A=sgfx>iKL@rc1b0T@>k9UgA9h39nmYgtNF`@>!){8CaOtzmX{_p_}D z)i`FG23j{Q8jNB|Skz%3n9=ckmSBq^11wQ?dT)#h=;6S7_>$Z#!qp3Zcp zgk2rR>Ef`fF`X{`oRM@j!{)6w8^3;o|6(vCPhnpC6QKFPCJotx- zW1pb*3Dm?5y5%N`O~-(o^~Q;~Xt}`592mW!8Y_?RoW9gsUvLd?Rs?l!U2n$u(c7{$ z>@`XBV5S@v9W5b;6azEmJ+yH5Ap7&34&{pL>m1+c`F<7D`gwf?R6)IV&1GU?Em7_l zc@-F;WQ-P1Dnu@_DLx=-R_Q5G%A+SMBOPyThjN!QKpB<=qz5{xp&wU&%G8v8!TiAA zEuNAObDTouc1@tnAAo0OY<$==h@-Ygtn;nXaAB$MFycluam*c2UT94(C)#J^%NJeu zYLPVIfC* z%XaQ@P6jlyQ>ClSL43EsLkZ^}$EM(L!86!n__E5%wCu^-K_(7(ienP#dZ`Yiv@Ip% zA2Z{M{qjg|#A<2PSKM^3`wN%0L_qJ%NQ@qZO~1$5qdU>Y2`x&#pl+HyHPf?Lw1^-z zV>;4E>!}31DKwp`=%I3fK!(*d#GQU3AzJyX+j?hq`2wyMHvHpmddZ?7$N$CNTR_Ft zY;D32f+V;*A-Fc~5(w_@?(Qy0aHk0bZQLz5jfddcxVyVUf`#OrCa>h)nR{pM_su{5 z+*vbgv6?#PRGq5YRlCmKyY_wxbUt4o=(;dBra@8P0?iI zZzI09RYX?TA?M_E6AZ;`?oNSwgq4}y()vbfzDT4bikFau<1P-?kZ{XB_vepBFV0dC z5-GB);ta0eb*bKd62oXm-F-fAejmdhj!_(>CL~}BIy(`(j;X{a=`?(U74F(-d+~hn zxvvyEh^ABXeqas72gxt?)^L^-#N4t{pIqIZV|=Tx9aCkWFx!B2K(hN@6t)Lku%rsp za)p!xH@zv~qGlv#fZ`1W&PX~lPg3h4GoqYdUzfDPfbU1vQ!V`Fp{Amx!)%BjK8EV2 z%ScWkvXHTg{^fKI&7rZ`M_o2>KOL+>RJz7JW_(tZ6k}X$Ps0jnLpGcweQW-~qe2Tz z5NkQvA1Bx|)RfgPIm-cY)YI1fP?TA81Z9-T%~lHl17I7;`12cnu#IWFrC!(+f`m4h%xdUI_x-%iW5bWQjfje(?YH|c zWM@@Tw?7PGQ}~4}oPoFl{ovj~v=9k;3j5eV5J^+FY1zH`>nH9nV2$fl=zOCp8cWK9 zq~t<3`Z1Gfx|w|<(nu!kh_Mp|ME77l92{3?e#%4Me4PKE0ux-`2*3jbgX ziIhH>IMJkh4IxpB&~`2!+9yp+IN4|o2G@{+^j^0xFD;ho*eDW*nS5dGRN0Xv09`Iv z)Zlxp_}S$=)$!q!4YlWQklF-PzHFW1zMs8v^1E7Z^$uIqGIid3KQl2Q4UsfazDU~g zAa_*W)p=+|?YgqApscG~QZTAhQy&&>p~=HW?x~}p8X8ka9MYJl*vS+9DpyQB{Whc12dq`XDLJLq( zs$1jIO}gWv=E@gs??09P1~VWX#=mkz4I9e`YYk&9Go@$;%+)2UEq1b9dm692QP2DK zx-cn02FAy)$fn({xnJ(uS$p;b@|E{3r3@No*d&VV=*I-S&zx}xccIH-Z#wi;+sf#b zg>VfQw{YE4B?Jn9uet`@So1!gWB6%Em+Xc&FFJN5v8rC!rgY+MkJebzl4WqIfM&|-_XU=~qJYQ{~2 zaEG3MC1rJd-W6RETyF4YY*(2PRU%GOjSOMz`E>1i@IVlu>_w|~IeKW5?% zDUbQ>v6$cDUENwg`z2K73;2<%5f+Jn9Zs}H&FNU~W)5Io?10VcSZWx~1k7!+pCG)5 zbv#(o5W{g1C+dD5hKwBKCq*jN>5R#MSdzcQCc{@k{3;|ZjjuFtqZSS)ykBUUr6NO` zG$Vx;v#+4%AjC~orNQChLwgx-?J#4Oqgd?MC-=zKP#pv*qJ-`-w4&ti{PW?TLjxxM zF5>c^V2=IMILE(x0LH}mTjVU|p8}vPojBBe0+%iq6DtZQ=drd&EQH^kFLE*IaxrA* zgI*>Ib=|B#pVf=`YJbD{1kzu|2o*18=MspG0 zL(fsu3N__K9nliiP+WEG+65Lco#1P#U(f!TA78El<%-Oz%u;YnBXn|~K`O9VVL~CZ3%9X87Jl*l1&HO7gz}wurAMt5vvH&G)S1VH zzHfXY!B+=P(}`NOL|2<>w9q$B9k&Y>-WJ=J-nZg!OWfviI;$CU&KZuqu#z>$C)-}n z=a38AC?N!iqQXUTGmftK#^B@SrC6|`W>#gWo)tML2+4u;JIP@zoUN&zAY!1Vv>SQ` zSi-4DZqCiBOmj;{OJ76gST86>Y@WW+GBgOR6cAMlnH*gVMtzYKS&OkiY!|>K=>wmfhZHr5m~m?z_Jc6Q-4|>j#nk zlm?z;M7*6(u}%6_9zhNjI0E97y}!YH<;0k2w`HhC^Yw$`;YBB7csN;^o`Y+-W$(jl z=Ijbx5L2nj@pq$C%-mb@skB!B!T%rq=;foK6}UsnLND3b7&jdT?Hmz_b?;*VB|*Tl z!Z?4N%Yj*6iCC(qIC;x&vGx1AI^k9i^07GC3ik8QTh?e3%-}kJqQoFwwg^F4}CE*X=!?{{};APU*)OoF^O_R}2l~rsJBB z_^RSgpze!ull=ue+w&t>Xzx3-2NiTbSnuN7$7IaxwlwMYRBtDT#W(3+zkS!`mQX-83F4N(mH_o}a-uswkFqw+%?nGwOs3N& zm{^BT!D=NcEv_nyP0t(fBGlbYZxx83xS0xixQX7}+uD1plnZ3r*gP9J-%W0Mf>eVD z_CJF15lGg?g^A%M&P33ivGJ9mfHY}%H`sUnsPF^+$3jnoWwyTEc{?x5PxtVC+iG1G z9Otf0`ZY0|g?D%OQr7f3VsUe7eN25ra#G|TaEHx}M!9`vbdhm9W2vRZ`3odO!NjFi zz}6uL1u1GZFD!7%uoLG><WXdcrGAAV4@Bo*;~7=>C|kR{k&;ry*@Ib+g&g6t)DD;tV-ZR^7yE5S-;>?dV#`&xS%hE9MsO= z0#UdCUrLb#(V5wN8nq0PXYSzcNcM=a53yrI&fy_5{7KdlWeZxIW#) zd8H;5=_4VJO<)n{s$=IF?B?7u-^UU*^G?mAcca2=U$7`kjr(Ma&e+lc+38Nnbn`^W zLg?+(CYO6cq|I*TSnNiDKK4L>Ml^9#7_yjLjkYEmX6a?wpn2?xdn}Wqm|b8Z>6^nA zc?6(A2Em9T-Jy3|@t_<$n-BVM`lqhpd?8KT2zzeiP!dy6R#~!*d0K5R8!8$P5jX^^ zfv!fMJ=&70yQIdJMt)gsh?1*5E|H%Vl5^h(BZEKLH{>e#<3*qV-XgaN4lYwtm5-e* zvi)+t>ZZVx&xi|=F3kDqPfM}~ZB197$oqsY_yH*sR)ev-S^1e1efbCMpboIa*smHH zVWKDf!eYTI#_(sxtrretV}sx?#NrQ5Q)jh{w7qGdcsrP7?5 zRG(=cDGrj6^WY;=NILB*7gs;S5L)`SZKo?Uo+SjxK;_g6eqOPnF1=5-MvKY<-8!gQ zFDc0{EkNbMki+oG&nz(D#ifWYH3N|$FQt9TyOw>bC#={PdzJ0`wu`sv@}|Oy7at2g z^b5&*TrS~)w>u6q8qj~+L=h2>T7`bzKap(yPDA-a*+GV){Hj7V52$MLFM5FoG&t>Y zTvu4s+!21$=y7M|Zy4X2##JR|hkl^RUf+MmUp~skb%^Y|ql(;=UWGsQc0PHEg}>MP zWd*o+TV_MiZMTC`8iCMHIhw#zZIr#@{qDv3PNGy~+=q#EE6jU~qcymKil`zTST?Q^ zGuGL#NN zRsgx~2h}A|$8WYWW{}EDARBJy0KoC+V7w}yLrEarz2!(Ih`aN>h7)a@D8$`IWEGXk z(aCWIFVtN99AZ|SB@^sOve!pg6`tY9Pu&5XRnuM0E|HRVMocw*{bBf0Xjjage3>Of zE4=y6QiI4=<{ceoED*gtYZ7mxg}p_bCX&{|IY)&oGY0Zvt_!DZ^4ODB858g+oW4Rh zs)+vKq4YrrmMBMIlqEjbsHIpgd9?oJZaf>M=79%NNqXTl#j_TJ*W~~C1X_NXHE~Fk zH+8A`C}B|>Gli5V%EBp;(5mo3H)ARNK_;%@#qn6SI|bD6(g18?7N+>IGbBOhncqW4 zV(LDUB4883-$O>Q?#>M$tYqmAkE-=IO}4Rc=ED39#*>g>S909HZ-?KoJ9V>(4%D%~ znWPS;p;Vl+!e5m(#SAwJ2R(yZWoo`rj_Md??>IMqTJMQA<6O?-V&BcF_>h13t(n!+Hw|cKDtP!=?Q+`0ae|lHVkyO z+ogW_D=n;ZHO&#tJd^8Wqttp=Bu9mFCaKTZW~?%5rAf&h7ME+y@wr=rTR0a;137oh zaV4%SBZMlW8TH!miI!JaTIlQ1geH$_51E9jx}UmA-|qT>?nrT?BRs-EFS?gSwWlVO z`I@p5+co*&b{+E|L>z%GUeT8|hv{e%lZU8vBn9#FiqRhr365Tb_AB8y3m1m-p>a34 z;9o@^KZgJ{CXZ@vnP{rThaeY@buE&PKm`Y@cW!idt=^0_)zTDIY?|`=Jhc+U1S4d+ z02v-a$^6jiMa1V&R`W!gHThVYP{}QO{frpXjqB->k^tAYH^km$E({E|ghnVDK#=t| zlb^f}LT1kL+I~gH#D9`o4!YGu@o;YSnB_fG;XXNsR-%!{BJz^EO=%2^r;;)O8nRMl zhf-d7UQTLL*h7Q*Ovr;PLvU*$QfQugJ~*87ynk+Ruz^+J>4-C?%fkf&K+Rmdf)8$kj)A?DfeN;n9aVLVAkPSqC7zc?c@+idl3uJ(FGgKs+s*;S z9;Hqjh6>j6;CTcJ|{tmfyQmUQDf4vn2ltF96Ln*UgcL7lmk;4KF(lPY^& zjC#uOr`B5StZ}i_v%;vE2hED=R2_12iAqb63FA59_z~-$&9{SZT#23)mM&n4^CN_> zMbaiys3@=S@^=Imlq6_x#-6sC!hgggdg64$km-Xn7q6LP;oj13D8aR@Lik5FaQ+WU zJbUwVkxi<-Z}1ztSN~|S*g$-kcJZxn8u=9ZuVgM>4;Y^`CmHvm*P4K`s4^*rgh0_) zsAI*A>v0I>Yhv<>m`yGDJq|rn2+K{(!IwLNVobviIRF(N9!Anf3aOE>_5TKwnrCU% zpe6V5=g8X-E6=H`LHE@xFa-z3IEi_qvA<|dn@%Z6p)$gm#l}ahST=GT%goGv0&n

I4{s<1fCM#XMlQR+y$xlOtZKuh)0Q2hO0Uahj*!xwB-z`#BZ-?Fr-YF_&H}85YE%l z*?hi$X-v7?Y>9~9`gr*Z0t)9pnT3$~D{$^!;Jxm{dk`VT3u}wA8z@)iS&G)+wvt`x z*5BQBb`UfvFuZSMu6nya$(SBC*EyM6-=Lsx z{ZOPp(+Kv*XQhutH0gqw|Mxv$kS2cu)hT@bEgSdGDHCKtJyD&YSRB`>TJd3qWA)u+ zsTp9VdXQuwEphSYTj|>1>OIq|O{@OXTt|M1^gDqy@yA;X6DpQJ6mKSYF1^~LpG?9~ z5tAW;Lhzin&)#6aqxIV_7ysX%uC5sd_055&$?G0l^w@L>-G9$?b>^u*nak(QABl7+ z)9>DYqyJB_pXD!*(8MP@uix7%%cWa9my6WSNmMTUZ)1diS>~ZB{Wo}TXJni1nZB26 z$E^Pb1C871U-7k(q-?Ev;8eGIkhp6kKd<>xv2ZrwW5}>F=F>qYVxSXEOwh1xnnWBE znD&`A-_OnodBtO;l^~gXki(HsWVVroq`~526LIp*k7z!)!r`{mRz@+2ei+`TGo(!t zY;Q4J@f{BxR__aE@XUPzKt z;8H_fslLhFO@k0~W8PyZrrF62HGXM1RsBGHR-(XHRN7@=`&ue#n6|>cAtlz8} zDqM{bbEWY#_Do&I zKVTN;Tm}nzu+@Y7mK(()S>!mATyb`HRA^ z-)}JD)4se$EmeNI@#h*m=q@Gx3WST_F2Mbnc1Fq-(E>H${P^m!Tj2?VTX@M%)_RR~ z_4{q@KWX~2$Dyt6z)DL-4R5w*G;A>Xd%A@|TcZwN1?!K6qUM*h5STTI7Na6FkvU{w zP)dN;b9Buk^G6=~uowuNN_ma}JXmkARab!^{?wxjkp%|xl4aK2VoMzl=1(2#r|)nT zV5_iZ!PT1rbJELKQWavn^L%a6uV8a#oV_SNe^G0J3&|Fr1}B~P6J%`p&Iu)CpGvX^ zIrYGHy0bAHHkX|D$!Q42`s-xP4M@AJ3aCLUHUDZkqx9Kqlo4Hjqu$C1(0+FKNUzf@ z_W9?R(eh%0jTjg0TjNgMaWl@psH~v5Iy9c)DJZ=&Mv|AVI=3i%Ze?bM$$j z1Bw+YUGZyZa#b0BO%A&MKMVEu=yE(kIyu)U!SXGF3X>X)=3nKZhW|3{VKDSWB5dJT z9?#$Xv9?Qxh*9=~=^PcYd!f$)4@`{!*0fV*{hYz4lmRRzt}kM2bs(lC>5; z`O|jL(M8S4g}%aLJ+1;!99<77+X6Ko8jfbHz12o;KfNhIrjLuFT3(g};U9vAomiT` zykoI~=KDD%AB*A47}z6v$u5-dCewAxnVKZ>1}RpOS<_BDbv}`?(UVb#c|A$eu^;2O zJ9!j59y+)Tgt9&lbUrIcDZ!!^yWfT+^x|zJNFInqyR|Mq`y$qgw~aG{1Lmz`onp+` z2i=mwU*YkcP|VH+>i;CiV>87wr1C@5ef(}rwlE)rmL@Q+Zc?VgcfnPQc^L!CYN#G2 zevBcmY0v{9U=w!$8+$HWRq%U{O!Tv(s;ha3{IUPv8|x1=MD==_vJXKmC!mg`W^~n_Su!tjaQ{=hi7pEWY{q zUtCbR9=9MJrbVlNU{+5;%Z0GlF^gPRFW>VKkqy;_Mfv&{E_AV7?q!E!T-`C%KH)cHlLbuD#Y9KAHo-8Bi~5fq1S#GpA(k| z{kxC+|5%(yu+GK#i8k^Ny>g%;`=78QKjrs7E}+JZDq*PLo@)pdBF4cHLoB*$zF-af z3a3xHjYt&tUk#|?;R%812zhBWqH6Ucyk(~JqHZ{U{=le z;Ooo7ro0p{^7p5Zz%zGa{Qv?v$5s7VFJ1pZ{AmW7j(3+#!i3cig!L3n@#;g1Qq{eQ zca`S$HpAUo%R#(VJoPgK4vugVYH+N#M-uwSe)I$b15rOL7FQ|b`Lisv2Z}$4OBDPE zsq#y9diq1_?IbvPVEt_I?XZr_GaxCG;p*+*WO?{(DW_(%F_*MnkDtZ)=gS2Dc{f1Ptc~fhW@$mev;Vqo<0~r+Xl16+A zL_71m9A-8eo6`0AoG87VL-wwgrMM-J-Z9G2S<5Xf(#70{)z}4)=MOg?ub)7j+KrrnGOpUnM3|(=l5062y?8JcLzL6 zvAZS*J0o_64+0JrH3osE-kvXU;T4E`^&xg!ks;E1h`!uIN@V(4;Ql?Qb&7mNy5mRevhNg5dbcm|8{u)moZ7-G>$jO4=Yf*Dog}-7QvK zo;p32cI&p~oz5V8dg0T_X2>QKLHj9%xVi*%5}Ov#e3OZ*CVoqkKuDY{y{x<8Hy>=;o*U5mG`A|u_xVRWSUhH)#9{T@;hvZM zUM(!mHHgu|)ONkX@MQR(gQ8J;s1tlYXbwdkS{ur^3`u?sU2rhvaU@lrFCM}4d;U>3 z9Ce`$H!+b{NPS9ei1BItae{~&#!~zqXMiJ<=ti&i*OGKmOx(Y9dHXx>x_|Tlo_RA@ zdNWw(?^X)P7p@6R6Y+ofe1o60h$j59TqYqwHH<$)G5&kJ^>cwJ(aM&{u4gu-;XxRf z^nOw`$}#jz)Iv$wYR()>*xv;UXn-;FLTUa4IMg}nj0;0X+> z-(^wF!Nat(R|lHSJBt;+!9-@iXo0hW8z_E|zx_VV+%LUAao?!yjge-sB7|2><;>tl zW}!z+2))979RE3pPa^cMrY31+YT|MHnf?3IZJ$bQ?b>B7!z5qkukE_ee*MXuse>xr8}*zwSD;vH?ABY!SDI z2rdhH=qgQb-!&OO^Idht+=sB)DcQql$Hk6s?-i=brL6V8j(fOE+VkBR*;;Z{joVqJ z*qk#AVVhv}?S?5m?SPQG6 z^4?CM(?k52bV{A~!23ku8xx*Fm$`>r9!Ceke9Y2OLf5EiM|Nms6>lunl_uK-|8t+^ zehv>}851XT9uX%um%QfGPn0f*(z7jeBW&S5xB^6bygy7k#NOh|#pmLY(D>ik+1x1C z^G#tqw7lmebn3ZO*HCRwaKi~LnCImy+UST55=PF&qbFrbii;gBai0KJxKAI0y90S_ zsGO12u*)nQ{F5;EP`wcu0|?#Mndb!raieSO&V7s6YkrtG(AoGPPNHo%`r780a6B_N zuM@+jurr{Fqh)kM4$Ps-hC#PoizX?;oRDHtO$HYxX*OoDZ{y8XyXk(85LG|i)i~23 zmCw^`=MKli^h3r1FiCF~4pcPB7nP2;SnKxnwy$ByA@fB)c^k1zp{8uc(LuSK)-&f~ zVtg`gv|&+4#tbjG=h{!ORp22gDZRM4sc^cR9<86rrRt)qQ~rW!X?}s3W@7}%9Z&ob z5wsMTB6sbgD4D&X1}ZTz+w`-vaig|t>zeS(!pEzboKx3Od+rJWV~qAkhxaJ7lLtuk zFs}iM%{O>jOu{p49zw!&j^Uw(15fpvXK<_man9CLwgZ6`A(xe#0AfjBx8wb`82d;bpugIRa376I<#U@Lp zNb2qia?bBKlP~r4+DoWOh-xTs(fqLLr}1jh4Ab`#d3t;J!L%%cYh2_YO$AZ2;Tn%- z_j`-8!OME}O^4G%gY^uHH=#Xz!O`9qP4`Ivsh79*$K5M>o&@P^Apsr;%WsAmGH8lG zMWD<(c`&M7>TDKaY=AJMYaD)27#qrk3>8{}lys+q=#bvh$fxJoeR&@4xV3$IBRFU< zn7Qwpdq`1F_WZuc#SechNQf%^a*5YG(wNchye`p6_IyYeQN8OfQ;=1B4#6t1rMEv| zoXl|NjUwHkKS;ZD6KK7~##h8fi7a|${~}$_ri#26y)p`$qp&0kUwU1CKG$r^ds|zY z>~sv?Cs*CugjI$U?0#O8a9X*!XX#N@q?swGK4qd*8pa2&Ab?U!kY_d==oi}L(?BAH zg}1F;NM2kL3CxPGRVUP@2(5)#(p_xQvFnX)NkIobcV0Q7@lPLoX%_L6ySHGqU#z=R zQL>eFF?>Qv6HbiWQ;RcOps%}Y^T$^QVN7ZXL3Zvugm(Wfx(tJ~ZI_1RL(Lgs4h0m> zFQbL460Lrg@pGec-zi``;w&l!u;Pe-pkz>L^^Tc^-8LrYr{*R+-b19DkY*wbldEZ! zX$=m*DYE6$!#As^GiI!_?Ey1)9vs?GST-z6H1hXC$VOsIPPkyH7&TQ&@ok|1e2V(COHHs>V_kl zNn;yLPBb&f45s94$^9!MAHFH8w_%pErSjhDhVnYT3{6#ZD4EURN20%_O!?Oj5vWAdQw$P z15JGsteqfGuyaOHqs%!?hf9nm3rPnq*obkscKQ5CN5WfV%utAJLFnY`*}jpjB0wvg z!beB=ky8OcBXuu5Lvb0_S$>pEx}Xy#c$*hlXN2kr>fJ}9NL2+D)qe5)Qme?C6bWqI zqlO*DXIE^o(inlmU~4iJ|13xj3CRrJ2cSPq{}zF7uP}{bpQfP z9z%t3d1(CH=~RbT5Qh;Y$utc!R`zUOQ@IacqD>{ZYaJV}-fy7!Wt zlT+;6BXeke`tGaww)M=UaDpV)nAzer6z`0dF&$#qUwxs6(s@|0x-%f|zL}GrsVnhi zD(*XO7_IiueZ;Ocvp^9TG6bh9QiBRt*SOhOTGN!1pC8fxGJy{!^}tG!LmSom?)08< z1eL}_jqe04fo{MW4V z;_ao{AWCc)O<2&nh?_1F(V8I%E<$Yrn~V~2nHQ$Cg?|o$w7$cB^m}HLr3pSFuzFA` zYi-f9#iN)B>|D|i4D<<&r&#Kv1E!8PWb3m)3-?3I!XAsF@VA=}{eD`U3NcFxNKbdc zz=Ac6d{K>UjR|#&QGydaKYiy{68>Qx1a7%8!u$~odfAkXR2h}WM1j-#jI>=AL45kO zK{7ADPDpMVys&K1KFY1()^3+2G9yWJEJ@HDAS2~~gA+Nfrup$*XxP+t5%11?dR5u7 z%*Mt%7PcnH(F1XG!z4#h0?=8Ot|gz%39+|(Jsj~p42%3|mll9!yYuKnomFhpL# z0~ARPKeFe=Cl<*UOjirV+Dt!jN9;8*5(AK`+w3v{U`QZ5Ps!wpjvWc@=otzlcc#Df zn^!lOZff(M6S?co@Rrj8(~gUy_#@o77kFZr`D4ipDu!Fiu_<*ELT6Nl=|OZaqdF($ z{PfSECfDj~M#^rI-(cuj%AiGDXIozD@20QL4x!#M(%Gev)X6Z5PoE_$PW5#4qmA!> zfzD`)Iy8FeLB<}xLruk90*EsBdf0EJAb{oV=reB4^)AQ-zlx50pOcq@+f~4g?!8Y7 z`kQ1n6AkvLR)qS=;5Iv8LG&`3)$6y&rht4FXnnJt+<&;Rx{)BHbN%7V1gPl2jw{ zKy-ko0XjnbbSbX-#ndK-_yW7)?r9}L0RR2bpnV+%6eMes@Q}ja2P>tzL=X8~oFmfF))wH)K2Mxvj z!lwC$5yfkygF0IT)ky=BewB6TzSrR*D9UoL(mx>{vzn`FzA7BZa;8Stfk9NPdLC#gD|u(Q-N{IFl3V{hUe(45_wo zdCpb^9G;k)6a=;tMb(TxmZE>`idJFKJZQ0$Y`%tss?qypl$hpnU0Z-_UE1tQ z&=G@BNN?Fjl$4y9yhZ$ZKdFR>SvDnUVl~>7vzj%T*&`K-9y?(e7HsAsJO6@XO-6cx zt=PCcL((GAT6@&;o{@6iUwLMxQ*fHIq9|45z}6Oy-~g#z@g=5z3n!rgZWPl{54Cz}gQ7L+7U|t??mbSf!TLAaP3;-ed7X>xB zs=vf@hJ5eH2<_tWBmrBKIQ{;>nXJR{?ksKBG{_2 z_9z}5kx?r>$lttZN%nQP32i#SJDp!ag7_&+KhC}++DOC7e6tsf`~G2;QYfkBT+lOV z6TtC3dkM^b)&NeTBui7o4}XzJD+dZ!TJth;Sc0iuxq(-Fw@5ZDLf$GkxK;H0dEc=l zgA3t-5jBZQC$iNW{SSdKv8&N_u0kN{jw_!>y8oo_wKuSH1mz2>suJm%ID_pWO=cd_ z!zO?XG0(jbyKK_3%XF*^#Jq!T8ihCQqf$bgBEDt6@qnaet?QHV*9hvU;B6LZd&Lj7 z`Z#*8c((M-A=U%V=Rc}%bd|Kn*QWaCsI)jUpcp)4R9u{EZel{Uy+4LF>BQwE@C~7V zs)0NQJ3qI2dzNk(C}?u2xibKr)8nSXcO*6odgQhIfOYAD6wcL%jyjLSSVxMs?p{F6 zV;h+MYJ(pH*(5(IAKR1n(5fLJLyqKiwd8^*I6DJw*RCIQKDo2@iX65%Hh`!4=DHjk zdy@4R8S58?0bS|Dk4kZ7aiD&(Z zNKJc#LyK4EkM%?Ts^9C7TMVzPL@|D;yqU4YB^<$PE| zi*pL(pEv`%%LmhJ*Q^a!g^IO_nE9(0d8PAhiq8(rqI{nhO%Iva*MC)TBRU7@ZaSF4 z4>h;KM?=>#T&8I*`4V)tF?bPoFXL8_l8IbUmeF7AXBgj#uYG6>%8l4ClvDh^bSPM2 z4j2Y?EU8u0pJ-3pYceW?%c$Q>626R3;-a)d@Xmjg2011QX(IFb^^Q5zAK4H+I;wBuGM$U64fqS^I~f9&ij_gi>-QB++i=8iN* z)-F@7ih{MdBs|F2G}NNUH{H)`iNciUxl6cbuj-zR+XIjoiJT7M#JwMqa4w&N77T<4 zfjwfBsoFABYZH_zi}+!U^=qlrpNbwj6-*qy_`E8`2`9-bjY=*|tSCx`9>ME&X5L^` zk0o03WbQ({&T+rAoB~U8Q#(XH$dYfgC=74U&z`-e<=ddkIr5(7b^6uQ$M?~^*PGqQ zQm9Xr#IEboq0g2+1V!$-n~b0|n3$wuxi>UyHh-gyRbA(8o6%rOyJ!b#i^S%nH))ef zPdHbc@a#TwviZ!a8GmwZWXl>uj9g@%ey`g^%dU7!PZ2*O-ySo%iOjrSUA2TFTxS3B zIIQ5gpr-8t2UknYl@*N7vcvgj+Y8S>+d}`*prSoH48g(RsgfsYgd8e2J=3{x{KN;V z&q9UpbH>+TolOB72?YA+jnPU_T2R{vt_HK92kd4CUx?rp5k|Y-V`Jp}6z+mRqE0q3 zC%D4Xs;V1o{53OJn?5%)%ofA?<9%z?p${-{z0Q|Y$EWjsPfyE=yN`4YCr*|Dv-kMR(7qXMog|!5vFcE z_w%--@`q5@dV90(VHu*fgWOhsttN)!H9DBv#{v@p|?dyNWUp9*fB1z&t{5J>4|x~PWZ#Y zru_F!jdK0V6BDPOTNrgyPHl9*?`s6ui*7d}EeOiqqoS><^eU0!`Ic#taAze>Hz4h} zyY?8k5^Pywd}=vFL2tZ@%< z#B<5c?V6fA4oW@ahnp|{>?=ORf6flv0zaSYVYCm{MLoo*nme~Lxv0#J_ID2NMR+o3}!$D%SG+2moV`g9NvO{LxJ7`LH5u8Yi%{PGd+V0voGcuD^L9X-gK zp20P3z21pWv9PfTVM~#5@s;B_$ajc8hrxwEbr0}HeY!@J(Zo$o-pl4{rQ+1S-Q}+i zDzXVBe@)~{e;Hz$sMe4Bmb`CZPxdfEdlp=PF;3ZxMn0eZ9<_<(_1GaAt`aE~OL9AS z-04bTyXG}%W}2iX*DMU~)n7fta)~@{^h9w*@&wqEup2RW_}S_{Umj`1xt``!9+;#P5T2q{HV!}F?1cN_;V?c>7#1=>^XCM*K>hlA0sw= zw4bSXZ-UJ(e%Ud3yZ}b%ma{3lyZ>uN!Hp<*P5;+|rtP z7}0c~7mNhnd>w!a3wQ!9Na0$oERRo@RyblKw_Y@?nJedv?Zkk@d@t*wFKxsE&M_ef z3qh=jE{Ro3WV9cjVuLEn z2O%ak9qS}+s4`bdPUcCH1@M?I5xWlN?i$%fhl`>CR6(BshH@u{L;3ihn9i0 z9F$w;_I}-;7=ztRhC7Y8;PGr*tVv%bs)9#ZdJ+o9IM{!bPI`wQceS_I$@$x1(520I_uYSEL1ch~=Zwij2d) zAk~DZt1~%Jc_?PTQ6FEw9YBr25^171-FW+w$u-`~+F5c0ZusDE&(BsKopakF21anp zGm~o<*S_nZ7kVb@h^qLc6o?+ww~(WKIOVs(9a>C?1FrN7)}7oVgF#NHEGqy-IZQ*C zJpwm!C!wdAs4GWTh=r@EE1&5vM(zNd-E5YZCt%<+v?Z2>W;c4tJ2TbbQEI}Q>pqRV zUq;E{U-ZXm(sfnwK*gnIa#PUwoWQ2MPigLiO3Q*JgP1xwOl!jAp2Q|bxq4m8XVTPQ z$qS^+&JZrO-p)JTB#;myFH7Ldy49X_G$_v4lqP?u+Q$W_qNsnxC2lsnB}87HvvIy& zJP~p_eF<$jJl+T#{mw@qLzwKY^m=Ejx6RXZBW2BlygV)q;IQ(dt%%rCn_bpbI$g~W z%)UmwtBRxMlqiY%SDZct$ofJ8hJF`nMAMV+pm`Uq$!?JccHc;oQuf!{Xflg%bw+hB z!4;4~pef)J_w&Ql^8-qBrVHMesl zGw;C1F#KtCDHPMQ_|6J4s>*^U4cnqinL5VqN z^l0gU_;LKhEnFZ ziAlaYIb>N8-)dzv4^*e&UsKpwTPD? zZct=WZ{N3-n+B$?-LA$$Xs3`E@B%qXb=q=+K;GpRk+psEly;!GHr@n%*yifJL1rSP zRz%c%Y!J%Sopl`d73<3Ga5_NGsO#pIX}yyoD;qs2K`Q{??`uS#KI`S`_80uzJrvejN^Hw)A86lsJ59 zMzuw%)@Nrd#ZB=#9Nh4w_PyMvu4Udrpei87Td%k~Rvh-sJ9FZ_G?_I_u%mDJUN^n< zA!8}5Z_Ibp% zA6jzkMtCXWaXfcayeD<{{wTAG0znopz1Rw4#`z5|PYQ_nUDQljaS^A>%;YoZvEF_A zJI*dPFvQES&Z0Z>$(${OmzP$B*K3(U&H6W`TSw^;|W!f=sE(R7@b-(+{TO6mC;V7+oB-QzV}mH}6~0O32oi^?~D`_vr@sM|oqb>r%&GoN$8Uj$H=`n_de0v= zMquNNhUbMU`r^dGWyNU-YpZ1~k?}P}SptzwjxnKyo4%4Y^QVd#`;q8D4jQnKpzA0z zCj<@6`()FeWyu)tQ@_V&IyvCcTV+senZIKhD7TJP$^|B%K1f#=K9K+7M7Lgp2G~Fp zu)NgxL=WN#jQpz>n!NoB1PS>=H5rugGnAOslyJ^?r=g8`JZ00dFkZ3J(j9-YA{n?k zsfuq%2>mL~n!oBQ%A0qQO(iBUv0=&>ON~z0gdxARE^PYXpYB49${8O?N+}lI_xea5 znb#&~%R}K;8>5K!*g7H9l_W^qwGCyJzJp#L1`Lm2t1Ods{vy`%@ssi*vT?z@Kn?h*Q?yD^;5ARe!0YmD2Z zfk5Tkb*Y0`+0BdJ$)*j5ReF3Eiq|tm%z2ZyhLH_@q-@alt3%14p@ag5_$l&3@eLyn zXCJOo0GI2I{!AAv5Rl#&d2Vv5{*3ELC@IKMIBu+XU;7tQ)C?go&l`lAXCD;`b{{VT z-U~R1YW6N$AV7<^8Dlfy6Ddy@!b=)^wpZ^%tkxK7v#sbl zRgBD_S7x4+>Mxf|qs6wZ;a(7T@xgAC#l>!Y`NxFKyi z(D~o0{yR=cGbi}3nzMj%{>MLI3g@^X_`A$)oM-wz%`;-Z1SZs4CLE$}Y^;BDS)4Vn zW>Wjd>A3kH1_hu*Bk0<7%F!P_F^xZ_AGn|kOp40MmXJa6lw}0OdDZf&WvNm&6U%9x zC^<@sdr`IQadQg^LIl!wl6GpIBP-9Yi?AKcexcw$q`Ol3&Uy2~mMe*n7tu-F;>XGD z7=5KW+ZRumdW_s2F!mYqJC3NRb@=c~=CY1%%yHg5Z!Y*bgiw`B#E>zq=d6$ps~V21 z8W(&xiCwyi9)lO%yX#85TuT1Q4&}Wc$w#S8*z=N(>a}>rABlI_MzTb8+<7GtIZr3A zSCY}4nvD@UybqhpD)yNQ>R zm(3A|rUj6<`lk4+86kp!7&tZz3yWWiZ&79LGvGdG*nSK`egvfdff!is>)#B1)7$VW%sj6!Q?RvNaFA+;uz!|b z1ubOnx=wrbpvUWGNyM*inAM9Ovkuic{*wmPw% zSD1^)0*%c zrgO#QuQI(-GU7)NY$@l7S}7aZD}_~ETUXo}=uT@5@Z${*24ckui*dppa!8@Wi~bU? z0I3j3M)C)NsAyUfa(z5>TKlrZxwblqHYj7Ctz}MP!i^F`^6)MYm^tQY3;nBX0s%E7 zLfTl9@)I;yw0I(U06aiEwYdFaQ|Go5edUVXUrkbm)zA2}*P?8Q+&91PfbP)A6W_t0 z)V3t*rF!>g(?Ak}4NCno*Io!*K@?|(^j<^mi^i1u$krG|vwwV&$x_a^(vV7&qqSO% zIS?Sk!R@+eU(B@p7FFoa$c&i3kVsrx^Hh}7_^gtAcRGY0ES?N*443KI3K82OC~;e8 zGD?LwH=Tr6*N8LzY;ugEW<%5Z973v>qv?()QM`?fmEe=P=MFh`FUQn0COa96vwY}7 zF)`W!S_>#5 z#{Wz1{DDSuAY1Z^;sUb?Br{QAabfpO+j2iOz4UQ?k`}D`IzHjTEefAhd&*nOact+& zfda!NkhLS?aKfGoYcZ0Bq=YyZzq>dMnRPR&Km^PkyKw$+l@1Q;vitc&kmiTb>g)@% z@*=nRT5hJk=}D6I7w7;(JluLs*5r(? z5DqfpiL4#mb4=-s^8p@Kq%2|?VCm%ILeS(VRHRY)&fjzG2=;;r3mJ==2I<~*e9fxr zZ`l2XWbCmb+h@w(7-!8?(gS>jyuz*ml-($+TmJbx$a!hK3P2v7x;rIkz|2$IuEU1< ze7TvjE5BJKP|L4BsoY~kk>c|Q{A$vga=ubfr;P0fhx@e-qk z-Jd0R>lFc_Mh=X#5-xV#`Rrb^6B{vLyEBwQqt0OBnn8F`nQ=qcXfZ$QCOj&|H8@Nk zJ(d3gEsWeyEhxe#!EjiJ444QN8-t6lV9Y`GX5bfTG)x#uEhXW(xJ7}M^7AX|k08n; z-X@HnmW^4(2{{Uw=&x>XrSJ(!zxzp8Z1wlTW6CYWc_&l{X#HNg^Gx%1Up`%!eP>Ucg^r`@IEoy`t4zTk)zP<}ANSD90&ISx1A=O2R z3Q4&bss*04JOj#vZpPQ?MG3`E%Br4m1k5$kQkIX;f3$NG)FmIhQO;oxm02PU7p;%T zOtodJ(%Y?OE#AeAYzsWHk@$ey+-(%q2jIkHT~PpTL&$R784hF1K4K;|Mm<1UOGqlP zTrv5m_2y=$yzGo3#R8(KMD%0?0UTyV@fDNiXA3Ezl?OBlwdc#HM2vhlfk`zM`}aJO zR0~xd`MoC%5lIuAv=a75`#v^SO zHO@7>emqP=(^GVEMj4!@v0}&Q6UkP{SZDRqIa9$jfzA_DSX4EO+G!Mf)apX~jlLuNapHDpJ2Y ziPB(`D&Z`CdAh)66^&ucero+S7xW&fs_AIbG5Z`Q;4o>gB1s>tqM#B6Cw}PtA+6ZP zBsjLxFa$D`)JVM)u-??Wi%gP&fzqQHm9U-=lX;GWy7_pr_t4;eh^U7i|3Z`*#>Uc^b`{uOn;N5mGXlc< zbWq-+!1c}G*_@K6{Lb14gK$83C};cNa$dNb#M0QDe-St z%22w^`Xip2Z)E(=1B@!f($rV7MoPkQ^h#G9XJc((nQ%7%7p6ZgOqOy{G?w_ zp{?EkPsHuJ91c|_LQSWi!U(rZ%Lzs0W=G_x#IDTsWL*f;gUU^tTZHP#Brz323D`dQ ziBT>lvwnd)7dvy+4CjR!^m@0GXq?UM9fER`6Hlm4&m+SxR5{Yr9C zX+d`@B#R+|M}?zC>6MU4RcqatC{`57A=B65V0%?;nss7furn@Ow`&6fKF@GGDdJG1 z5l`kDiBJPxN$%pXNU}RMjXJi@?q^%;{nz~se4db9P%_5sEGgPbF<(+rzkl5YJI%K% z(A0r>l<@oFgt&TfiEmnv@xcXhzSPV5xvKU&u(d;@UIDRs&`g9u!%H^aPz3-@a=+TP z=NnI+`8I0YT0Pal+3B$$(PsU%7hizt{K^leNb5Scng+)K4h-eg<2okuAq*g=Ys(;i zp}6i7-fGP~b0LQMTkryxM@;CdD*M_%mE?eFr4W&8Ze#{}Wm_qm75gq<}NYA;moBtx9XQ1rx54rh-_mxkD(?Acf}ve)2-% zE|7l>Xj=&_zabb%u5n<6>lSNgJrG?+`haXHhOGmw90~m2`H1|q^fbovnui( zR&m!U8^tqrSLo?8E>J;uDYlIbYQd;zpuZ#~NdZrkx+&1y`5hBPky(Xs1?v;KL>V@Y zO%*+ms0XyWX6!y%65~6BrE5`AnM$wAaG{M4qm6K6Dk30zEmdRefG*v=Dbo~Um&Rk1 z;J`Gv_*TEQZu-dGNZJg{VrGcc*lwzDjXDmytiUc?SX|~!G${7`hChUfO{sPIynpAM zqo;TpwB*pbEJo19w4irlsG<0UdT>`8!v=2mzP74t$aJH)YOY3AA}qa-yd!DyXR!bw z^-ng|OKHNyoNBftjd8(fDWb_)Rr*NreF|I~dLuCbbtc#*z6vGp^ZvoxE}0}@!H>1V zuNN(7SMSQ3uS2Fy#GWs|NWv$McRQQqg=WeljsqT4g`KsM3V{s-&|g%9IPkY| z5vFA#ETba;+^Wray52*-m9P;R6$DP->_pu!Ic4*!jz~<1fkf51x#;LBe(7n56BG=5 zjt<1pCGd-a7VJ%C_Mjfk7c`xmav?~Df(l}HAfcnbXn#W*q-}l-` z4ESoUasF}gXP#PDW*(BQ{9#uKzaNj=CBE&S31`v>5{Iivz0$J`SfGhrc<*VA;EXP6 zF0^sZRghqYyVIq*oHI!6ve!V2oZ$+F`9;lJsqH8)wNb02ej&AkH!sn2#uXk)12xy`7aU&e3E*_Se(+<(%lWm|DSJmKTnoX# z#uGZgWdIFd_f_9!erh8$J4-`sc^5L|0l+Wo`f|!aRXx}D%ChC9NpL{Spm2-EMp-B> z@o3K_kh0IlqBy6AIgw=ml#WCZO^aMi|IuhzjHYLA?@}NJ%6TW6W5d=j_s&lAxzt8^ z(uVt5;bJRY&SN>4Zt&!gv?kxsR~yFI8w93mJ4gNWvLS$D^;Jdc#Kt$TU=T|&>sD(1 zE=yF|?2q>C4yM7)SW{<{@*c~xrnF2!!BHH?r|b5PfU;py5<4*qF1mvkqJkp~Ozz?Y1eh)Yt4=n^KumT%de}4b z(;d^&9;)E6ZAVLwN+c;qP>oYV)Il}iNi9wYOpbVky?fjMljBb8}&_VhYycEptodtHE*E zz-0_g8*0`idW5sGZCuDe$d! zR{=6kVxraCD_3ZfN>s!VV98`~i&3T(bVm%{iZ0YKkNFymL zE8J%5?s{+lXS9i9p@k)Y-=WAu{(Vi2Sj*0uF%J8(TCFfW^=GDDr`=atI`!UwkVzHF z6q(OHtMZ(lPsi0Fh7VQIL$LTCM_?GCv2Np9(x7DL@Bz}2n{JzK1Kl<4+EK;Jbh_cS zWK=SKZoE?BE-)gu1?1tT0SY`f+Dn74iX~>3fe{AQ(5Fqww@F)J)~;eL+2hy52bHCs z4;P>|K~*89+(`t{rMxV8l@=odT-N8oM{-TCSBkrfKsKLyD=Qe|@GPX4WTc<^KUWyN z68>`Z=Aokf$D2{+AUGNSL|@pHXr=;rMwv5dLsWDYag*TZLsSU{0gVmKYI3l_9G-B? zL}T01I{@d~)GyIfO0UPw_O94k$`OJDwbS@ip3hMfPLGlKmirjcqr7*i@4q6Q{pyi)Yac$Ku0X7uLoz+t zo}C{#DxJ#RLbs_o%%wALs+C;8svtsY33+sj&H)njR|BHzjb7JDX!?_B7}SOI?Hm546( zq_>!%&}S(nrBpgJfE3k7C3&%x9;@3a)u1@!G^!-pa6+UTpRu}eDQxN$_BX?fvc6A6 zJuFZ6-7@PM%M*)f1R5&ZDcsLE7M9ow=oWqc^jt8 zYw6r}Rx?t?G%JZ7vDD=Ow%jPn>CI4APz8!lP76Xs@-lgMAt+J}WL7IM7%zGcgBnECSH3 z<^mp%>f;D7bdc3RKz}bDEOmHg`}G z;SfvvpiWxRHOxHaGXYycC7S$-zz{>OPg-jGspFJ14uW)>sw-{^f^sxqJPYKLf!6%f z6uth1g;e2>QdJEBM$7Cc;%x2FnKjlKKs@t84xgQ=muKQbu~vBR1OkuvY@doMob*%d z^K4DK$0}`UP1cjND>q3ya7k6o9mK`QpH|Ep6}(xs8G+PuJ^=>hbh24(4q8L|-~& zL_oyj)u~DHYX6+*oiLMtH2sEys1YXXDO-337H#_BgT-Zo+1X{?mW3VZ2&JkSu0$xr zfY#JbQt1id!fG5f8ql?glB9Yt2R6DE0C&C-w3UltP42?U9s4N8YJL5se$PYNn%y+j zz?aLjnzUJn3Uzr+9y3cA>|ykF3j8MS8FoYsu?1za4!8Ij&-mwadaR}pjoH`*dwF2; z>t{7u-^O(#7G<}}GWA!qi6#}1^B4Ud zss4z1q;kTaT96Rb;Q-;=0rNxvmPgc zD|i^zzt7G_9%?ef*F{+oRq;{a!?qx81W6!Uy@^AP)`m- zqP0Ff)K|&#{Yi<57e{UR_!`T-{G+Uyd+QX;IHY9a*{wbJ5zhm1-kC`)L6XjXu;SFa zXFT*QQ?+dM^*N&N_JfUHMvFC;(EG#?n#5;Sufk4;nd|`8#e2>qS_;NtyagoHtN;&> zpp=~6cXtUXnJnH=qXZZ`4@JbIrwbXHDqoZBR@T@d_7#@R#} zMCzTyOWQL9XV()p@p3I>9`K1`&?n&)CXBb%Kb_+b0S7*6WYG6&%j+i(x7P&Gmzqb$ zcuZ)b@csC|ru$g1wdXrduJ1=$MpM_}?+DVSf zY3pJ>y&7D!N^F;H%5KJ7-P&`m=zE)b=ZVa#sWYuRF+Cgk0;6{tZv-DA*hngBy|S}A zRlV)XqYQd%i5j6XHP@%hD9g89YDOQMJrV0eIYM!X7b}AMOuf2sn~Q`;u)y zLulQRHHlxpVOjkLLU&ol+Ib_{39~vL9a7DiH#@1M4y$}h3GW4Grvj$Gq?wCv0jX;F z#(C?-*$_q*Np-iF^>b|%J~n-hrr(jl8z;QbL~@XgcuHKmWu*|8i3{&?oP&u^1XCBV z)}g;)M(b?ggGhN!Vxp`=XY|~ttYNC5-etK7%!wPu!gCsp&I`sNI?BP9r=dT?k2kd7 zWY~9{FPPNq)wC-Y(G5;l%)-i4e2&@h9{V}`Ib1I6adu5(P4!}ka(Us|r|*$1<(Htv zp@mnQUScID;vUNS@oORKj~GAUcZg+<%cPjZ*UioDs?HduNe2y7O|Byo#6pG$p+az> zEwiB-?`OGb+)-uj9tegQ`iR9|);-qz@LG3xRdjfTPp@hW{TPYfX@exDmNQVc`kxIO zEKa1J4m3S}ywxKeTj?rcLBr4<32I z^C?vi&%q2!?*f%Bk+EoyuU0zrc=v~Ku5;9k_m8&MsH9V@td&J~fdSWyD5Y@^?p`Ro zhB;EP8|>l=Mp-uqr2@@(G(^=IAy zeOi#-imaT)oY7l$a#!VH9Uz8 zO26q(wd85d5hbzpq7ZpYlbX%g{BoNuscE*kX=i1EG@JT3%4|Ek==wAw^J-;&=K}ZC zUr6!uce)8Zz);g)NYigEW#kB*7QUwTKSxh%X?rhu+NaFB*#6G(T~|8D_2heNH+Qa# zv~+~yl6t`8c;qwU!u+san2_(Byup_>*Imj_fYbe|Qa@BNp4(erWUSb0?Kin0$b?`3 zx$7}Q;7{lC=O|yeV&TReOS2T|wxG4F+}(h3CkFnH0t`5hNvF1n8fB?s9{aK-@=21a z=yHmexi)b7EMf{TKiEU^A|TP$QyrPyIO<-ZB%tY$Q8JGRv6?0&L3i{F8X52((nCq# zXR@IOTz!ArZdm{1Q%RBCXh^C)3jVPLr-=wT0uC`(``}c5&uPvdU*UckAcSl2subJ* zTW_WCPJL2hr)(X7|}a9m z%WnvTyzvqzJ32PSNT(hwExkOqQ*|Nm8=tYOFv^A4l@|+(zG7DEJ*N?mSU1eC{nZ~7 zW+U2c_@CM=Z};^}Gsjef9cmB44pn#_iAzDZ0k_N#RFtOhYQG*bg%p|cfKnD{tOWR; zF#SaVj*r{pY#P9KTaR#>Lv^I zcO4hWeZ#Qsj+P7G4ALnlJ8rAu6FU-$Mw@*cM$EhVye8~?a(%}vc z_Pxa-)5AVz-}L&wu4RZn{y0aHga(=DU3%UQCZYMT0qJ72r2L$5Ey16*w?~686@WYJ z8LT+0b6^}E=<5W1cq67S?=&ouwVdyTkKU9RThNz3ON&E`pUucDdUc+|(=amgjmVup zDtHD@fzT>a3ExM4w5O{ldE%`^`k8@IGK@#0a$j*U!p0`;QL%|DFuj%P0qMFZ>T(45 zQGu|9iZ-S+&ZB|I(E{I-KGu(sXLW6fjoU8#Y}*McKKq~hf86bz#~Z7GQF~Yz`kWQ_ zec5>Xr$m{;7&9|UWAjpk-U?gNby5K)UfFo~hm!Q}+7p^aO*}VAY{1R97;o!Ls%@Bj zut);INvck}p=fsiPv}UYVKziZv`1vrj?dJND8tC<@gW#U8S+(=w-vS{;O;kWUT&wb zZGLTPV{O*Ei`W}#>H@ljWYz8Hj87qYU;wq`<6$xMg*)EBNG)jC+y{NPDwvhYQ16!}oI0>DrNpyiulB>u00+ zMfQBQMkqNMM;vzfp{!2u2OIBDHEv((mGT_4({LaL12Hz9@Z6^Z05H7`=)oX&_UY`9 zX%g>Rb$rDLj&3?bzW z0xhB{XS??V$s=Lk=IOHVFC@)RJvo7LCPx(@6ag$(qmNlKU5k4?+N*hm`K6f(#zC4J zlO}q7{6uFjVEmu4>N)(z($|rMHfN0BIsVTawUT$tHV}?AJ9>G6x_#_al#$M$`BT$L ze%k2nN@IiE?jic3xSdbl5cD&d`M8-s{siFp!Ont`C zjs4Oq(pM-ywa?#T4qN;{=N%t7%l{T+%;I}WI&ED!k^#o)-n0Ew_z}+Uug;`Mo?qk| z#&EPcLO~%@y;QjtaqgqC<>ikWgz!?Ej`A}?aVMBD7n?j7r2xz$+q=ye-BkGdhgXmu zuW>Vkf$j2~JP)5)l-A3k`%hgO49Y9~#)H%P6OP{XFRbGGv(3l9vFP(N)BH9Z@V()> zikxXMs{(svVJ>jax=^zz{u=6!ePr? zPm+cjS0$VeE2O$Q3pvDVZ_CEj+$UW+b04)%Bq9yB6Uu|OURyh*7AWbx=V?*Z zw$#NLOb{^fryQ^6t?^LmFFVH+)t498iNspx&`q01`L`b;*G=w6Q1pCm^^B^W-y> znE*zg9U>>(z-MYdRN;XBymVVbb9;aAt@r+}Innq4|CpeiGh*Fz^`O{TtJkTN{;}}r z6B0r>zTBQ-+uR6iLweBa?n7yeMzo%h=COzN0wfa$hm6$d3iRwYzSWAey$6ThILxda zsk2{ruZufSB^9MWV8chW^Fj03ezr*%3;VX>ycAF`X5mQ3M8_XQQ>s^n%d(emMZERB z+n^Caj~800Q)6)QM}VrvjGAz6_dJXt?bS(G zi>%I;(;FA@4*C7Lg~WLLb;qD>P7haC7w=2fDz&7XTh*C~H`v`gdT$Cs*itc4KWC^& z@nQM9R}Y@=G;an7o*4c}vA`A_nwnJ!p3qT~MZ8b(=qE&jHofkd;1?zT-9 zH4x5gN`%w1$CC4@?^AxXwG?Ot5EbSVejdJ4m)mo4r(-8e5s8+B>p&jXg~claNNWY< zWAn7{jxsi{^&uL&k95W?zl$c~kf?fN-h$tY?}JnAWV`IPYZ!NInUG(Z!;7z<5B-=( zhFv~x9P*!Py?_mhXUgkQs(_Q{zQ&(YMfB@3ig$D8nDM*Cn%1RB;EcSmE?y$8dbGH~ zVK3syo>k-|BuXX9US%kR8@r?s?2oRdr1oLzm=#11`VibbI|}!`n`3QKOU=2hDRxQ` zysSaF!^O3yqGI16HxtS8*egGex;8v{+zNQqbs54zLj>)yVa;=MmpRew-|1tGUU2AC z5LXZsT|m^Oj&!!DawNUCq}Vo1T8%GCK;ff|45x&6lg#3C&mBtah?y>U7h`8}RcbS( zK%6PDYJ@yuu+ke}dO&G!a!x7RFuh!`E%|1)=?L4J%lUp)ue}pksq2lAmo<29zU(l1 zF2qsaHaC$Gx{=I41TjhGcmD~Dkj&_>COv-g#%Zt<8%$;I;67oiRQRp;-A}2GH)%vS z=kwQjZY~1DJ_?}J4bw;PM|u^-J)tH*b&huZ&`I@Vo@`}{0@Uc(P@X4SaW3+k@TL|Z z!V>Et)K7q)XI4Y|QS!bbMz$HSvszB<#nIC0HvKcYO#JBt&OCcy@ix(zvzaA_2xcfA zRrgN}0d1L=&T;}Tt9ePb!z;X>1jR|MOAM`TBsk>$Of@+ZLNDwcsuF^=6h*d`HBurS zia#*&eu|yhnGrU9RQ%}dvXfj}g;B}G#>}YYLFfymu%>PE<-&ojqEx+{2!%Xf=ZExW z>GE%wc6w^G>r|plYqiSr^LxM27P_Cy5FLTAua}W6j(HwaGC#~M?R6R^5j|(3WBou} zso+JXN0s!X3I+0<`86hRpx7yrB9}PE@(rZiP;MYgMr%%y6&Q~<&M)LFN;u3kAt@v2 z3;s&%C5s!tTP^<-DjMp~iS;0XgV({}deLxH;zOo5iNG~ACXk6eG+h!B&GC$&pfJMA zKzfM531{?c+UYgZiRLy-dw$f$lIS&bPd~J7azYyhAeRaqg$1M|16JLfoz~r&)|*;% zgQsRqUaJS0Oj`<<2q9@sDPg}fkD~XX6hA*ib~oeAdIpTwYiA6pJN(pZyJzqVNnVOC znd-P|#qgK02ef5kg2a3b5<@9ycuYX3>Hej-+M8i+jBZN;4b~Hi~!vL>N zI|S#)R0Y_7n_#)eT)db#y~k*oI|-%QSe4X4Vh55wp-P}UOjnw=P>{o)+a+P z5V3JGf=$h+t46u_`E!A*^cbI6pX*iRWi!)h=G@B-D%(<*x1|K)qRK~-I=4*)zQLmj zN7mMDH+oN57OnCve#sl63JyA6>Jk=n+p7;H z&&~ASJGGrJD4Mk)7YoW0Uv?}$HGt@7t6+o}q-5cL9Ms#xu<;&mULKQV2i}+3k^In2 zm1Kn?(vcYYDLOEhpd=9@W*%@ZTnP;MaOvTqDJcP-QHRN?N9ZW25QFJ_k2vicZTv?^ zm9i)+!woF#q9gHB%AVv!FHzZ#7r9j>QD$#6i~|VGH>eJt*)U`m_U7>nNwOLNvfb_K zP!g+VM|PHU0nW?~ffHRk++C;597^u(XEteE$AX5l;@oByqxZE+LkN|CjcGm8G=H{N z5KP=#XtvwSw){Dd_c=^;_mRpG?-_k+P8?Zp5xaR=UPW;TR4IkYyd1}GWMs^Kw7HCs zT$t>-*$7Dgt8pOlXRq zFKS+|!zF2PtDV3!x|)m}S`|eJ)29Sx;bV-E%;CuIn-_}^_W{i;U!cyS1I1?bdgdvk z)(zMg2M1eCEq`j9lOS^`w75h-o0v~YL!QZo+VD&c*|p-0z7SrS1*in5`)aX)U{m;G zIEJ_?XbqV5eTG*>Sz1UrAy05@c>QZj^0=NC=ve#sUIox)lPLR&thaLWdiU20;wB?l z(`Pf^KCH7SF3M!dN%S_4{$mCRysD9V)4?c<&{`SuI8D5fDMlOYnsZ^%qaA({YKreu z0aYsDa3Uws#|Hb!Pke$_C^eeOxDeMIXmcpozC=^-L%5eSHn>z_s9S)kS7x$%zH(VKd=uAqJu0E8cR< zfSaDxS|LO~;(#ZnD|n}{FPP<0B5u?LXfLlBUo7@(jZq<(VNzv#SkdGc&nV7>4V?s= z8rtlpHoeLiPSw=Hwfi`Dj&fZsoj9y0bB*PC0%voJazlzC+EWASk1ldRXO`FdC8=IC z;xQ3Ia)5LtecuIy0Bq^~IebWi@cf^UATS08P_u+n7M4W_{bU1FB*)E&>f*G!B4%Nl zLS+dxjiPGHjum}2$6q`>@U*N}7QfFfNn9^+V|p0Fe|O<@&cTI(#2l*;{uqcZWVh{z zaa4RjT&Jf>S!!~Cy<|TU($^HbV`X%D@>%7ZLy#&3CnrJ+!NL}Wb|ZY%y0Dv67+TyB z1Hes{V6MKotgH}ZTlg@gZ~VcNpA$@54b2V@ChR87uKSRgESOmJk*ydf!**R=<9gGm zCE1hBEiOhJU4D-OXyX;757Qqvbrv}L(kwq)bQWT4n{vVdqfL(vW@`;>*+2JpX7R?w zX*>28>afkdA3a7zY36;fi zCCUHfhbqaeOH6;9il)5Y<1hMp@$%fDMr&G2kZDZ3IxLw!Qeu07LYUw9{fXASD1R5B z8_0OUy@xiuA*NYdSL@ZsS`;YC#|~(9B(%aFg z*rRd^A0H%(-r>42eCxc`{YGQz!TfMRTz_7L-x+>U8ou5B>PjTaH;S~%y%9l_`n3H~ zdw9${O?cm>CSGUp$ulP6>TvTScZ6S;Q=DXjLu8PHjjwG0)xU#p%qaT8?%Hvvs7uZ05u{!mZ`*`3ev)vxP2E ziX|(tn}^7wTgqY1d+7!HOVrJHHARTNW63LsMt)#Y>=qMG=;=sR;FD?M-L~aF_i|P4 z&`l%fz)=Paf2Dx+5l-wpfXw@1M|s`Mk?Erw^@G=kIsIABZ%N@Z<@w%>r=p%D&3BF! z$OA47vEPjdy=f40KLBrLdJ4wxq@_gUD#TG{HuN$m?mb-Mt3wiX-xrH$MTC|y%TezI z*9co!xq*7RT!sEldTvIRuX^<^Tt}(h+B{{~wWb#E-oYISVh=AXSfsR!4pX0m*HuoT zTjImSQXiaDO}JeBm6%G@?j2cf9t4s`R2q``LTS@{6kBLB|GSTOnF4g*mOeyepD|Gw zn8?(&-OV%b>4bW_qBRCFr+xlW9^Sc9fan`6RH%%8T6j%eN70C3HVISjBQCy4MPS#= zw`lVNcf;+t4sSl?kV(<2;Umi*`L@Rhc{80o1LZi0x;I{OhfTtifZ;RKQZ%`gH6kN4 z4|Ph|ZF2%D~bvo7am!E#6*9v+TciB_IJk1{>;YgmUam<+=Q(PENvEra&58ENWka=tve$V=EBeMV6CcLPz0R2{s|#c`k#WY%Q_*jrQafl(VVjRPYP zY|5pdEU;CXbp_ZZ$o`z#Zn+%pD|xS7%*dXn%FlHqYlik)p|ab)OS@K1b)`l=vGznV z;kwV~0mSyorWt)FA&MI<&TwG)higuWKeb8~T_e}p84AtTmSi`$>A^CU7AvRl3MhYn z85}}`Sf42|Ski>eXw<8zR1WS0Z5BF4?A~Yj4*_c>A8|fkSW*br?py??ymS?|Vh$HQ zMXXHQE9bnOOAz}utF>dCb0P7$w%PK(iwhe1isBeExI4g*HrL=~w|f=VUI>eruYmE` z>9D|idDx&un->87awlrh%j@!gs%N284E!31%)%quY>BxLtGv_jTJ-fwfDDNQcwJ=XI*UBYN`zZm^#e z5sI!%#U6bTjk>rm7cupBI(N3qAoB9Uh{vru)z&$*RS;!3g&Z<0Y^R{m`m`DLd_VGa zvz%u7c+*t#fp}L34K;hh-#S#Sz>umt#J+q(4d0dM-|jtSxJ-zE{aGDia=O2s@yx+g ze>SERgv0aCj)x{+^;cQ`!{`e{0?4?Qv?sHg15ctkMr-nEDf@Ek%Ek$Y14e8~``4vL zOzT0;1`)fY%IjAaW)v<0O%&tLK}ggfcTr5_{<&AyEPuFMsdE?oLV9wk@s{FCSY^(? zgYEn;=qCSx%lQ9ovinU_iT1q%){HY=7+C(l`MdjU{{cO%_&4;l?z`Wx4_WtQKSjXk zkN+>Z3I5G?@AvD!P#z5YmzW^`D_&|d(0k8#+r}U%*Y}i)qCmh?Ojo+C;+R=f#Yv8} zXA3tk9J3DYvUS4#_vJs9!5a#rE{l)nkC=UbAz5u>w7;u43yzi^W|AJJ9s19M!cCAX zzbCSAp~&OB7X}sD_C2{6JA7vT{2!dGKxU{_>o25yjeaBO@o#0*{3m-~r!dzu%Cd)> z;dJ{#W0(G~Dn0Z3_VM)Zv;XkB>B|21<;B0sK;Fg^h5Yzlqkl^*_sI^Cxp+5L!@c;@ zcE3Mk(c2R5{&zwo5?OeEi*%rz!1_7P0lgAbNBy=li1D9^O8)12ZTNjZZGNUl%r5_B zeVV8-!Obm!*hRBlw=S0C1p`W)z3+x2blw451%Wm}#c3CV*TWIx`w`8yQvu0Rym4*4 z)(Kkh3Vz}45=yp_0udGpM_Q9<3o*kVH*<{jadAm3Qjntjd>^*xh`fLF^gq%0_izj-<4yh=?|XUW{V z%3X1Lw)2cCXNFQhgehZi5gxi zE3XA(UTo2U5PIrChb>(UUf=#6kex;s#kt$CKRv(6F_}Q7b!x=r^gvbob+c&y@yqDo zsDz3OgLMuU(3i_4CBwo{Nv??oW9!>;16@oTCj+aE+HVPUJ1KR%qQYVC+deha9W@S} zpj;LZes~3E^4jjTh_KdYD94`)r_h^lks6g8GDGews{E4c`jv;wkXrfTl&4ze)oQ1d z6S4|+Phuujc$b2)kyy5?Sy7<%{O3*)%q}DkYXYm@jFRiCm!R%pcH|hcUgp5e(&|jX zz=iFIg>-q4fy_v`N@D_;>|sby`PYqH)Kx9S>fm}z%qs1z!P ztJaV(L{4?77A=N%mUeY30p#4wWUriDVv0;bio?mV)}ikPY>EJWNICYEaTUZSI$f8JQ%`D%8` zw#<~1nOZ!lhJaK*=vd7B7&+0BQO?EbKVA;2BI98zGXF3@pBu)jux+Myli|9Hhp~fB z;ATYcb;>@p){!f2yG=*8-}#>yHvMfa#Yw@BiyB9^M?K4LG)nsbUUfjdT_vM)%N8;? zT;Dj1iCoSNCni=QTu(WbU#>8zK~TUo0D^QH1^Q`cTUdpB93HI8w{_~7YL8Zw_~s!E zr3bN~AdfEVzEIW)pO&hys~MyiA33leA*b{`Ev7*gp_p2Cch*rtF{yrZpmbiB`B>{Q%6U{iU(`RfVQTj0)QdE9JGc^4RoM=Xn(3bXF6RMt5d@< zvL?SkFCu65ylU>{wxJBLFI^jqerus+fm`s-<~yDAUe==j=YT&)`1(I_DI$NPHE&*N zjH$GrlNEpg;IEIC`bw;0Mp5?3?LtX9`TGrvvnRXCa9}ot8oy>rJ)u@8V6|XIhrq z70t+b*92YUsE733KEYVwOLChcOv~hJEY`@vC;*Se+j9iG$(FV@8KWPzpM{0yE)Q8` zby8i50(1D}t&<_0uU^IGkF&KezmV*leVKwDLkt{ynG?&OPQ?vK>p48QsLAXZ08)>_ zu7mb=%3SF!9+kO8h4LV?c)a4n@twK;DTc!~psZKUM(?TzkYO-$!F*2X8Cq*Rp-Da2 zg~QBfuAESQ$FEU+U{f3L-Z0P}6tiK;+&?M#?Aw{G8Rm_MUM7HwNUM(Mo20sm#vIw| zmos6KG(4c|5?p&)Pe!~E1o{dxR>9m^ep-#pnDIuzBd14w_;f;62;4wMq$%dbW)|ws zC;uP`{l&Pjp)(0ZNu{!45^kjMoGE`C`l^)bY`aLx3MUuE>83Muc`W?q-LrSxxdPE^ zOk}FKa$#_;>^OWk#F1CjgjB0pkCaf8tNg}*S?C7Qbpe-+O^M;6PG#FS-ku~{x!`2=+teh1JmsZ+dWuXG zK%7H_#ZCHLw)r;4hLtq>s&dEa?aTtw z-ISXi>umBT#Lmb9#enSe*d98ohdbO|PTC*dobLKeKGU0yo4jW5uPWmGbHkHkto~E_ z;J-x4`ZxLL|J@VGJ=Vk@@Qx*6$v@s`mE?X4(5MY$1u2aL)_9E0Wd*%LP}MGp9o@W( zU#V$7PP&mou+`cfw`~8^^cel#ZoG{uU-ribl|N!3sIC9_AoM%d|0e$@(FGaV3kQq9 z=w~cito+uv&(i8WSI&MRvG9zFP#34WNaz6n2w?r=a}ecb?0(9>6PD)_`2I07e0p2(uQS7Aqdl6p0{NqetMN}CbdFMmJB%N= zt-#hihwh+kG7d+Q_iZfXeD%AW{~Pe%mxsQ<>{OWk(dO?ik0QS@U!gp&yJj6c zlFMoJj>2|Gedw#n*8+pqvfKw*c+yW8r>r2fSFH7I{$t*jSOmT1=L_8(Pp@yvqsP?6 zFCxpg5G1ZH62vzds}jFczRG29q7F}3j*;=V@{G|MK04ix|Nr^V@Z|6+?#r1HW7D{$ z5z@?&iTrw9*vRw~9b}S%QmSg*yFkOh$jqd-=*hpC|9-!k{U7Gu0<5Z~Z5ZB)fPi#L zNlSOLL69zy?rzw0mm(#N)CM*oAhqdk5TsK|x>M;!Kt10Etngy z^ZA>!y&gEd9=Jaz84}8#CJy`ixn4P$;eUPyn)iloRhe#8_2*hy{r{+wGGdXZ)f@sz zsxknNYTwOR99A*qxU~Ue>*>wA;^bL=!c=GvV*>t41c%9!h0cn{cF|dwy}U487B$YL z81uVB$N#xVl~y0L^`l=`>439#rS)FvxxJV<^ieGZ>PrXGrBl5#l&RTA zgj4sh@cXA_HuHNjO$u|+odZyO&vY<`?MQ+6eu6Jsd=g}0EgWPB@>X*5yL;($xp?O) zSG{UeCEgNcwfEZ##Cf50EtQTf6LfIhi%sH@k3K+{g6MXPa819GgiN>>(w#Dnfi4g5 zRi*C@ym2u$jE}xWNz`6Fk}%U=3g2rsvK*dq_)zxF*wUVtrlKrP9ZHphrI)nLdj_wT;BX-OF0Q9ga&}8LuJYF9LL)_{iJkq4Nwh4YVRpU6nItzPD|Z{cgEIW$PT@m+e?*Hg5aV%A&^vGUk>kMlvISYp*; zfbI^y1s`@UI(%lKuA?6X{V)FX_dCBjAM2(q_>|_V`4hkH>8u9-sfXVd!g{_mQ+DlU z7Vq3MhEHF9_lPItK;=HMG3G))!s&thN{yModBFTU{XIp@|4#1V$Zsjs`o!fWkH5CR zq{p~0KYd+JC6x3vcRQHkJ6_2Cju#lPr>hb3VX5e^EoFsIu97KVWg=jDM-P(?Zt*rD ze!)XKj%qT;ho$fF^HW_Jva5SqLhA3+yDXT7=)Y8(*2$?lNM}Xed0zo%I|r+2D#>7; zslwKY#ETazGr^F8P4IINm%O+q)bb6BvoOh2?*{Q9>-5*5mBq$6AvS$!SzJc!)gXHC zhq5|C8(KWHI{c-nwpN=OD+dRhVIg<(P|eh*>nche9Ac5jDMP2PW){kOG(8v!{6Qie zP>nOSMC!QyN#@CLCxX^yB~x_!(J&2AsYPV&g*EU{S!r*v#DuNC*Ogl?FkrG4pj*PO z7Kk@#oan>(RB4(t)=$Hn40gO~Sp?B7-Vfo6W(r zujstwIS!Nrw2OZfMAofy^|6<#)4>pYSX2~+MnK8+U*o|VV>pdJSW*aNxF=aH`({rd5@Thatf5V=98L}Auw z4`ye$*G<-pHIZ|>d+V`|eDN>JRM#as#s2`*@V`Xw_xAS2%kP$2B1%Mu>_}J*DX<&pKCRWBDwph-aBH!CY=#^>DJPO|kXYE$_j1$AP_#~SN( zYmq!Bo_f2hOis^lgSpPW4-oCXjxc!(k&Zn*ZkgHVGp%IUM;2jqTwUyzQrv=NjctQvr9GZhhW$)N(wr|kMmad?K)ID8%&{NIruL&viMUMlb;j)5jF7HI z-l}5q%w1Se)ZS`BbNPGa8`k&nbUG$>xVJyQ_Cv;mC1TBl5M&)rTe`jBAZ)pec4R3K zUPf;H>^$73dEbdcNvFri<;dUpb6iCywAYA{PLZcbXz5htMu8&b!vRaxf+2Q8F42GT zxz2>9W?ltCSAqp>mlfo`eD`8g^Lr2#tpJrhsmpN(PxQ}>Brq;&kOdZ~1^!MU-m8GE`3>qb;_4SG;In$q0R9Z4C?%X%mj)bK&VCC{R1@ZzQL=1u3(+%YH~+I zuj{eaJF97%&&z2u&&Qn3lj+U9s!QbMWgsJs#pZCdVp zeW%go?L|1euNv#*cx}BKM#FZ9MKHnZEAQLvA0iUUkdf;=+`lgjk{ew1S=3(0pG-yt zamwc(ZkkuE261J|)`sa=4GBo%GFoKLx)KS{TahuFxl^(WC|hfg@E4$A)8=9!EjkC0 zd|Smg?|H9i7dX--h%dTtRXApR=~`g3;&M!iD3AWN*z@-9r~hB!*0eWYUQWq`T;o=$ zjH~4J;v#=V@&9ER4vM}=huxSVhw{i?-?U8uL;x@B3SN&tNBse~&$9EtzA9X-J&G|n z>~GGI|APE~JYFk$|07|EHYdb&kFzjhxM?sHvz}kG+;7KyzoOn3n%};ZTGd(b5I_ODkUaDI*gYN`Z0Z)r7UC z_SA)5IyRjxbFNcSE$qBq%=_!05`ahltycf2s*)WP-&+m<2l}}ggqLgw2))+Exv(_X zBO$4B*w6M{Xl4Yr966qZvl8gQs)9C`$^YB zv&v6R;OmyQIi}K>dfY(eojUPZF8a_-e@RQ6wz%iPLzi#Z*M%}TnK!kf*iuKf=#92s~KwN}(a46ewD zs8u#JE}@FW?`CSbW%hNpYKnlVe`}@`9(>(JPUbWuM-~;EfCO^)kpBx(|4OcEgKDrM zP+cA=Wk7>*UC@{N5ijp1(m>uSKF@En%tay-Zp9xe4J|jZP7ew9GP3ZQ29UQ~Y1vFG z0AO+45jW%~A^8zOGg~ zH38b9-;QGw6Tcx|#O5n6uQqFwt?t23i8=}I3% zIaaZ9rpa+dT|fRy>RR3EC4nCVLO>I8$QHremtFH>D$#v^Tj;Phzf%H5H04PnK&j9< zyTcePt+zz(9QXLP6lq!l(A`3%F{`+9_{=C7F$9^AO$$mTqY{O4F<0H1uDTzl!;nLa| zo-k(TIY*(9bx;E5Z7tz;>z#R5aYhxZHCBCqij`8s)*IGg{>dc~&D0JAv%58DrkV{= z(~oeu%0M6FV$niV5Y0_Sq@OusS}t~4Prv|{4%|HvTxekfiOYQGM?#=h%=aPr4N zSQDibbJ!hRm-p8U)sK_!ztsNoLHJ$5vMCtTDsL)u`@8^wLy#a3`(=$I72_afpY`-y z&2l3{ZIt~biyP^$*w{Q@RM#l*WY5Yv$T z6lJejNPXN@OGiO*Ql$MEM312IzSRDydg<+9j-%wed)9i3 zvs0SdRg*&O4upiA<07JJ-6X{Q5ydfd#uj(L>t&W_b4EL*?{c88(Y$fxTB+Kmgu4Kv z9F9aGOkR4cv%1D73@DQF#$L|n2_%uklvVj2!KC6BsGQ+-FAJ_lRv%=3z+*hTK8U@g0S)@rv3~FIgf-X%>oMV@mu};gxi4bkdqq?9~ zjrE-g@%4jQ#mR9#%@DpMV5ZXk;Kywe#$q1tNY86-AMwN6xo__>^nA3xE^O1|ndk{> z3odMz7*`M=Acn)3tey5v`gP{RB!T4yA&t zzSg7$Nh7Ov2(xo+4Xc)!6eF;WRhBSp-eGe6UmBN!qWc;L z{3ljy{#tTP|EcMk6~p_%ikV!QIh5H!VM42N^PC0uzv0CSy3!XMGxP)zF5}z1JW-Qc z`bxdI>$Miz%Pj8ngCAB(B3Q4&XX(PcHUd|y41AbW#tyZC3XCpw{zgry2%`q%lS0(G zxEAfDi9{$AArWlv!RJU0JT@i8f$qIY3#zJIV&L}-vuD~#JaiG~`B`FG*Yr99);9&W zOPIifXb*+%`NM{WtTp+~{V}zb=LL)K+y02W5(HK4$5zzE4C5B_k zs_R)$wj#>d9c!D_JXt~^!N;9;*U7Lol8mg?;-Tw$JG_|anMzA<3 zd-|0g^PHqby{XkytZ`}HCa_+bfuc<_DD>PiWqof_xm8Wo=)QV2*<$8-^wc&IKLI$BQ;rS{{NCMt0Sa{~Pk83KD? zA@YOXptaeDGd$bm$7@V7x3JERUSFFM;U=z9V(xk^CSD0c(W)#(&v)rRs5Dpo0jPz( z>Sbp6`gWT|v|K|EH*5uS#3)`B6G>-sIWqMPEaGHMmIx|bWQNDb+P9~F07~l<5yD0vn^IlGpO=BY)wssCJEk#nhY3hOH%MQodns#5!-v zvSaz)eQ9C^@dEiU(8&`NQuGjFk;-G2CuuO(SWEBdhA?6dkF29R(B9f)(SUD=QR9Z0 zx?6_ss6ItWS+~(2AHAzhA7`ya!!84F7toCej{YDQM$tNRfV z+dh@aiQ;g@DvcVUW#k4S5q33(#po=LaN7BPP_4K$vG=s%N80bD`?yqKBRjA0b!Qj2 zWbUg27+JFEt4yR}!+R(iqWoT5R#A5??r=mhX}~3z53+G!zhc}W-Jp(bxo=SlG#FLx z{bZRH65DciAs&g3Ut_hxJUC{|W}MiQmlJZJ)P+s$^rbQ_~|t-YW%Q( z^aN(PuO_&eowSXKdF`Sqg7sZISpMTAmyo_Wz{O(6p1-=Jif~p6qyLWr!e8drzeI&s z=1cfF?ax2=Jh!+ET>tZ9qi0+KSWJ9C?4PnziyOXJ2CiBZ{N_x1tt7E3D(QYdT})vN zvv(-Iwp(Vp){=0Hy`IH{FN)4i=UH=RAsw4YU;``2kdj{ z6&e5geb>AoE-jedtD^q2ln_4ZjyU{Gr+~7(m$TgV9%*Qt+P|9enNC#ko;6cxsu%^V z;H-i0#O=tYVqT2G_YXLRl|rzXuVyL?6=Aznw(BcR9&Tt3IBK>&ixvYtPO z|8Cms$t3Yn$0`aIBZ%c$TdSqGz9vczqlsFg-zmfuFWLS`7Q}lBk6K)0qrB-<+j|gy zo=8m0hZk)GWQ?cuh;y!blIuk_=b^%m&`B%>idYmM*ml=gsM6=gmcB;;3aCbQL-y5i z0T0wPs7HpyT}=1TXF{n$D601u4Ku)Fn&}$OlH=n&xMFr*ee@3+<4j4N>h_1}CAE_8 zWcGlFm@7LRVSLUoG^&OJm<=+}SlQXk=&s9hvqlGJ_z<)q&x)R(m zF`+TiP%%mgT5h2=DiAxr zmKL_sg+49&laJFqI_p7P=_HQP8$?Idp`U1>mJc)qGB_kLg1hcTQcQJxS!Q%z$_9ty zR#h%7ft~cW2SH`r!n%Wli268@)z$nV=4i~`%@yt~`+or3$^5cqYQ%9FE7;ZEX%bgD z%2D=%x;S|)+^2V%3$)Q9(H|D2S-Czjj3~C&(}C~v{gOorZ@{n>s2?|fXwp+C&xBf4 z1Rk$yV9bs`iSDBcHxce0nyBeXiu1MmxK?9M1tszeigl;2c`|!W4n6eVhy+!?C9auB ztpu1^smKCDNgv`&p9LgB@d{?b;tucfD}L;s~fpr*R-BZ_HN`C3(xZJ(yG zqN=LODZf0AQNK0iJ!>y#RKOi5N#eehS(|K=am}l_6Mwd2DJS3k;p)%%(9(LVd}REx zEET?vLmswos~(5R?k|qtQooPIY^U~-U7+mC*CJx;1}^ftTJCFzYid5(bn8509o2Vs zjN)G3sL&5p3C&*tsaABhnw3j(q61VJ=%}2_`6ZK^R_nTw7;qIom2D^2mx$-=QZ&iJQfE;SI z(I&Ie>}u#M@sY*NqfKo_4<$O>j$O&)DMf>;+j(GW``W;cc3LC80PG!TgWF40W2{Mv z_dgyi34Y$IWc-48nSWDE2ZMbam^a$rXl6s)Yfi7Y6U`qd%^Rv~nvLSoHRTqHiD~+i z#8Tp-4Hl2Kw8Ood_d}C=rStu%*1dY5z{JE66BEJ2ynYL70mh{@aZ~;Q=#;&boMmDE z^E1=R*qc5IiuKIj^>TkZ)E^bUyXs+I8M!#a?8?-ltp%mOwqjnZgSiFqf+FBwC^`+u zUI|-hwNTP5odz&(Z$Na8#DweC6M(}QQdIWTs9mVj)2<@0g$bq}S&{zU4ief){N_a1FP+4VV zH}Mu;{f4rQ-EvX>n|IWS!qXZxuhcp)Fh-5tv42F5B8tRG&NhpH;DJ%u<^dv%ZKOp`&u%4KtX2D@{_9i1QJQA6QR>!r9IayPk<3fIr9CM@FD1-eioTU%GHEk(# z0!LU3!xoQAN5?|7M3NEAyY$ZIy-9Cd&^k4&EXP}xdr=ghfNE%L3)rkE zA8jp*jNUOO)Hx&KD#E8aM50m>rF;_~X%fNhq{Aq3fHL1F`ItWcO=wUW_RD3Q?ZAV| zmlzu;>pUNfb*1G)Csr_2pNh*&TS7EdI>C(z{9SAhwf&ND_NAJ>K6~JgIHnlt-UiJw zh;ou8bMjtfq8tpsoK{bn&J`u)5+o6%76hv7G1-2qW^Rf~@lp*hv>(P)xgi?yGqQ7A ztSu??(~KoxN$!>9W?}_v3HDlIyAp7cNZl{c&gj8&I%Nuqz|xjU3J2;k&KQ`{!kQHw z+25}2Um%I+*8*Cxn~O9KMVzJzMcmEVDh#p>_R~2$a?JQ?m4soXh~WaYKkY$s^V=9} zr--X$$Sm;&_2rL)MH%`+JK1=82Uu?;2*1R*kK;t{W@8ZjnF|!Dcmcen7lx4+yku7y z!PkOXwhSX$qd0@ZAi+O_GyRPK_jh^jzo1qp`F*|aBIC+?CAUmHIr+MmSox+i3)}>VG7P+3><@5w$ zsoh#fOIP!$te%M_dxIn?%%kK$!U(r4#<(EQW_6yp9MkQr7UmuA)#W6=QkM(onTu0W z6v?}3{V*x|0u>fSun-nGfFImGRj_G-iLs6-^F)ak6Bd!|@}~XmiKfHuPgG86XILA2 zovX3!F=ZNdBm+E=>qX(ihK+8V^8yf!0X(AX*4e)miB_677QzOZ9wXmV4<`T4Bwllh zfBJW0UZIp}r{&YOopLSxo`$CWG_Uw&o*M5y z&ixvN9;a&14$dSb!;${1WX}iN^V`|5wV~0k2W~uylB+_VtJ+@`haehTCX#fkQp<`L zt11-TwPCo4d5Mq7v85=`2MLHB*O$DU4pb0pQQ$IR6QWHtyj&ZuBu(XVBIn&)Hjvcv zcHS<(_2eYR?7g}`1yIUKN`97CAnE}w^k&j@P~W#R$foTv<_%E#&%5R9+Alt-j~*~r z3|+=`-to(+&$NKp-lqzD_Xhws@IczW57c zga@0Cx_zRr|Hp{a2ABVj&gi>9JO;{Em;v+OksW_i@y|y)fAkV-cdr_94RU_sQD>L< zI*Ak<*PfHD?5&`7*2&E*@Egvg?a4(ni(Z?83KVBG$ol&JJ>$t z_Sb7sUCrVhk|KF->}JsWwm2P!Nm`IfoNC+zqTJ zxAz{pPhdMNWt2O@q2?wyUU+s1A@-AdWrZ;$D!^GMM+!EDaW5*@r}1(4o{jC^bNx_1 z!&AQ)<(KI>DNmb|^z(e`ml<1m3&qEc9f~Le%T@GUyM&2ar(F03 zV!YY$*fUB$?3w4_#|Ht6O1b%TeLxBpD7q)btcqn3y_?ok6HPs?rWM=|;gXd;(~;nu zP>k^QC8c9K#c615B++$Gv{@P!6-K6MW(9XIj^jx@fC@Il+zQR6?{1Q)u9o+a`LX^1 z;Ai0LV#OiN<1@7!@_Vg_WN5{_&T3m{f_d24J!fgHkFnW-TZ0y!7b;?rbdY4Ik{UESN zC2KCg=9)e^J=N&WgbX!?fDX6~W}f#kAIyP-dhpk|AKiPtM5^D2Kd@O;EIY$>t}mX< z+OkA2e||X1?|QE({WZ@UnVN&Hpu51dM>Mmx?54KX+L7Kh$BkQzWbJRPm!h|sTm%`G zO(TwY*UR>gn?t(SDJb&(2A5Hg{b!;$YX14imB{q^YUZWZ=OZihQ)2p4#$bi1{KFsP zDo(?-EjA8T{ZqqIX?eMG`Z=5`>Vb$HhEfch4gNC{f_P5nu|jb4p{(2nU*v*j zR_t@WI$O^`Dmh+dZ?JSeRi1ScM+RfiE%G}6`7cYh^mO~(S?Rb=Dni_RYY|KVO+^&* zNpF+A4(Sa$G*`<-6-h)>&QsaEIf8qr03t{7pK03;XldEI>DCuGV~BeWt5#w=BD(4) zl;e$2;UC3zFzIUgKa&dPX1ss{4UTE(#}B69sZ4^{g-syP{>IB7+sygfHl61|XeJ#B!AyLYfv#6mh7KYyzu$K?DY|oNr z6xaa3GFwXlTw?&E9R{=fdoUda@Z|a^%ZaG55;hE*UQGxFi0j8@0)^s6ww* zvDHRB7)upLnlU+EylU3DKousnD1H03j1T|O?WDM)o<{fs6E^_&)yK!}ehPiNx*0USH zb9x8*@nziNJP-vQxGIx%>xBX0g@&|hS`ab_JSVt2@Qkl$YF|ChRyv@)jq!E~7f6o9 zTe|_4j2f;43ioc>LCg86sIp488Cc4{+ghi)?Sy99Pa@1wP77a1D`09wQ z0)e%dkQl*XNaojjo;56iC-wP^;zJ)kjBy3z{mTl3bOYE@MssM=f+QQnRL4fV(e z|F-Z0EJW4!6rEk6pI~Mu>>yz8yZaJ4JE(mFeZKqStxhS+XZWO4v-!dQ!CLh%6d9CX zuUrUj{!3+J;;Gz-W0ih66Tk#ZzM**XfZ974VMZ22#D*=KRdFF@v>CWq893=T`otUI z(+|Ha5k$WZ6L&_HKd~VHBFfUg&KW@L+cj%Q&rRRb>V-C#L+LdYHGS1Q9#Om3A6>-E$H2Z#VQ3j=iF1jgR2Ko~EH2BIm{gDyzEV&=Z=0bPwnCi3;o^;eQTdK^>O82TB>)tOb%T;|53GZ?iJ7=}Tj_ORw;7 z|5)X)^TOx8ccQMGGCM!FN^_c45X5g}rmoc&ozvwETFC^|{m~OLrX}{zQ3Z(bz~(yO zZTUhRt=RvqV%6?pgGHvv#{*(%1z+N#znjd)-)LQK|L(7fz~Pzqb>k~N?$O2d^~&0( z=BK?Z<8Q)&jO(Z-SL$OF#H%1CVMEVbMd;T#xzb-D0WWds4?gnD79w4JH}%!Tc*?(O zvj}fOu)GDzr0LD%D=Br-#Ecox}OJx?Q z3osM-{3L`PzATp@MYALpQCn7g^K)5F3DVw4lL`bYE2@gX!!xXes~BNdy?F0LxLs47 ztEYo6gc{MFRZy6l2M!G$k)n(~QAIU&A_X`!bvDMqHqo=6o9JW4WQyvMTG!JHewq;( z@))gr-%k8!`|-39@u&{F=iu%SzO3uZ=3aQm;eOaDgZMk7#D$qmT z@`|$>V&$T?MC$l+Ky{%@PKzglS$W>DsdLi5_hQ@WNxC1tZG*vq=UsZza|;tln&5Wa zunM9a<=)zA0$l&Ey<9xB4Yz+0nqXLEFRP${O&vpLMQCn)7vIAIZs5-41fFhe2U%ge zWfn-D*PG}(>;1Y38D`v!_JTMiZA{8FioEIzKSuyxSDJ}0KPFQ2Z4iH`Mt*X0n7i<; zchj)2AlrqpMhxa9g_!){pw##$xdCdb02B8w5#0BqBCaMR{?~-ilZ19KIKiUKEA`U2 z2v9)tto5nBUvDe$| zr&HK-@a2_-r*lW&KOLruEMvy3ez$mQ=7SS0HwR8TEd?~75a-Mk%|NgEHO3Q)F;{B*el2Pk3XAqHZ}; zEee=60G}xEq=&f-p3(&H8l|Dt%A4bNO}Tn6_4CYSQ3`Y(d#Oqk%A zPE1%FUp77NX)wNQCYkP)1o`RJp)W$cYhOnFn8G(W^t;A`_NoEh(_m^G>LHncYf{6% z)t~~1hx|8sjlbpXn69jcs?@H+r>JAH!D1SyOTv7vE%7R@%!lr8D(v3%H=LEM^-0Kk zSJud_gK&=$H>Ny(EFK{6lr0m{eSnj;>nq9Z;uHbqMw_Vx5@g-fgP|;A4DAt64u^a|eWAR*w%9pj z=)pa<_e8$@A(YP_!1>@tl%%)6%^SmYw|N*d(p-P9Yz_6xKwU2Oi>q7L;{`w5Qzr?w zCBWK=tF`5P`qO^>f%;V+SDiuL&3v4(2`|6D58-T;1m1LJwUpvj`R#BwH%c#};da7ky6T#gzln)z|AU`SbmX^)}03#pwXq>GZl zD6|6IFR271=LGqDLRG^%bomXr?z9gr)*`2!2vu$EvvdY3lP&1n=DFr5QA_Yt_Bjr@F$H!`kyrp&s85>#l?Kw{Nn~@sRsU~e+>I2f*GU8{Xth4{BZK5 zrM@;KMjjW;&M4-le4&PDZF-==DxaC%POnM16b$4m^VQv88^Eh0$)am>u%JD)CTcS= z39|ay`Dp--w)=1;8I}uP1Glr`(Nh>aKTH3py6A)T+Rmaqre)&8h&UH2@ODP!LGQ=x z$ct5n3W!}l?mBlap)FU4IF+iZ3!5lLLW?YtQ3Gq4n5Fes5n7{?XXR>K&jwJ zwFTMOi$GCzA#x%s2K&!Bc_#aV(;7K00XD{o>%22MHFq9dXvJNc1~%tnq*)di=f}7< z(@%_ua1MqP!MVC4V7b(yY@A(n#{7Pa1R1idJ->XZ{$l6BGR}w2W-vsib6mNPn9=Jv zoUPx!pLcR{7B-d5e*iY|NE8LV*@SA){oKTqe{fyjg`d9#mkvpTYBtoIlI0f_T;0t}(80Tc36A3jt*iJ|C} zm+O?ewfcGP&CHU0Ezi%PRpCnQ*gThNyf{wgw*J$2!qArn|rSu(vXR6yFv zkc27~T7$yr@RF;}zuThjsMaI*b)7T!YfNyIk$o@yq#SRN3CL7~`DpHr*y>vfTi!!` zfuBpMGLGVdW>eNG&q*{fv8>d^i5~DlS&Ta8o;~xoEh&~-Jbbtg>t${Eic4`K7|O)R z2_>AeGGuMDe;kBpURKlB0DCB zeXA-*5<}at$Kf=};^RR$1vn;+Z-z=Wo};#V=oTo7*#hsOHi>%T&vB{dH8Aav!M>1D z-`r{r^~oM;I_D^0d~UFNeXiLxbC(!Yv%k}j9fjDI39i6+1Mws2i3CIL^Nb%$d7T(; zWbEq$fA$5#EXOI7w)P+)Ld_y|lo{+KWqDMphtKH{FC)QAT~Vm`+=4~#7Dza;yEM9k z?ryWImvh&?j6pN=TXM*?L}|YUf~(%>t>K#rwRMBqB0wh^q0phFI2wp{S;nBNBqv_@ zcufj#{F*fa$?9>Mm{s`9+pkYPb8dW*Km5(DQ>}?Up0)s%5h4? zehyx#73=4!%l?1UybSH+$!B`i7$Dx7ZDVhdnb;2_njhwxPffDFUHSArzP6?zgNhHs z#2o+Lr|{+Y{$@NT^`Mppusnfn@Q?vV*#X4E!w z@j>SNmxt(F94$Lvj38!W=fnr*>r^@@mc>o=bk+*MuqNdft@2{$I-l-EOM=yAE!q0w z?3c`akVjjm9>k4>2KOaSTS{9DeVp&xr}NfW&pcLKiaTs4LRk%^fBlfUn0(5KuVu>) zmDfRYXlQ7ziYX^AB2(JPiQ3(zC8Bd>fH0>6IKp&sVWEl2cwE*!E|2MzxM9|3j(1Qx zDqbNfHC@P#Kyai7JrA}w>?J@t3g$+#Sjjx@^b3MS=9qP!XZUbX_jc_I^jRzq+2m5| zFMjqG8I`AOf8uOd=+y1R!NHxyI(s-o)2F~61`QJ-5o98w;PagiKX**HMGkmXXVpdf zw{+;ADsG%nzjM}qiUftgal=vnKjf@w!$y~E0~8HkNv;0@@YzG)crp=!W)0ABv1^1g zrLph`&>c6#AiOWv(xo)1%X(X90x&a-T1tARt7C74R6v~*_WXWOb~N9vyQ}@yJ{}&w zWruu*|+*`jKe2$sBC)JC1XJd)Sp7Mj^;`{_%rJ&)*W zin9js?$5f~gtRp<=kT0A04E*zJv@H^{M8KiJEx};+oK1^M`c8((UcrX@B2Oyt8~J2 z(+zqmah~a(;=y6;9kYSw?qIXcl$#CneXa)GPJ`Shp^Cv5o~SptyJ*)?omx$DC82V` zmxPbVC5VV+1+P6bi!Mp`D4NW(%*x?VP9irMYX>*Ao>Bu;5+iqe?SrLbHnt;)E?Jy7$J(-z0iy{{i{;_**^yq4INj4F6{0t4XM(R1 z-?)byYE;(NS}l&|Cg-HSS&?9phv&k%=bKQfK#T4KT4wCiTO?ryD@Xp@81{eReJObn zb@;&K=0D7$FZ2e+=&r1B9>>A>s{d+2`xI7ACx#KK2n+mn%}0G#|5yCa0u2NQ{|lp> zDO*aNX*(Jl8)3ikIV4^1Y>^jvJA*Eq0hd!^vcb3=nxZWsqwqZdfWmRX1>tlogPr-FuER6_g;vAFLlSJZReqXGsA-HP2~~-6 z4DVIOH=)B)?fX&W!yKfdnU!>)C(DPyfc4P@lk(x%p(*p|%Q^OU8z2x$5eN z%z!V&#_nqaoev+A%98d&VjOF@tjMeZ|)#paVC{8-J=(CC@t z28+P=f2$(zd0Lmg5`3!+&W)Sl|2E{vV8$p>fA=27_?RTp+tVoqf0vbX22Gt7!WIw` z|0Y(U3BwkV{OWSP10w3=tF@1%TpS5}ogPR|{wYqWI=tA+75yG)Aus^M9W&W}(sWp^ zt~ORLK~&YLuG<8$fWY^?U52aKZjMj0ePUx7=7M4Zyh{l zFSK9m`cUtaSaA`Q{g%NILos6WH@M8Mt4(O!?JO@=gIQ!Fo3E|OoBD3>m6Q|avZ|5- z=X|yL+d<}|lqb0%XYppRcCrh>cf4X1i^J#$2@es`=$F<3pv>r{1R*fvCbrffH z+~CU}esjM(;Qx$+=@g%aD#FOR?`}c==?>=#6RVKH=*FbQQW)KM;ql490L@~edt+hw`yez$F6xmA;KL%G`OXrYb0nFgGi{UdR5_mYo zi&?A#<>o6Eg?dM)c9Dfol2xfm)CNIcU1;vhiZmaB!NPM=))1nkSsj~ckN8#ai5i2T z9lnsJJsT}e<1UZ|MLP28Q**a>v|OL)jT1>Mo@tgx$qO&17*pm`)& z{fdQ9Sw${eYOwnfCf5*xzC`my@=S%_`DGKwp5ANJ8P7(3WTK^GE-qg8K51nE9bF>n z57lUZ&pH-4iX6m@f>aMi-5CI2H`TI}a;cB{PdaTaXA*-p>h8=h#935wKR19L$k_M3 zGy8lJ;wiz4e;rK*f8f_A` zF2KOW_nFNTYgcI^-F>YBHiiHiN}~~5mPv2C zyT~^_g(}wp3*+xz$Kb-c(F+m0-+XiS`Z!Ph`&BdTL-{YSoBx^rw+|&cj*99fhwnFh zNiJ?`Y2qqJX*gY~$e4?l(XMONV1|v8$k~~0V4(*)+s&vrX{F&OJ{G$%?N;Y0{ES{+HLI%Z`QG=ELAOI{+e;1(&`KT#m)}WG zFE2r@_y-_2h!^tFUu%D1S3mfnya2u$(~Inq|JB{K$1~mU@%13pBFil%a(Se%C=a>i zV4LKbORh1OA-+uf``K&1_v^Fw@ALh>-`~QLq8sJ-jWmTJyonqV3ieV}$iALpaDs7W zk3NA%Ktd6=z|za^itg%m8y#N6Ne#+PNl@Z=Kp#Emh5ek}_VWsszS4!h?$SH4Jed?D zLk8hxd`CD#KW%CP6VL1m`B1oP_egVK2C$?e?QGQp1`SP*LQUp@Bb_#7nrAfX6_m~- zv0SQhip!^%t(#H$ea3rz?6NJ*FP0XEY4*D?5hA_>4CBDH7>9$S$af^AH<2QqLmS>k z~ zV?FoOuWN{4vv-p0$ivY;&6Foy%du8(S|=eo{ZQNWxQQi1cCCE4)ikb^Th6ZM&8yHk zN7}sYr&S)cS7T>&cn2nBqphK~${xw*=GXg50%sOrJ&nDgXs91rmr}MxG_W{8xfAT& ztWsAK=tm-xzL(AquYmY|Cirx?6a3#;H2mAnhc)uN=%3B!TRC!-Qh5`5qB|b{@!#Ul z_$ONRV1EE~1vSLa@w^~KcEW9q#59+&S2)BOSz)KqK-2lUx%PHPWd*%e^?x z7iJvSXnX~9ywkMkeP)bTkMZi&t+XQXOosjVS*;ldIitL^Tt`&rudU`Do4IP_#6z!`!7ir^9;?7)eSJt~=^3oM$m2h1J+Kf4>I zP%`7yes%wQ|b;Xrdj@|uu3p6f2W z(`AFP5=$$fK0c~6ft!BlI~d%Jh<($KWBJ)eXizdvWw*?_0#Zfr3ANX@m)8R^{cOnD z0=V68^6WMc?HkE30RRVlNB}dGVCWtne*oYR$JgwJ7yKufchKnXMJSp5#`k^tOVnxJ z??G%z>?4;10BSeJKWPeD9R~1QQvsK9*E8ED#m8>eH3CQj+HU}~N18S)9V?ll2-oc@^l1o>2`CbC++N)4z{fcG`{<>J+}zAKAZYd}z= z$k7%YmXV{Y#^wv#)pqp8KmhY~ZUo1X#g&uyvLig}G3^+X7!DhFSAOwXygao31Llm}CF@^w-whecDMUKzKcUOqcp;fR#2LFxpek_9;`9DXcevGtH zry;xXY&_^m+-bQ|#{z_^lBGg;w z5sLWwrmG(D-WJ*9XriATIx;!SU0$@|&f{R($=46PO!p<2bsssECPHJ8B*CS{rdKPj$Ys?Y_K?_HK{|bi`mFb zJwlf$>#gnXc|i4l3cKop92Og}^9(&__I9Avf-1Akr_;?}-(MD%c~eyn+t;5Pl5dgp zWy57-2QicV@w1bgoAh=?82=cJ?rOTF8rV}-DzA6db3b0UW&E~QMxCjBN~iws3; z_g{9jhw~~w;ilk6uH@z)160EKl~}#AWDD;8n;3{cG8!dw+nM2YVYIGn;k4M_NmqrG zxs5FJ;UJOpZ?SA9Eyf&44D)+O|DjNa{L}Vv((R1Xv(O2+JA_-PVtM4V;khY{OBtQ% zW+S;+)T#Zc5sjWH&SzZk`3Zs+G9(Ywq6KbQi|k<48nGZYX&70Xs1$84$5kXMYU#DB zV5GgZbFp#eJch_g#NIJXowbB4Y!Bm+@j{qPStUVQqcH8D;+W6;C8dUupm47c|Jn#w zDSUCTr!A!I9X>WRltk4i6o~%BqP`MZ$U$zLdf*C(`W$M_D635W$kMlUs#frT23>nt zza4qkep5}_mn^3@3*HD<)Vbek%C{uo4Z%MQKE=f@m(AzE_GL}8bY9erUb<^40_(0VjxvQ`fOm_8bFK{%c$fx=H?;%V$Tinw zIk~yIx^`|LBAeOG;#>DbqL$*^ffWkTCR%4@Ysf40f?dIs^&SNQSQxrx!2=uB%|xD#gg7XLh5ltfu@_|UuJ$mg>U7jTrCbuJ z>R(E#cpF^h=WA7JT~@ss9o*#LD3!aEFQ3PQ3RSI)em2fpel>!{_BB9`dTNmhS2xo* h$trx!oA9rR6WBnrS`PUBJ4-<}RPC4BKt8zg;&-XsX08AL diff --git a/docs/images/script-editor.jpg b/docs/images/script-editor.jpg deleted file mode 100644 index 8931e1cb408abc094cf53b839210db4c357fdd8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60108 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

H=@&en+AN<4r79l#Q$qskN;IW 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 L`uSet--iAd@&e>{ diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 0c9d168..0000000 --- a/docs/index.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - - -AI D&D — an AI Dungeon-style storytelling engine - - - - - - - - - - - - - - -

- -
-
⚔ Interactive fiction, refereed
-

AI D&D

-

Open-ended adventures narrated by an LLM — with an engine that keeps the numbers honest.

-

Create a world, play it in second person, and let the model improvise the story while a Python - referee enforces what's actually true: hit points, an ally's trust, a raised alarm, a quest milestone. - Bring your own model, or play the demo with none.

- -
-

No sign-up, no API key. Hosted on a free tier that sleeps — the first load takes ~30–60s to wake.

- -
- The play screen with the world-state rail open, showing HP, mana, an NPC's trust and a raised alarm flag -
The left rail is live world state. The model proposes what changed this turn; the engine decides - what sticks, and the chip under the narration reports the result. The ‹ 2/2 › - under a turn steps between the takes it has.
-
-
- -
-

What makes it more than a chat wrapper

-

Four things a plain "talk to a model" app doesn't do.

- -
-
-

The story is a tree

-

Any turn can hold more than one take. Stepping between them is free — the story below simply - empties, and the server is told nothing. Writing below a take that isn't the live one is what makes a - branch, and a branch stores no turns of its own: it records where it left its parent and borrows - everything above that. Twenty forks cost 1.007× the page load of the same story flat. Switch lines and - the world state, the script scoreboard and the cooldown clocks all come back to what that line left.

- The branch map: one horizontal lane per line of the story, each joined to its parent by an elbow at the moment it forked -
- -
-

The AI proposes, Python referees

-

A scenario declares stats, flags, milestones and a named cast. Each turn the model appends the changes - it thinks happened — and the engine clamps them to range, enforces per-turn caps and cooldowns, keeps - counters monotonic and milestones sticky, then strips the machine-readable block out of the prose. - Word-labelled bands (40–60: minor damage) are what make the model reliable at it. - No dice, no scripting required.

- The scenario editor showing NPC stats with ranges, per-turn caps, cooldowns and labelled bands -
- -
-

You can see the entire prompt

-

Every turn stores exactly what was sent to the model. Open Insights on any action to see each context - component, what it cost in tokens, and why it was there — including which trigger word pulled in each - story card and the similarity score behind each retrieved memory.

- The Insights panel showing the assembled prompt broken into components with token counts -
- -
-

Real AI Dungeon scripts run

-

The three familiar hooks — onInput, onModelContext, onOutput — - with shared persistent state and a worldEntries API, executed in an embedded - quickjs sandbox. Scripts written for AI Dungeon import and work, and there's a CodeMirror editor in the app.

- The in-app script editor showing an input hook written in JavaScript -
- -
-

Memory that survives a long story

-

The modern AI Dungeon memory system: AI-generated memories every few actions, a running story summary, - and embedding-based retrieval that pulls an old-but-relevant fact back into context when it matters. - Undo and retry roll the world state back to a per-action snapshot rather than only rewriting the text. - Every story stays where you left it, and the home page opens on its most recent line.

- The home page, showing stories in progress alongside scenarios to start from -
-
-
- -
-

How a turn works

-

Player input goes through the script pipeline, into a token-budgeted context, out to whichever - model you configured, and back through the referee.

-
player input
-  → onInput script modifier
-  → assemble context:  [narrator prompt] + [world state + stat guide] + [AI instructions]
-                       + [plot essentials] + [story summary] + [retrieved memories]
-                       + [triggered story cards] + [history along this branch, token-budgeted]
-                       + [author's note] + [player action]
-  → onModelContext script modifier
-  → snapshot context (Insights)
-  → provider adapter → AI (streamed)
-  → extract + referee the world-state delta block, strip it from the prose
-  → onOutput script modifier
-  → store & render
- -
- FastAPI - SQLAlchemy - React + Vite - Postgres / SQLite - quickjs sandbox - Server-sent events - Docker - Any OpenAI-compatible endpoint -
-
- -
-

Engineering notes

-

The parts that were measured rather than guessed at.

- -
-
189×
less database egress per adventure load
-
440
backend tests, run by CI on every push
-
64
schema migrations, applied in order on boot
-
$0
to run it locally against Ollama
-
- -
    -
  • Database egress, cut ~189×. Every adventure load was pulling the entire assembled - prompt — about 74 KB per turn — just to read two small fields off it. Moving those into their own columns - and deferring the heavy ones took one load from 38.5 MB to 0.20 MB. A test hooks into SQLAlchemy's cursor - events and fails if a bulk load ever names those columns again.
  • -
  • Turn cost, made flat. Assembling a turn walked the whole story, so it grew with story - length — 839 KB of reads by turn 200. History is now served as tails and slices from SQL: the same turn - costs 129 KB and stops growing at around turn 50.
  • -
  • Branching that costs 103 bytes. A branch stores where it left its parent and borrows - every turn above that, so nothing is copied on a fork. A 40-turn story forked twenty times loads in - 31,652 B against 31,433 B for the same story flat — 1.007×. Reads stay cheap because the ancestry is - windowed the way the history is: the number of SQL clauses is bounded by the context window, not by how - many times the story has forked.
  • -
  • A shared demo key that can't be drained. The hosted demo funds a model for visitors, so - model selection is pinned server-side with a structural backstop that raises if any code path tries to - resolve a model outside the allowed set — plus a daily per-visitor turn cap.
  • -
-
- - - -
- - diff --git a/docs/self-review.md b/docs/self-review.md deleted file mode 100644 index 2e09a02..0000000 --- a/docs/self-review.md +++ /dev/null @@ -1,156 +0,0 @@ -# Self-review log - -**Every correctness bug on this page is resolved.** 20 are fixed, and 1 is intentionally -skipped, with the reasoning recorded below. The record is kept because the reasoning outlives -the verdicts, and several of these are traps worth remembering. The *cleanup backlog* at the -bottom is a deliberately open list of non-bugs (reuse, simplification, efficiency), not -outstanding defects. - -Original review: 2026-07-05, whole project in scope (no git history at the time). -Status key: `fixed` means applied, `skipped` means intentionally not fixed, and `pending` means -outstanding (none remain). - -**2026-07-06 update (branch `bugfix-code-review`):** every finding was re-verified against -current code. #1, #2, #3, #4, #6, and #12 had already been fixed in earlier sessions, so their -statuses were stale. #5, #7, #8, #9, #10, #13, #14 (backend) and #15 through #21 (frontend) -were fixed in this pass. #11 is skipped: real AI Dungeon's `addStoryCard` also returns the new -card's index (0-falsy included) per the official scripting guidebook, so changing it would -break compatibility. This is documented in `engine.py`'s prelude instead. The cleanup backlog -(R/S/E/A items) below remains open by design. - -## Correctness bugs - -### 1. [fixed] seed_demo.py doesn't stamp schema version → server crashes on next start -- `backend/seed_demo.py:10` -- A fresh DB created via `Base.metadata.create_all` leaves `PRAGMA user_version` at 0. The next server start sees the tables exist and replays every ALTER TABLE migration, causing a `duplicate column name` crash. -- Fix: stamp user_version to latest after create_all (reuse migrations.bootstrap logic). - -### 2. [fixed] Turn-lock race: two simultaneous turns can run on the same adventure -- `backend/app/routers/adventures.py:302` -- `ensure_not_generating()` runs in the route handler, but `_active_turns.add()` only happens when the StreamingResponse generator is first iterated. Double-clicking Continue sends both requests past the 409 check, producing duplicate Action.index rows and interleaved generations. -- Fix: atomically test-and-set the lock in the request phase, release in the stream's `finally`. - -### 3. [fixed] Migration 10 renumbers indexes with a correlated subquery on the table being updated -- `backend/app/migrations.py:34` -- SQLite may evaluate the subquery against partially updated rows, so duplicate indexes can survive the "repair". -- Fix: compute new indexes in Python (SELECT ordered, then UPDATE per row). - -### 4. [fixed] SQLite foreign keys never enabled → CASCADE/SET NULL clauses are dead -- `backend/app/database.py:8` -- Deleting a Script leaves orphaned `scenario_scripts` rows. SQLite rowid reuse can then attach a future script to an old scenario. -- Fix: `PRAGMA foreign_keys=ON` via engine connect event. - -### 5. [fixed] Provider generate() silently yields nothing for non-SSE 200 responses -- `backend/app/providers/openai_compatible.py:84` -- A server that ignores `stream=true` and returns plain JSON produces no `data:` lines, so the result is an empty AI action with no error. -- Fix: buffer the non-SSE body and fall back to parsing it as a single JSON completion. - -### 6. [fixed] Fire-and-forget asyncio task can be GC'd mid-run and wedge the memory bank -- `backend/app/memorybank.py:146` -- The result of `asyncio.create_task` is not referenced, so the task can vanish silently and leave an adventure ID stuck in `_running`. -- Fix: keep strong refs in a set, discard in done-callback. - -### 7. [fixed] Memory cursors are list positions but Memory.source_start/end are Action.index values -- `backend/app/memorybank.py:213` -- After any action deletion, indexes keep gaps and positions shift, so summarization skips or duplicates blocks and `_update_story_summary` folds the wrong memories. -- Fix: use one space consistently. Track cursors by Action.index (position-independent), or renumber on delete. - -### 8. [fixed] Pinned memories don't count toward memory_top_k cap -- `backend/app/memorybank.py:120` -- 6 pinned plus top_k=5 injects 11 memories, blowing the token budget. -- Fix: fill with unpinned only up to `top_k - len(pinned)` (min 0). - -### 9. [fixed] Embedding-model change → cosine() zips different-dimension vectors silently -- `backend/app/memorybank.py:69` -- Old 768-dimension embeddings scored against a new 1536-dimension query produce garbage similarity with no error, and are never re-embedded. -- Fix: return 0.0 on length mismatch (and ideally clear stale embeddings so _embed_pending redoes them). - -### 10. [fixed] MAX_STORY_CARDS cap is a no-op for cards created in one hook -- `backend/app/scripting/pipeline.py:59` -- `len(existing) + len(seen_ids) < MAX...` never counts newly added cards, since seen_ids is a subset of existing. A script can insert an unbounded number of cards in one turn. -- Fix: count inserts made during the loop. - -### 11. [skipped] addStoryCard returns 0 (falsy) for the first card, indistinguishable from `false` rejection -- `backend/app/scripting/engine.py:39` -- `if (!addStoryCard(...))` misfires when the card list was empty. -- Fix: return `storyCards.length` (1-based, always truthy) or `true`; document. - -### 12. [fixed] scenario_id=0 truthiness bug in create_story_card -- `backend/app/routers/story_cards.py:28` -- `scenario_id or ...` picks the wrong owner when id is 0. Use `is not None`. - -### 13. [fixed] test_connection 500s on non-dict JSON from /models -- `backend/app/routers/settings.py:54` -- Only ValueError is caught. `data.get`/`m.get` on non-dict input raises AttributeError, producing a 500 instead of `{ok:false}`. -- Fix: catch (ValueError, AttributeError, TypeError) or validate shapes. - -### 14. [fixed] AI Dungeon exports with `worldInformation` key lose all story cards silently -- `backend/app/routers/scenarios.py:133` -- Import reads only `storyCards`/`worldInfo`. `worldInformation` is in _IGNORED_KEYS, so it is dropped without being reported. -- Fix: accept `worldInformation` as a card source too. - -### 15. [fixed] Shared debounce timer loses edits (Play PlotPanel) -- `frontend/src/pages/Play.jsx:28` -- One `saveTimer` is shared by all plot fields and story-card saves. Editing a second field within 600ms cancels the first pending PATCH, causing silent data loss. -- Fix: per-key timers (e.g. a Map keyed by field/card id). - -### 16. [fixed] Same shared-debounce data loss in ScenarioEditor -- `frontend/src/pages/ScenarioEditor.jsx:22` -- Same fix as #15. - -### 17. [fixed] Continue button silently discards typed input text -- `frontend/src/pages/Play.jsx:469` -- Clicking Continue with text in the box sends type 'continue' (the backend ignores the text) and clears the input. -- Fix: don't clear input on continue (or treat non-empty input as a normal send). - -### 18. [fixed] retry() optimistically deletes last AI action with no rollback on failure -- `frontend/src/pages/Play.jsx:475` -- A failed retry (409 or network error) leaves the UI missing an action that still exists server-side. -- Fix: restore the removed action in the catch path (or only remove on first stream event). - -### 19. [fixed] Settings test()/save() have no error handling → stuck on "Testing…" -- `frontend/src/pages/Settings.jsx:66` -- A rejection leaves `{pending:true}` forever and produces an unhandled rejection. -- Fix: try/catch → setTestResult({ok:false, error:msg}). - -### 20. [fixed] InsightsPanel race: slow earlier request overwrites newer report -- `frontend/src/pages/Play.jsx:302` -- There is no staleness guard, so a slow getAdventureContext call can overwrite a newer action snapshot. -- Fix: track a request id or cancelled flag in the effect. - -### 21. [fixed] extractPlaceholders ignores ${...} in story-card trigger keys -- `frontend/src/pages/Scenarios.jsx:50` -- The backend fills placeholders in card.keys, but the modal never prompts for those names, so a literal `${hero}` key never matches. -- Fix: also scan card.keys when collecting placeholder names. - -## Cleanup backlog (reuse / simplification / efficiency / altitude — not bugs, apply later) - -- **R1** ~~three copies of the debounced-autosave handler.~~ - **Half applied in phase 17 (2026-08).** All three use - `frontend/src/hooks/useDebouncedSave.js`. A shared StoryCardList component is - still open. -- **R2** `backend/seed_demo.py:228`: re-implements create_adventure. Call the router logic instead. -- **R3** `backend/app/providers/openai_compatible.py:122`: complete() duplicates _request()'s body building. Add a `stream` param to _request(). -- **R4** ~~six copies of child-resource get, owner-check, and 404.~~ - **Applied in phase 17 (2026-08).** All 32 handlers take the `current_adventure` - dependency from `routers/adventures/deps.py`. -- **R5** `frontend/src/api.js:26`: streamSSE duplicates request()'s error extraction. Extract `throwIfNotOk(resp)`. -- **S1** ~~`Settings.stream` is dead state (never read).~~ - **Applied in phase 17 (2026-08).** The column, both schema fields, and migration 65 - drop it. `R4` went with it: the ownership check is the `current_adventure` dependency. -- **S2** `frontend/src/pages/Play.jsx:6`: MODES and PLAYER_TYPES are identical constants; lastIsAi/canUndo are computed twice. -- **E1** `backend/app/context/builder.py:119`: joins and tokenizes the entire adventure history every turn for the trigger window. Walk reversed(actions) until budget instead. -- **E2** `backend/app/scripting/pipeline.py:77`: rebuilds full history dicts, JSON, and a blocking commit per script per hook. Build once per hook, slice to HISTORY_WINDOW first, and commit once. -- **E3** `frontend/src/pages/Play.jsx:561`: every SSE chunk re-renders all action rows. Isolate streaming text in a child component or React.memo rows. -- **E4** `backend/app/context/builder.py:48`: Section.tokens is uncached, so the whole context gets tokenized 2 to 3 times per turn. Cache counts and sum sections. -- **E5** `frontend/src/pages/Play.jsx:490`: the keydown effect has no dependency array, so the listener is re-registered every render. -- **E6** `backend/app/memorybank.py:182`: catch-up summarization awaits blocks sequentially. Gather independent blocks instead. -- **A1** ~~no UniqueConstraint('adventure_id','index'); index allocation is ad-hoc per writer.~~ - **Overtaken by phase 14 (2026-08).** `index` is a legacy column that nothing reads: ordering - is `(branch_id, depth)` now, allocated in one place (`tree.place_action`). The column is kept - unread for one release and then dropped, so a constraint on it would apply to a column that - no longer does anything. -- **A2** `backend/app/providers/openai_compatible.py:45`: CHAT_CONTINUE_HINT is appended below the budgeting layer. Assemble prompts in the context builder instead. -- **A3** `backend/app/routers/adventures.py:390`: import endpoints hand-coerce raw dicts. Use a Pydantic bundle schema. -- **A4** `backend/app/routers/adventures.py:207`: onModelContext flattens (system, story) and ships everything as user content if modified. Pass structure through the hook instead. -- **A5** `frontend/src/pages/Home.jsx:87`: the client appends 'Z' to naive datetimes. Emit ISO-8601 with an offset from the API instead. diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index be50862..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the Oxlint configuration - -If you are developing a production application, use TypeScript with type-aware lint rules enabled. See the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for how to integrate TypeScript and Oxlint's TypeScript-related rules in your project. diff --git a/frontend/src/pages/Play/index.jsx b/frontend/src/pages/Play/index.jsx index 0eff45e..4c29811 100644 --- a/frontend/src/pages/Play/index.jsx +++ b/frontend/src/pages/Play/index.jsx @@ -5,8 +5,8 @@ * imports below. * * The page component still owns all of the session state. Lifting it into a - * `usePlaySession` hook waits for Stage 5 of `plan/17-refactor.md`, which adds - * a frontend test runner. Moving eighteen `useState` calls and seven + * `usePlaySession` hook waits for the frontend test runner M8 adds + * (`planning/BUILD-MILESTONES.md`). Moving eighteen `useState` calls and seven * `useEffect` calls is a rewrite rather than a move, and nothing would catch a * mistake in it today. */ diff --git a/plan/00-OVERVIEW.md b/plan/00-OVERVIEW.md deleted file mode 100644 index e02e8b3..0000000 --- a/plan/00-OVERVIEW.md +++ /dev/null @@ -1,122 +0,0 @@ -# AI D&D — Local AI Dungeon Clone: Plan Overview - -> **[STATUS.md](STATUS.md) — where things stand and what to pick up next.** Read that -> first; this file is the shape of the project, not its current state. - -A locally hosted web app replicating AI Dungeon's core experience: scenarios, adventures, -AI-driven storytelling, AI Dungeon-style memory/context management, JavaScript scripting -(compatible with real AI Dungeon scripts), and full transparency into what is sent to the AI. - -## Confirmed decisions - -| Area | Decision | -|---|---| -| Backend | Python — FastAPI + SQLAlchemy + SQLite (single-user, local) | -| Frontend | React SPA (Vite), dark AI Dungeon-like theme | -| AI provider | Provider-agnostic adapter layer; first adapter: **OpenAI-compatible** (`/v1/chat/completions`) — covers Ollama, LM Studio, OpenAI, OpenRouter, vLLM, Groq. Endpoint URL, API key, model name all configurable at runtime. | -| Scripting | **JavaScript, AI Dungeon-compatible** (`onInput` / `onModelContext` / `onOutput` modifiers, shared `state`, `worldEntries` API) via an embedded JS engine (quickjs / py-mini-racer). Real AI Dungeon scripts should import and run. | -| Import/export | AI Dungeon-compatible formats for scripts and scenarios; JSON export/import for everything. | - -## Architecture at a glance - -``` -frontend/ React + Vite SPA ──HTTP/SSE──► backend/ FastAPI - ├─ routers/ (scenarios, adventures, actions, scripts, settings, insights) - ├─ models/ (SQLAlchemy: Scenario, Adventure, Action, StoryCard, Script, Settings) - ├─ context/ (prompt assembly: memory, author's note, world info, history budget) - ├─ scripting/ (JS sandbox, AI Dungeon API surface, per-adventure state) - ├─ providers/ (base adapter + openai_compatible.py; streaming) - └─ data.db (SQLite) -``` - -## Core domain model - -- **Scenario** — template: title, description, opening prompt (with `${placeholders}`), memory, - author's note, story cards (world info), attached scripts, tags. -- **Adventure** — a playthrough created from a scenario (or blank). Owns its own copy of memory, - author's note, story cards, script state, and the action list. -- **Action** — one entry in the story: type (`do` / `say` / `story` / `continue` / AI output), - text, timestamp, plus the **context snapshot** (exact prompt sent to the AI) for Insights. -- **Story Card / World Info** — keys (comma-separated keywords), entry text, optional type/notes. - Injected into context only when a key matches recent story text. -- **Script** — JS source per hook (input / context / output modifier), attachable to scenarios; - copied into adventures with persistent `state`. - -## The turn pipeline (heart of the app) - -``` -player input - → onInput script modifier - → store player action - → assemble context: [AI instructions] + [plot essentials] + [story summary] - + [triggered story cards ("World Lore:")] + [story history, token-budgeted] - + [author's note inserted N lines from the end] + [player action] - → onModelContext script modifier - → snapshot context (Insights) - → provider adapter → AI (streamed) - → onOutput script modifier - → store AI action → render -``` - -## Phases - -1. **[Phase 1 — Foundation](01-phase-foundation.md)**: repo scaffold, FastAPI + SQLite models, - React shell, scenario/adventure CRUD, settings (endpoint config). -2. **[Phase 2 — Play loop + AI](02-phase-play-loop.md)**: provider adapter with streaming, - Do/Say/Story/Continue, Retry/Undo/Edit, the adventure play screen. -3. **[Phase 3 — Context engine + Insights](03-phase-context-insights.md)**: memory, author's note, - story cards with keyword triggering, token budgeting, per-turn prompt snapshots + Insights UI. -4. **[Phase 4 — Scripting](04-phase-scripting.md)**: embedded JS sandbox, AI Dungeon scripting API, - script editor, script + scenario import/export (AI Dungeon-compatible). -5. **[Phase 5 — Polish](05-phase-polish.md)**: AI Dungeon-like theming pass, placeholders on - scenario start, adventure export/import, quality-of-life and hardening. -6. **[Phase 6 — Auto Summarization + Memory Bank](06-phase-memory-bank.md)** *(optional)*: - modern AI Dungeon memory system — AI-generated memories every 6 actions, Story Summary every - 15, embedding-based retrieval of relevant memories into context. - -Each phase ends with the app runnable and testable end-to-end. - -## Public release (phases 7–10) - -Goal: public GitHub repo + hosted multi-user deployment, linkable from resume/website. - -| Area | Decision | -|---|---| -| License | MIT | -| Auth | Email + password, **optional** — guest sessions play instantly, register to keep data | -| Signup | Open (rate-limited) | -| LLM keys | BYOK per user + shared server-funded demo key (capped, details TBD in Phase 8) | -| Database (hosted) | **TBD — ask at start of Phase 9** (SQLite-on-disk vs Postgres) | -| Hosting | Render (tier TBD in Phase 10) | -| Domain | Platform URL (custom domain later, optional) | -| README media | Deferred — text-only first, screenshots/GIF in a later pass | - -Open questions are recorded at the top of each phase file under "Ask before implementing". - -7. **[Phase 7 — Public repo & portability](07-phase-public-repo.md)**: MIT license, portfolio - README, Dockerfile + compose, cross-platform run instructions, publish to GitHub. -8. **[Phase 8 — Optional accounts & multi-user](08-phase-accounts.md)** *(the big one)*: - guest-first sessions, optional email+password upgrade, per-user data scoping across all - routers/tables, per-user encrypted BYOK settings, shared demo key with caps. -9. **[Phase 9 — Production hardening](09-phase-hardening.md)**: env-var config, quickjs - time/memory limits, rate limiting, size/row caps, locked-down debug surface, production - serving, database decision. -10. **[Phase 10 — Deploy & publish](10-phase-deploy.md)**: Render blueprint + deploy, seeded - demo scenarios, live smoke test, resume/website links and blurb. - -## Post-launch - -- **[State revert + retry fix](11-state-revert-and-retry-fix.md)**: undo/retry roll the shared - `script_state` back; per-action `state_before` snapshots; undo concurrency lock. -- **[Phase 12 — RPG world state](12-phase-rpg-world-state.md)**: structured world/player/NPC stats - + milestones per scenario (`stat_schema`); the AI proposes deltas, a Python engine clamps them - (min/max, per-turn cap, cooldown, sticky milestones); band descriptions keep the model honest; - World State drawer + Insights delta report; reuses the Phase 11 undo/retry snapshot. -- **[Memory-bank embedding cost](13-memory-embedding-cost.md)** *(round three of the egress - work)*: a turn fetched the whole memory bank's vectors to pick five — 96% of everything it - read. Packed float32 + an in-process cache + SQL-side filtering; a played turn went 6.4 MB - to 123 kB. Includes the byte-meter harness (`backend/tools/`). One step left, see STATUS. -- **[Phase 14 — Story tree](14-phase-story-tree.md)** *(designed, not started)*: the linear - action list becomes a branching tree, so a retry is a sibling rather than a rewrite and both - paths survive. The real argument is bug elimination — seven bug classes trace to "the story - is a mutable list" and all disappear when nothing is rewritten in place. diff --git a/plan/01-phase-foundation.md b/plan/01-phase-foundation.md deleted file mode 100644 index 3516b26..0000000 --- a/plan/01-phase-foundation.md +++ /dev/null @@ -1,38 +0,0 @@ -# Phase 1 — Foundation - -**Goal:** runnable skeleton — backend serving a database-backed API, frontend shell with -navigation, scenario & adventure CRUD, and a settings page for the AI endpoint. No AI calls yet. - -## Backend - -- [x] Project scaffold: `backend/` with FastAPI app, uvicorn entrypoint, `requirements.txt` - (fastapi, uvicorn, sqlalchemy, pydantic, httpx, tiktoken; quickjs deferred to phase 4). -- [x] SQLite via SQLAlchemy; auto-create `data.db` on first run. -- [x] Models: - - `Scenario`: id, title, description, prompt, memory, authors_note, tags, created/updated. - - `StoryCard`: id, owner (scenario or adventure), keys, entry, type, title, description. - - `Adventure`: id, scenario_id (nullable), title, memory, authors_note, script_state (JSON), - created/updated. - - `Action`: id, adventure_id, index, type (`do|say|story|continue|ai|start`), text, - context_snapshot (JSON, nullable), created. - - `Script`: id, name, description, input_js, context_js, output_js, library_js. - - `Settings`: single row — endpoint_url, api_key, model, temperature, max_output_tokens, - context_token_budget, api_mode (`chat|completion`). -- [x] Routers: CRUD for scenarios, adventures (+ create-from-scenario copying memory/AN/cards), - story cards, settings. Consistent JSON errors. -- [x] CORS for the Vite dev server; production mode serves built frontend as static files. - -## Frontend - -- [x] Vite + React scaffold in `frontend/`; router with pages: Home (adventure list), - Scenarios (list + editor), Play (placeholder), Settings. -- [x] Dark base theme (AI Dungeon-like: near-black background, serif story font, gold/teal accent). -- [x] Scenario editor: title, description, prompt, memory, author's note, story card list editor. -- [x] Settings page: endpoint URL, API key, model name, sampling params; "Test connection" button - (backend proxies a trivial request — wired for real in Phase 2, stub now). -- [x] "New adventure" flow: pick scenario (or blank) → creates adventure → navigates to Play page. - -## Exit criteria - -Run `uvicorn` + `npm run dev`, create/edit/delete scenarios with story cards, start an adventure -from one, see it listed on Home, and save endpoint settings — all persisted across restarts. diff --git a/plan/02-phase-play-loop.md b/plan/02-phase-play-loop.md deleted file mode 100644 index 6897585..0000000 --- a/plan/02-phase-play-loop.md +++ /dev/null @@ -1,45 +0,0 @@ -# Phase 2 — Play loop + AI provider - -**Goal:** the core game is playable. Player acts, AI continues the story, streamed live. - -## Provider adapter layer - -- [x] `providers/base.py`: abstract `Provider` — `generate(prompt_parts, params) -> async stream of text`. - Takes an assembled context object (system text + story text), so providers decide how to - map it to their wire format. -- [x] `providers/openai_compatible.py`: - - Chat mode: system message carries instructions/memory; story history flows in as - user/assistant text continuation framing suited to a chat endpoint. A configurable - "narrator" system prompt frames the AI as a second-person storyteller continuing the text. - - Streaming via SSE from the endpoint, re-streamed to the browser. - - Optional raw completion mode (`/v1/completions`) for pure-continuation models. -- [x] Errors surfaced cleanly (bad key, connection refused, model not found) with retry affordance. -- [x] "Test connection" on Settings now real. - -## Turn engine (`POST /adventures/{id}/actions`) - -- [x] Input formatting per AI Dungeon conventions: - - **Do** → `> You ` (normalized to second person, stripped punctuation as needed) - - **Say** → `> You say ""` - - **Story** → raw text appended - - **Continue** → no player text; AI just continues -- [x] Simple context for this phase: opening prompt + full history, truncated from the top to the - token budget (tiktoken count). Real context engine lands in Phase 3. -- [x] Response streamed to the client via SSE; final text stored as an `ai` action. -- [x] **Retry**: delete last AI action, regenerate with same input. -- [x] **Undo/Erase**: delete last action pair (player + AI) or single action. -- [x] **Edit**: PATCH any action's text in place. - -## Play UI - -- [x] Story view: continuous prose (not chat bubbles), player actions styled distinctly - (`>` prefix, accent color), auto-scroll, streaming text renders token-by-token. -- [x] Input bar with mode selector (Do / Say / Story) + Continue button; Enter to send. -- [x] Per-turn controls: Retry, Undo, Edit (inline contenteditable or textarea swap). -- [x] Loading/streaming state, error toast with retry. - -## Exit criteria - -Point Settings at any OpenAI-compatible endpoint (e.g. Ollama or LM Studio locally), start an -adventure, and play a multi-turn story with all four input modes plus retry/undo/edit, with -streaming output. diff --git a/plan/03-phase-context-insights.md b/plan/03-phase-context-insights.md deleted file mode 100644 index 288590d..0000000 --- a/plan/03-phase-context-insights.md +++ /dev/null @@ -1,62 +0,0 @@ -# Phase 3 — Context engine + Insights - -**Goal:** AI Dungeon-grade context management, and full visibility into every prompt. - -## Context assembly (`context/builder.py`) - -Assembles the prompt each turn from AI Dungeon's **plot components** -(per help.aidungeon.com/faq/the-memory-system): - -``` -[AI Instructions] ← behavioral guidance for the model (always included) -[Plot Essentials] ← key facts for constant recall — the classic "Memory" (always) -[Story Summary] ← running summary slot; manual in this phase, auto in Phase 6 -[Triggered Story Cards] ← "World Lore: " for each triggered card (conditional) -[Story history] ← as many recent actions as fit the token budget -[Author's Note] ← injected N lines (default 3) before the end of history -[Latest player action] ← (+ script frontMemory right after it, Phase 4) -``` - -- [x] **AI Instructions / Plot Essentials / Story Summary / Author's Note**: adventure-level - free-text fields, always included, editable mid-adventure from the side panel. -- [x] **Story cards** — five fields per official docs: **Type** (organizational, not sent to AI), - **Name** (not sent to AI), **Entry** (sent when triggered), **Triggers**, **Notes** (not sent). - - Triggers: comma-separated words/phrases; **case-insensitive but space-sensitive**; - **partial-word matching** (`boat` triggers on `boats`); matched against both player input - and AI output in the recent-story window. - - Not instant: a card triggered mid-response only enters context on the *next* turn; once - triggered, stays active while the triggering text remains in the context window. - - Triggered entries injected once each, prefixed `World Lore:`; story cards are the **first - component dropped** when context is full. - - Editable per-adventure (copied from scenario at creation). Soft-cap sanity limit (AI Dungeon - allows 5,000/adventure). -- [x] **Author's Note**: inserted near the end (strongest steering position), formatted - `[Author's note: ]`. -- [x] **Token budgeting** with tiktoken: always-included components (AI Instructions, Plot - Essentials, Story Summary, Author's Note) reserved first; story cards get a capped share - and are dropped first when over budget; remainder goes to story history (newest first). - Budget = `context_token_budget` setting. -- [x] Slots for script-provided memory overrides (Phase 4): `state.memory.context` (prepended), - `state.memory.authorsNote` (replaces/augments author's note), `state.memory.frontMemory` - (inserted immediately after the latest player action). -- [x] Builder returns a structured `ContextReport`: ordered sections, each with source label, - text, token count; plus totals and a list of triggered cards (and which keyword fired). - -## Insights - -- [x] Every AI turn stores its `ContextReport` on the `Action` row (`context_snapshot`). -- [x] `GET /adventures/{id}/actions/{id}/context` returns it. -- [x] **Insights panel** in Play UI (drawer/tab): - - Exact final prompt text as sent, sectioned and color-coded (memory / world info / history / - author's note / input), with per-section token counts and total vs budget. - - Which story cards triggered and on which keyword; which history got cut off. - - Viewable for the *upcoming* turn (dry-run endpoint: "what would be sent now") and for any - past AI action. -- [x] Adventure side panel: edit memory, author's note, story cards mid-game (AI Dungeon's - right-hand panel equivalent). - -## Exit criteria - -Create a card with key `dragon`; mention a dragon in play and see the card enter the context in -the Insights panel (and influence the AI); verify memory and author's note appear in the snapshot -in the right positions; long adventures visibly trim oldest history within budget. diff --git a/plan/04-phase-scripting.md b/plan/04-phase-scripting.md deleted file mode 100644 index 58d4e3d..0000000 --- a/plan/04-phase-scripting.md +++ /dev/null @@ -1,84 +0,0 @@ -# Phase 4 — Scripting (AI Dungeon-compatible JavaScript) - -**Goal:** real AI Dungeon scripts import and run: the three modifier hooks, persistent `state`, -and the scripting API surface. - -## JS runtime - -- [x] Embed a JS engine in Python: **quickjs** (preferred; check Windows wheel availability at - implementation time; fallback: py-mini-racer, or Node subprocess as last resort). -- [x] Sandbox matching AI Dungeon's documented limits: each hook runs **isolated**, **16 MB - memory cap**, **2-second timeout**; no filesystem/network/process access; script errors - captured and surfaced in the UI, never crash a turn. - -## AI Dungeon scripting model (compatibility target) - -*(Per official docs: help.aidungeon.com/faq/how-do-i-write-scripts-and-use-scripting)* - -Three lifecycle hooks — `onInput`, `onModelContext`, `onOutput`. Each script defines a modifier -and **must call it as its last line**: - -```javascript -const modifier = (text) => { - // script logic - return { text, stop } -} -modifier(text) -``` - -- **onInput** — modifies player input before context construction. -- **onModelContext** — modifies the assembled text sent to the model. -- **onOutput** — modifies the model output before it is shown/stored. -- **Shared Library** — code prepended to all three slots. - -Return contract: -- [x] `{ text, stop }`; `stop: true` from onInput prevents the AI call. -- [x] Empty-string `text` from onInput/onOutput → user-facing error (replicate this behavior). - -Globals provided (exact names from docs): - -- [x] `text` — hook input (player input / context / AI response respectively). -- [x] `state` — persisted per adventure across turns (`Adventure.script_state`); includes - `state.memory`, `state.message` (shown as a UI notice), `state.placeholders`. -- [x] `state.memory` slots: `context` (prepended to context), `authorsNote` (near end, before - latest response), `frontMemory` (inserted right after the player's input). -- [x] `history` — array of recent actions: `{ text, rawText, type }`. -- [x] `storyCards` — array of `{ id, keys, entry, type }`, backed by the adventure's story cards. -- [x] Story card functions: `addStoryCard(keys, entry, type)` → index (or `false` on duplicate), - `updateStoryCard(index, keys, entry, type)` and `removeStoryCard(index)` → throw if absent. -- [x] Legacy aliases for older scripts: `worldInfo` / `worldEntries`, `addWorldEntry`, - `updateWorldEntry`, `removeWorldEntry` mapped onto the storyCards implementation. -- [x] `info` — `{ actionCount, characterNames, memoryLength, maxChars }`. -- [x] `log(message)` / `console.log` — captured per turn, shown in a script log panel. - -## Pipeline integration - -``` -player input → INPUT modifier → format & store -context build → CONTEXT modifier → (snapshot includes pre- and post-script versions in Insights) -AI response → OUTPUT modifier → store & render -``` - -- [x] Insights (Phase 3) extended: show context before vs after the context modifier (diff view), - and script log output per turn. - -## Script management UI - -- [x] Scripts page: create/edit scripts with a code editor (CodeMirror), one tab per slot - (Library / Input / Context / Output), description field. -- [x] Attach scripts to scenarios; adventures inherit at creation. Enable/disable per adventure. -- [x] Test-run a script against sample text without an AI call. - -## Import / Export - -- [x] **Scripts**: export/import as JSON bundle `{ name, library, input, context, output }` and - as raw `.js` files per slot (matching how AI Dungeon scripts circulate — paste or file). -- [x] **Scenarios**: export/import JSON including prompt, memory, author's note, story cards, - and attached scripts. Accept AI Dungeon scenario export JSON where format is known; - map fields best-effort and report anything unmapped. - -## Exit criteria - -Paste a real AI Dungeon script (e.g. a simple input modifier + state counter + world entry -manipulation) and it runs unmodified across turns; state persists; export a scenario with scripts, -re-import it into a fresh database, and play it. diff --git a/plan/05-phase-polish.md b/plan/05-phase-polish.md deleted file mode 100644 index 3ed8cbb..0000000 --- a/plan/05-phase-polish.md +++ /dev/null @@ -1,38 +0,0 @@ -# Phase 5 — Polish & quality of life - -**Goal:** the app feels like AI Dungeon — cohesive dark UI, smooth flows, safe data handling. - -## UI/UX pass - -- [x] Theming: refined dark palette, serif story typography, subtle textures/gradients à la - AI Dungeon; consistent buttons, panels, modals; responsive layout. -- [x] Home: adventure cards with scenario name, last-played time, action count; search/filter; - scenario gallery with tags. -- [x] Play screen: collapsible right side panel (Memory / Cards / Scripts / Insights tabs), - keyboard shortcuts (Enter send, Ctrl+Z undo, Ctrl+R retry), smooth streaming autoscroll - that pauses when the user scrolls up. -- [x] Scenario **placeholders**: `${Character name}` style variables in prompt/memory prompt the - player for values when starting an adventure (AI Dungeon behavior). - -## Data & robustness - -- [x] Adventure export/import (full JSON: actions, memory, cards, script state) — backup/share. -- [x] Delete confirmations (trash/soft-delete skipped — plain confirm dialogs). -- [x] SQLite migrations story (versioned schema bootstrap via PRAGMA user_version — - `backend/app/migrations.py`). -- [x] Request logging + a debug page tailing recent provider requests/responses (bodies redacted - of API key) — "Recent AI requests" on the Settings page. -- [x] Graceful handling: provider timeout/cancel (stop generation button), concurrent turn lock - per adventure. - -## Nice-to-haves (only if time/interest) - -- [ ] Multiple provider profiles with quick switching (e.g. local Ollama vs OpenRouter). -- [ ] Per-scenario generation params overriding global settings. -- [ ] Retry with "give me something different" (temperature bump / anti-repeat nudge). -- [ ] Basic light theme toggle. - -## Exit criteria - -A friend could sit down at `localhost`, start a scenario with placeholders, play comfortably, -peek at Insights, and you can back up / restore everything via export files. diff --git a/plan/06-phase-memory-bank.md b/plan/06-phase-memory-bank.md deleted file mode 100644 index bdda5b2..0000000 --- a/plan/06-phase-memory-bank.md +++ /dev/null @@ -1,47 +0,0 @@ -# Phase 6 — Auto Summarization + Memory Bank (optional) - -**Goal:** replicate modern AI Dungeon's Memory System (per -help.aidungeon.com/faq/the-memory-system): AI-generated memories, a running Story Summary, and -embedding-based retrieval. This phase makes extra AI calls (summarization + embeddings), so it is -opt-in per adventure and gated on the endpoint supporting it. - -## Auto Summarization - -- [x] **Memories**: every 6 actions (starting at action 12), summarize that block of - player actions + AI responses into a short "memory" via a background AI call - (same provider, cheap/configurable model override). -- [x] **Story Summary**: every 15 actions, update the Story Summary plot component — a running - overview of the plot — folding in recent memories; compress it when it grows too long. -- [x] Story Summary stays **manually editable**; user edits inform future updates - (they are the base text for the next summarization pass) but are never overwritten silently. -- [x] Summarization failures are non-fatal: log, retry next interval. - (Failed calls appear on the debug page; cursors only advance on success, so the - next turn retries. Implementation: `backend/app/memorybank.py`.) - -## Memory Bank - -- [x] Store each memory with an **embedding vector** (OpenAI-compatible `/v1/embeddings`; - embedding model configurable in Settings; feature disabled if unavailable). -- [x] Each turn, embed the recent story text and rank memories by cosine similarity; - inject the top-K "Used Memories" into context as their own component - (between Story Summary and story cards in the layout). Pinned memories are always - included; top-K is a setting (default 5). -- [x] Configurable bank capacity (AI Dungeon tiers: 25–400; ours: a setting, default 200); - when full, evict least-recently-used/least-retrieved memories ("Forgotten Memories"). -- [x] SQLite storage for vectors (JSON blob + in-memory cosine ranking — pure Python, - no numpy needed at this scale). - -## UI - -- [x] Memory Bank panel: list memories (used / idle / forgotten), edit or delete, pin favorites, - see which memories were retrieved for a given turn (🔍 on an AI action → Insights snapshot). -- [x] Insights integration: retrieved memories shown as a context section with similarity scores. -- [x] Adventure settings: toggle auto-summarization / memory bank (per adventure, in the Memory - panel); summary + embedding models are chosen globally in Settings (deliberate - simplification — one endpoint config for the whole app). - -## Exit criteria - -Play a 40+ action adventure: memories appear every 6 actions, the Story Summary updates every 15, -an early-game fact that scrolled out of the raw history gets retrieved via the Memory Bank when it -becomes relevant again, and Insights shows exactly which memories were injected and why. diff --git a/plan/07-phase-public-repo.md b/plan/07-phase-public-repo.md deleted file mode 100644 index 8af1dec..0000000 --- a/plan/07-phase-public-repo.md +++ /dev/null @@ -1,62 +0,0 @@ -# Phase 7 — Public repo & portability - -**Goal:** make the repo public-worthy and runnable by anyone on any OS, so the GitHub link is -immediately usable on a resume — before any hosted-deployment work. - -## Decisions (confirmed) - -| Question | Answer | -|---|---| -| License | **MIT** | -| README media (screenshots/GIF) | **Skip for now** — text-only README; visuals in a later pass | - -**Repo name (decided): `AI-DnD`.** Ask before publishing: whether the existing commit -history/messages are fine to publish as-is. - -## Repo hygiene - -- [x] Add `LICENSE` (MIT, current year, Parth Thakkar). -- [x] Verify no secrets or user data are tracked (`openrouter_key.env`, `data.db` — already - gitignored and never committed; re-verify before push). -- [x] Add `backend/.env.example` documenting every env var the app reads (grows in Phase 9). - (Currently just `AIDND_DB_PATH`, added to `database.py` for Docker/hosted volumes.) -- [x] Decide what to do with `CODE_REVIEW_FINDINGS.md` and `plan/` — keep (shows process, good - for a portfolio) — just give them a one-line mention in the README. - -## README rewrite (portfolio-grade, text-only) - -- [x] Pitch paragraph: what it is, what makes it interesting (AI Dungeon-compatible scripting, - memory bank with embeddings, full prompt transparency/Insights, provider-agnostic). -- [x] Feature list with pointers into the code (scripting engine, context builder, memory bank). -- [x] Architecture diagram (reuse/refresh the one in `plan/00-OVERVIEW.md`). -- [x] Setup instructions for **Windows (start.ps1), macOS/Linux (manual), and Docker**. -- [x] "Bring your own model" section: Ollama / LM Studio / OpenRouter free models — emphasize it - runs fully free. -- [x] Placeholder section for screenshots/GIF (added in a later pass). - -## Docker (one-command run for non-Windows users) - -- [x] `Dockerfile`: multi-stage — build frontend (`npm run build`), then Python image serving - FastAPI with the built SPA mounted (SPA fallback already exists in `app/main.py`). - (3 stages: node build → pip wheel build with gcc for quickjs → slim runtime.) -- [x] `docker-compose.yml`: single service, named volume mounted at `/data` - (`AIDND_DB_PATH=/data/data.db`), port mapping. -- [x] `start.sh` for macOS/Linux dev parity with `start.ps1` (optional, nice-to-have). -- [x] Test: `docker compose up` from a clean clone → app works at `http://localhost:8000`. - Done 2026-07-06 in WSL2 Ubuntu (Docker Engine installed there for this): health + SPA + - deep links 200, scenario created via API survives a full `compose down`/`up` (named - volume). Two real bugs found and fixed: package-lock.json was missing top-level - `@emnapi/*` entries (regenerated with npm 11.18) and the build image needed node 24 to - match the npm-11 lockfile. Note: from Windows, reach a WSL-hosted container via the WSL - IP (`hostname -I`) — localhost forwarding didn't apply; irrelevant on normal hosts. - -## Publish - -- [ ] Create public GitHub repo (`gh repo create`), push `main`. -- [ ] Add repo description, topics (`ai-dungeon`, `fastapi`, `react`, `llm`, `interactive-fiction`). -- [ ] Confirm the GitHub rendering of README looks right. - -## Exit criteria - -A stranger on macOS with Docker installed can clone the repo, run one command, open the app, -paste an OpenRouter free-tier key, and play an adventure — without asking you anything. diff --git a/plan/08-phase-accounts.md b/plan/08-phase-accounts.md deleted file mode 100644 index 8d6105e..0000000 --- a/plan/08-phase-accounts.md +++ /dev/null @@ -1,81 +0,0 @@ -# Phase 8 — Optional accounts & multi-user ✅ (implemented 2026-07-06, branch `phase-8-accounts`) - -**Goal:** turn the single-user app into a multi-user one where **accounts are optional**: -a visitor can start playing instantly as a guest, and can register (email + password) at any -point to keep their adventures across devices/browsers. This is the largest phase — it touches -every router and most tables. - -## Decisions (confirmed) - -| Question | Answer | -|---|---| -| Auth method | **Email + password, optional** — guest sessions work without an account | -| Signup policy | **Open signup** (rate-limited) | -| LLM API keys | **BYOK + shared demo key** — users can paste their own key; users without one get limited turns on a server-funded key | -| Demo key funding | **OpenRouter free models** (owner's key, `:free` whitelist; default `google/gemma-4-26b-a4b-it:free`) | -| Demo turn cap | **20 successful turns/user/day** (failed provider calls don't count) | -| Guest data retention | **Never delete** for v1 (no cleanup job; revisit if the DB grows) | -| Password reset | **Skipped for v1** (no email provider; forgotten password = lost account) | -| Login with an active guest session | Guest is **abandoned**, not merged (its data stays under the guest user) | -| Memory bank on demo key | **Disabled** (no background AI calls on the server-funded key; visible note in the Memory panel/Insights) | - -## Data model - -- [x] `User` table: id, email (nullable — null means guest), password_hash (nullable), - created_at, last_seen_at, is_guest flag, demo_turns_used + demo_turns_date. -- [x] `user_id` FK on `Adventure`, `Scenario`, `Script`, `Settings`. Story cards/actions/ - memories inherit scope via their parent (ownership checks resolve the parent). -- [x] Settings **per-user** (row per user_id, unique index). API key **encrypted at rest** - (Fernet; key derived from `AIDND_SECRET_KEY` or auto-generated `secret.key` next to the - DB). Key is write-only through the API (`has_api_key` instead of echoing it). -- [x] Migrations 13–23: create "local user" id=1, assign all existing rows to it, unique - index on settings.user_id; plus a Python bootstrap step that encrypts any plaintext - api_key (`enc:` prefix marks encrypted values). -- [x] Demo/starter scenarios: `user_id NULL` + `is_public` — everyone sees them read-only; - `seed_demo.py` seeds the Sunken Crypt scenario as public (its scripts are unowned and - ship with it; the sample adventure belongs to the local user). - -## Auth & sessions - -- [x] Guest flow: `GET /api/auth/me` with no/invalid cookie → creates guest User + signed - long-lived httpOnly cookie (HMAC, `security.py`). Other endpoints 401 without a session; - the frontend re-establishes via /me and retries once. No signup wall anywhere. -- [x] Register upgrades the guest **in place** (same user_id — data kept). scrypt password - hashing (stdlib, no extra dep). -- [x] Login switches the session cookie to the account (guest abandoned). Logout clears it. -- [x] Every router handler resolves `current_user`; every query filtered by user_id - (scenarios/adventures/scripts/story-cards/settings; debug log is local-mode only since - it's a global buffer). -- [x] Rate limit on register/login: 10 attempts / 5 min per IP (in-memory). -- [x] Local/self-hosted mode stays frictionless: auto-created local user, no login UI unless - `AIDND_MULTI_USER=1`. Local installs and docker compose behave exactly as before. - -## Shared demo key (BYOK fallback) - -- [x] Env vars: `AIDND_DEMO_API_KEY`, `AIDND_DEMO_ENDPOINT_URL` (default OpenRouter), - `AIDND_DEMO_MODELS` (comma whitelist), `AIDND_DEMO_TURNS_PER_DAY` (default 20). - Demo only activates in multi-user mode. -- [x] No API key configured → demo endpoint/key/whitelisted model; per-user per-day counter; - 429 with a friendly "add your own key in Settings" message when capped (checked before - the turn starts so no orphaned player action). -- [x] Memory bank + auto-summarization disabled on demo turns (decided: disable, not count). - -## Frontend - -- [x] Auth UI: Sign up / Log in modal (register default, toggle to login), "Playing as guest — - sign up to keep your adventures" nudge in the header, account email + logout when - registered. All hidden in local mode (`multi_user:false` from /me). -- [x] `api.js`: 401 → GET /auth/me (new guest session) → retry once, for both JSON and SSE. -- [x] Settings: demo banner ("Using the shared demo key — N of M free turns left today"), - write-only API key field with Remove button, debug log hidden in multi-user mode. -- [x] Public scenarios: "demo ✦" badge in the list; read-only editor (fieldset-disabled) with - an explainer banner; Play/Export still available. - -## Exit criteria — verified 2026-07-06 - -Two sessions (curl cookie jars + Chrome UI): each guest gets an isolated world; register -mid-session keeps all data (same user id); logging in from the second session shows the same -account data; duplicate email → 409; wrong password → 401; rate limiter kicks in. Demo cap -returns 429 at 0 turns left. Migration tested on a copy of the real data.db (rows adopted by -local user, api_key Fernet-encrypted and decrypts back to the original). Live OpenRouter turn -through the encrypted-key path works in local mode. `vite build` + oxlint clean. diff --git a/plan/09-phase-hardening.md b/plan/09-phase-hardening.md deleted file mode 100644 index 5f04b43..0000000 --- a/plan/09-phase-hardening.md +++ /dev/null @@ -1,93 +0,0 @@ -# Phase 9 — Production hardening - -**Goal:** make the app safe and stable to expose to strangers on the internet: config via -environment, resource limits on everything user-controlled, and a single-service production -build. - -## Decisions - -| Question | Answer | -|---|---| -| Database | **Decide at start of this phase.** SQLite on a persistent disk (zero code change, but Render disks require the ~$7/mo starter tier) vs Postgres (free/cheap managed options, better resume talking point, needs SQLAlchemy URL + migration tweaks). Revisit with current Render pricing. | - -**Ask before implementing:** the database choice above, and target monthly budget (drives -Render tier: free tier sleeps after idle + has no persistent disk). - -## Configuration - -- [ ] All config via env vars with sane local defaults: `DATABASE_URL`, `SECRET_KEY` - (sessions + API-key encryption), `MULTI_USER`, `CORS_ORIGINS`, demo-key vars (Phase 8), - port/host. Document each in `backend/.env.example`. -- [ ] Fail fast on missing `SECRET_KEY` when `MULTI_USER=true`. - -## Abuse & resource limits - -- [ ] **quickjs limits**: per-execution time limit and memory limit on the scripting engine - (`scripting/engine.py`) — user-submitted JS must not be able to hang or OOM the server. -- [ ] Rate limiting on expensive endpoints (turn generation, script run, auth) — per-user and - per-IP (e.g. `slowapi`). -- [ ] Request size limits (script source length, memory/story-card text lengths, action text). -- [ ] Cap per-user row counts (adventures, scenarios, scripts, story cards) with friendly errors. -- [ ] Audit debug router (`routers/debug.py`) and `/docs`: admin-only or disabled when - `MULTI_USER=true` — debug log may contain other users' prompts. - -## Production serving - -- [ ] Single service: FastAPI serves the built SPA (fallback already exists) — verify the Docker - image from Phase 7 is production-ready (no `--reload`, multiple workers or async-safe - single worker; check SQLite + multiple workers interaction before choosing). -- [ ] CORS locked to the deployed origin (moot if same-origin single service — verify). -- [ ] Security headers middleware; cookies `Secure` + `SameSite`. -- [ ] Streaming (SSE) works behind Render's proxy — verify no buffering issues. -- [ ] Structured logging; scrub API keys from all logs and the debug page. - -## Database (after decision) - -- [ ] If Postgres: swap `DATABASE_URL`, verify JSON-blob columns (embeddings) and - `migrations.py` work; test full play loop. -- [ ] If SQLite-on-disk: confirm WAL mode + single-worker (or serialized writes) is acceptable. -- [ ] Backup story: platform DB backups (Postgres) or a scheduled dump of the disk (SQLite). - -## Exit criteria - -Running the production Docker image locally with `MULTI_USER=true`: a hostile user cannot hang -the server with a `while(true)` script, cannot see another user's data or the debug log, gets -rate-limited instead of burning the demo key, and the app streams turns normally the whole time. - -### Verified 2026-07-07 (uvicorn, `MULTI_USER=1`, fresh SQLite DB, curl) - -- **Fail-fast secret:** `import app.main` with `MULTI_USER=1` and no `AIDND_SECRET_KEY` raises - the RuntimeError as designed (won't boot). -- **`while(true)` script:** `POST /api/scripts/{id}/test` on an `input_js` infinite loop returns - `InternalError: interrupted` (engine time limit) — server stays responsive afterward. -- **Cross-user isolation:** guest B sees `[]` for scripts, gets 404 on guest A's script id; - guest A keeps its own row. No leakage. -- **Debug log:** `GET /api/debug/requests` → 403 in multi-user mode. -- **Rate limiting:** 12 rapid `POST /api/auth/register` → 429 after the 10th (auth scope, 10/300s). -- **Body size:** 3 MB body to `POST /api/scenarios` → 413 (limit 2 MB) via BodySizeLimitMiddleware. -- **Security headers:** CSP, `x-frame-options: DENY`, `x-content-type-options: nosniff`, - `referrer-policy: same-origin` on every response — including the SSE stream. -- **Docs disabled:** Swagger UI and OpenAPI schema not served (`/docs`, `/openapi.json` fall - through to the SPA `index.html`; no `swagger-ui`, no API schema exposed). -- **SSE streaming:** `POST /api/adventures/{id}/actions` streams `text/event-stream` with - `x-accel-buffering: no`, chunked, incremental events — the pure-ASGI middlewares don't buffer. - (No LLM key configured here, so it streams the "No model configured" error event; a *live* - provider turn through this path was verified end-to-end in Phase 8.) - -All Phase 9 exit criteria met. - -### Postgres path verified 2026-07-07 (real Neon, PostgreSQL 18.4) - -Pointed the backend at a live Neon `DATABASE_URL` (pooled, `sslmode=require&channel_binding=require`): - -- **Driver/URL:** `postgres://`/`postgresql://` normalized to `postgresql+psycopg://`; raw - psycopg3 connect succeeds. -- **Bootstrap:** fresh DB → `create_all` builds all 11 tables and stamps `schema_version = 23` - (= `LATEST_VERSION`); the SQLite-only migrations 2–23 are correctly skipped on a fresh DB. -- **Embedding column:** `memories.embedding` is Postgres `json`; a float list round-trips - intact (`[0.1, 0.2, 0.3, -0.4]` back as a `list`). -- **ORM CRUD:** user → scenario → adventure → memory create/read/delete with FK cascades works. -- **HTTP round-trip (multi-user, TestClient):** guest bootstrap → register → me → create - scenario → list → settings → delete, all 200/201/204 against Neon. - -Remaining Phase 10 step: the Docker production image specifically (build + run the container). diff --git a/plan/10-phase-deploy.md b/plan/10-phase-deploy.md deleted file mode 100644 index b85f3a5..0000000 --- a/plan/10-phase-deploy.md +++ /dev/null @@ -1,56 +0,0 @@ -# Phase 10 — Deploy & publish - -**Goal:** the app live on Render at a public URL, linked from resume/website alongside the -GitHub repo. - -## Decisions (confirmed) - -| Question | Answer | -|---|---| -| Platform | **Render** | -| Domain | **Platform URL is fine** (e.g. `ai-dnd.onrender.com`); custom domain can be added later anytime | - -**Ask before implementing:** Render tier (free-with-sleep vs ~$7/mo always-on — depends on the -Phase 9 database decision), and the exact service name (it becomes the public URL). - -### Decisions (confirmed 2026-07-07) - -| Question | Answer | -|---|---| -| Database | **Neon Postgres** (external managed; free tier has no persistent disk). Path fully verified against real Neon — see `plan/09-phase-hardening.md`. | -| Tier | **Free** (sleeps after ~15 min idle; ~30–60s first-wake). | -| Service name | `ai-dnd` (→ `ai-dnd.onrender.com`, adjustable in dashboard). | -| Region | `virginia` (us-east, matches Neon us-east-1). | -| Local Docker preflight | Skipped by choice — Render builds the same Dockerfile in the cloud; its build logs are the image test. | - -## Deploy - -- [x] `render.yaml` blueprint: web service from the Dockerfile, env vars (SECRET_KEY generated, - demo-key vars, `MULTI_USER=true`), `/api/health` health check, external Neon Postgres. - README gained a "Deploy (Render)" section. -- [ ] Set up the Render service, connect the GitHub repo, auto-deploy on push to `main`. -- [ ] Seed production with 2–3 good demo scenarios (public/starter scenarios from Phase 8) so - first-time visitors have something great to click immediately. -- [ ] Smoke test the live URL: guest play on demo key, register, BYOK flow, scripting, memory - bank, Insights — from a device/network that isn't yours. -- [ ] Free-tier note: if on free tier, first request after idle takes ~30–60s to wake — add a - friendly loading state or accept it (revisit tier if it feels bad). - -## Post-launch guardrails - -- [ ] Watch demo-key spend/usage for the first days (OpenRouter dashboard); confirm caps hold. -- [ ] Set up uptime monitoring (free: UptimeRobot or similar) — optional. -- [ ] Error visibility: Render logs are enough for v1; note how to pull them. - -## Resume / website - -- [ ] README: add the live-demo link + "Try it" section at the top. -- [ ] 2–3 sentence project blurb for resume/website (stack, the interesting hard parts: - AI Dungeon-compatible JS scripting sandbox, embedding-based memory bank, prompt - transparency, guest-first optional auth). -- [ ] Later pass (deferred from Phase 7): screenshots/demo GIF for README and website card. - -## Exit criteria - -A recruiter clicks one link on your resume, lands on the live app, plays three turns of a demo -scenario as a guest without configuring anything, and can find the GitHub repo from the page. diff --git a/plan/11-state-revert-and-retry-fix.md b/plan/11-state-revert-and-retry-fix.md deleted file mode 100644 index 18eda1c..0000000 --- a/plan/11-state-revert-and-retry-fix.md +++ /dev/null @@ -1,81 +0,0 @@ -# Plan: undo/retry state revert (+ concurrency lock) - -Fixes three linked issues around `script_state` (the shared per-adventure -"scoreboard" scripts write to) and undo/retry. - -## Background - -- `script_state` is one shared dict on `Adventure` (`models.py:90`), mutated in - exactly ONE place: `pipeline.py:89` (`self.adventure.script_state = state`). -- Today, undo (`adventures.py:445`) and retry (`:422`) delete *actions* but never - touch `script_state`, so state never rolls back. - -## Issue 1 — retry double-applies state (pre-existing bug) - -Retry deletes the last AI action and regenerates. The output hook already mutated -`script_state` on the first attempt; regenerating runs it again, stacking the change -(e.g. "add 10 gold" → 20 gold after one retry). Same root cause as undo not -reverting. - -## Issue 4 — undo has no concurrency guard - -Turns take `acquire_turn_lock` (`:189`); undo does not, so undo can race a turn -that is still streaming. - -## Issue 2 — Memory Bank leftovers (smaller than expected) - -`run_post_turn` already clamps `memory_cursor`/`summary_cursor` down to the current -action count (`memorybank.py:178-182`), so there is NO cursor stall. The only -remainder: a `Memory` created from a turn that was later undone stays behind, its -`source_start/source_end` now pointing past the end of the story. - ---- - -## The fix - -### 1. Snapshot state per turn (Issue 1 + enables undo revert) - -- Add column `state_before: JSON nullable` to `Action` (`models.py`). -- Migration: append `(25, "ALTER TABLE actions ADD COLUMN state_before JSON")` - to `migrations.py`. `JSON` is valid on both SQLite and Postgres. -- In `run_player_turn` / `generate_turn`, when the FIRST action of a turn is - created, stash `copy.deepcopy(adventure.script_state)` onto it — captured - *before* any hook runs. (Player action for do/say/story; the AI action for a - bare `continue`.) -- Fix retry directly: before regenerating, restore - `adventure.script_state` from the deleted AI action's `state_before` so the - output hook starts from the pre-turn scoreboard instead of the mutated one. - -### 2. Revert on undo (Issue depends on #1) - -- In `undo_turn`, after deleting the popped actions, set - `adventure.script_state = .state_before` (fall back - to `{}` if null, i.e. pre-migration turns), then commit. -- Only wire this into undo + retry — NOT the arbitrary - `delete_action` endpoint (`:830`); mid-history state revert is undefined. - -### 3. Lock undo (Issue 4) - -- Wrap `undo_turn` body in `acquire_turn_lock(adventure_id)` / - `_active_turns.discard(...)` in a `finally`. It's synchronous (not SSE), so no - `with_turn_lock` wrapper needed — just acquire and discard. - -### 4. Clean up dangling memories (Issue 2, optional) - -- In `undo_turn` after deleting actions, delete any `Memory` whose `source_start` - is >= the new story-action count (i.e. summarized a turn that no longer exists). - Cursors already self-heal, so this is polish, not correctness. - -## Known limits (document, don't fix) - -- Story cards a script created (`_apply_cards`, `pipeline.py:87`) are NOT reverted — - only text + script_state roll back. -- Pre-migration turns have `state_before = NULL` → undo falls back to `{}`. -- Demo turn cap is not refunded on undo (intentional). - -## Test checklist - -- Script that increments a counter: play → undo → counter back to prior value. -- Same script: play → retry → counter changes once, not twice. -- Undo during an active stream returns 409, doesn't corrupt state. -- Undo a turn old enough to have been summarized: no orphaned memory left. diff --git a/plan/12-phase-rpg-world-state.md b/plan/12-phase-rpg-world-state.md deleted file mode 100644 index a9a25bf..0000000 --- a/plan/12-phase-rpg-world-state.md +++ /dev/null @@ -1,269 +0,0 @@ -# Phase 12 — RPG world state (AI-authored, engine-clamped) - -**Goal:** each turn carries a structured **world state** — world stats (e.g. `day`), -player stats (`hp`, `mana`), per-NPC stats (`health`, `trust`, …) for the NPCs currently -in scene, and **milestones** (story-progress flags / quest objectives). After the player -acts, the AI reads the current state, narrates, and **proposes** state changes; the -engine **validates and clamps** them against a schema before storing. Stat meanings are -described in words (bands) so the model reasons semantically, not arithmetically. - -This is deliberately the "AI owns mechanics, engine enforces limits" design — NOT a -deterministic dice engine. The AI proposes; Python is the referee. - -## Design decisions (settled) - -- **AI proposes, engine clamps.** The model never owns the numbers directly. It emits - a delta; the engine applies min/max, per-turn caps, and cooldowns server-side. The - AI cannot be trusted to obey its own frequency rules — the engine must. -- **Band descriptions are the reliability trick.** Stats carry word ranges - (`0–20: very weak`, `20–40: hurt`, …). The model reads "he's badly hurt" and adjusts - down, instead of doing math it's bad at. -- **Dedicated NPCs, each with its own stats.** NPCs are defined in a `npcs` section of - the schema, keyed by a stable id (`gwen`). Each has a `name`, `desc`, trigger `keys`, - and its **own** `stats` block (a dragon can have `ferocity`, a merchant `prices`) — no - forced shared template. On adventure creation each NPC auto-creates a story card (from - its name/keys/desc) so lore injection + in-scene detection keep working. Live values - live in `world_state["npc"]` keyed by the NPC id; only NPCs **in scene this turn** get - their stats injected into context. The AI addresses them as `npc..`. -- **Schema on the scenario, live values on the adventure.** The scenario is the - template (what stats exist, their bands + rules); the adventure holds current values. -- **Milestones are sticky story flags.** Predefined objectives the AI marks reached via - the same delta channel. Once reached they stay reached (revert only through the undo - snapshot). Injected as "Goals" (pending) so the AI drives toward them and "Achieved" - so it doesn't re-do them. Emergent/AI-invented milestones are out of scope for v1. -- **Flags are two-way booleans.** Separate from milestones: named on/off world/character - state (`has_key`, `disguised`, `alarm_raised`) the AI can flip either direction via - `"flags.": true|false`. Have an `initial` value and a `desc`; no clamp/cooldown. -- **Stat guide (descriptions + band ranges).** A fixed, per-scenario legend injected each - turn, describing each stat's `desc` and its full band ladder (`0–20 very weak, …`) — - handled independently, so a stat may have a description, a range, both, or neither. This - is separate from the live values block (which still shows only the *current* band label), - giving the model the whole scale to reason across without bloating the per-turn line. -- **One-call turn.** The AI narrates AND appends a fenced state-delta block; the engine - parses it and strips it from the visible text. No second LLM call — matters on the - rate-limited free-tier demo (20 req/min). Parser is forgiving of messy JSON from - weaker free models. -- **Memory bank / multi-memory is untouched.** (Confirmed.) -- **Separate from `script_state`.** World state gets its own column so it never collides - with the scripting scoreboard, and reuses the same undo/retry snapshot pattern - (`Action.state_before`, see `plan/11`). - ---- - -## Data model - -### Schema definition — `Scenario.stat_schema` (JSON, nullable) - -Migration `(26, "ALTER TABLE scenarios ADD COLUMN stat_schema JSON")`. - -```jsonc -{ - "world": { - "day": { "type": "counter", "min": 1, "initial": 1, "desc": "In-game day", - "max_delta_per_turn": 1, "cooldown": 0 } - }, - "player": { - "hp": { "min": 0, "max": 100, "initial": 100, "max_delta_per_turn": 30, - "cooldown": 0, "bands": [[0,20,"very weak"],[20,40,"hurt"], - [40,60,"minor damage"],[60,90,"healthy"],[90,100,"full health"]] }, - "mana": { "min": 0, "max": 50, "initial": 20, "max_delta_per_turn": 15 } - }, - "npcs": { // each NPC has its OWN stats - "gwen": { - "name": "Gwen", "keys": "Gwen, ranger, her", - "desc": "A loyal ranger and the player's ally.", - "stats": { - "health": { "min": 0, "max": 100, "initial": 100, "bands": [...] }, - "trust": { "min": -100, "max": 100, "initial": 20, "max_delta_per_turn": 20, - "bands": [[-100,-30,"hostile"],[-30,30,"wary"],[30,100,"ally"]] } - } - } - }, - "flags": { // two-way on/off booleans - "has_key": { "desc": "Player holds the dungeon key", "initial": false }, - "alarm_raised": { "desc": "The enemy is alerted", "initial": false } - }, - "milestones": { // sticky story-progress flags - "rescue_gwen": { "desc": "Rescue Gwen from the bandits" }, - "reach_capital": { "desc": "Arrive at the capital city" } - } -} -``` - -Per-stat rule fields (all optional, engine enforces): -- `min` / `max` — hard clamp. -- `initial` — value when first instantiated. -- `max_delta_per_turn` — largest absolute change allowed in one turn (extra is clamped). -- `cooldown` — minimum player actions between changes to this stat (0 = every turn). -- `bands` — `[lo, hi, label]` triples, used only to describe the value to the model. -- `type` — `"counter"` (monotonic, e.g. day) vs default numeric; counters reject - negative deltas. - -Milestones carry only `desc` (the objective text). They are boolean and sticky — the -engine accepts a delta of `true` only, records the action index reached, and ignores -attempts to re-set or un-set (undo is the only way back). - -Each NPC in `npcs` carries `name`, `desc`, trigger `keys`, and its own `stats` block -(same per-stat fields as above). All defined NPCs are instantiated up front; a story card -is auto-created per NPC on adventure creation (skipped if a card with that name already -exists) so descriptions inject as lore and in-scene detection works. - -### Live values — `Adventure.world_state` (JSON, default `{}`) - -Migration `(27, "ALTER TABLE adventures ADD COLUMN world_state JSON")`. - -```jsonc -{ - "world": { "day": 3 }, - "player": { "hp": 55, "mana": 10 }, - "npc": { "gwen": { "health": 80, "trust": 20 } }, // keyed by NPC id - "milestones": { "rescue_gwen": { "reached": true, "at": 7 } }, - "_meta": { "last_changed": { "player.hp": 7, "npc.gwen.trust": 6 } } // action index -} -``` - -`_meta.last_changed` backs the `cooldown` rule. NPC blocks are instantiated up front from -each NPC's own `stats`. - -### Undo/retry snapshot — `Action.world_state_before` (JSON, nullable) - -Migration `(28, "ALTER TABLE actions ADD COLUMN world_state_before JSON")`. -Snapshotted and reverted exactly like `state_before` (Phase 11) — same call sites. - ---- - -## Turn flow - -``` -player input → INPUT hook (existing) -context build → inject [World State] section (current values + band scale + emit-rule) -AI response → narration + trailing ```state { ...delta... } ``` block - → parse delta → validate/clamp against schema → apply → strip block - → snapshot world_state onto the action (undo) -``` - -### 1. Context injection (`context/builder.py`) - -New always-on section `world_state`, placed with the system sections. Keep it **terse** -(competes with story history under the default context budget, raised to 16384 in Phase 12): - -``` -World state — day 3. -You: HP 55/100 (minor damage), Mana 10/50. -Gwen: health 80 (healthy), trust 20 (neutral). -Goals: Arrive at the capital city. -Achieved: Rescued Gwen from the bandits. -``` - -- Only inject NPC lines for cards **triggered this turn** — reuse `triggered` / - `card_records` already computed in `build_context` (`builder.py:120`). No extra work. -- Milestones: list unreached ones under `Goals` and reached ones under `Achieved` - (omit either line when empty). These are cheap and always included. -- Append the value's band label in parentheses so the model reads meaning, not just a - number. -- Append a compact **emit rule** (once, in the narrator/system text). Instruct the model - explicitly to: - - end its reply with a fenced `state` block **only when something actually changed**; - - **omit the block entirely** when nothing changed this turn (no empty `{}`); - - include **only the stats that changed** as deltas — never restate unchanged stats, - never send full/absolute values, e.g. `{"player.hp": -15, "npc.gwen.trust": +5}`. - This keeps the emitted block tiny (saves output tokens on the free tier) and means the - engine's clamp/cooldown logic only ever sees real changes. - -### 2. Delta parse + validate (new `worldstate/` module) - -New module `backend/app/worldstate/engine.py` (mirrors `scripting/` layout): - -- `extract_delta(text) -> (clean_text, delta_dict)` — pull the trailing ```` ```state ```` - block, tolerate missing/extra fences, trailing commas, `+N` numbers; return `{}` on - parse failure (never break the turn — same philosophy as a broken script). -- `apply_delta(adventure, delta, action_index) -> report` — for each `path: change`: - 1. resolve `path` (`player.hp`, `world.day`, `npc..trust`, - `milestones.`) against the schema; unknown paths ignored (logged). - 2. **milestone path** → accept only `true`, set `{reached: true, at: action_index}`, - ignore if already reached; skip the numeric steps below. - 3. lazily instantiate NPC stat block from template if missing. - 4. reject if `cooldown` not elapsed (`action_index - _meta.last_changed[path] < cooldown`). - 5. clamp change to `max_delta_per_turn`; counters reject negative. - 6. apply, then clamp result to `[min, max]`. - 7. record `_meta.last_changed[path] = action_index`. -- Returns a report (applied / clamped / rejected) for the Insights panel, like the - script report. - -### 3. Wire into `generate_turn` (`routers/adventures.py:249`) - -- Snapshot: `world_state_before = snapshot_world_state(adventure)` alongside the existing - `state_before` (`:271`). -- After the `output` hook and empty-text check (`:340`): `clean, delta = - extract_delta(text)`, `report = apply_delta(...)`, store `text = clean`, stash the - report into `snapshot["world_state"]`. -- Persist `world_state_before` onto the AI `Action` (`:341` block) and commit - `adventure.world_state`. -- Do this only when `scenario.stat_schema` is non-empty — zero overhead for plain - narrative adventures. - -### 4. Undo / retry (`routers/adventures.py`) - -Reuse the Phase 11 wiring verbatim, in parallel: -- retry (`:449`): also restore `adventure.world_state` from the deleted AI action's - `world_state_before`. -- undo (`:489`): also restore from the first removed action's `world_state_before` - (fall back to `{}`). - ---- - -## UI - -- **World State panel** (play view): render current `world_state` as a readable sheet — - world / player / per-NPC, with band label and a bar for `min..max` stats, plus a - **milestones checklist** (reached vs pending). Reuse the collapsible-tree styling from - the existing Play State drawer. -- **Insights**: show the parsed delta + apply/clamp/reject report per turn (next to the - script report already there). -- **Scenario editor**: a `stat_schema` editor. **v1 is a raw JSON editor** (CodeMirror, - reusing the script-slot editor setup) — settled, no form builder for now. A form-based - stat builder is a possible later nicety. -- Graceful when `stat_schema` is empty: panel + editor hidden, app behaves exactly as - today. - ---- - -## Seed / demo - -- New seeded demo scenario `seed_data/*.json` with a small `stat_schema` (player hp/mana, - `day`, one or two NPC story cards with health/trust) so the feature is visible on the - live demo without the player configuring anything. Keep it `:free`-model friendly. -- Extend `seed.py` idempotently (matches existing seeder contract). - ---- - -## Exit criteria - -Play the demo RPG scenario: the World State panel shows hp/mana/day, an NPC's trust, and -a milestones checklist; taking a fight action drops hp and the narration matches the new -band; a friendly action raises an NPC's trust; completing an objective marks its -milestone reached (and it stays reached); a change larger than `max_delta_per_turn` is -clamped; undo rolls every stat and milestone back to the prior turn; a plain (no-schema) -adventure is completely unaffected. - -## Known limits (document, don't fix in v1) - -- The AI can still narrate against the numbers occasionally; injected state + firm emit - rule reduces but won't eliminate it. No post-narration consistency check in v1. -- No dice / skill checks / combat resolution — this phase is stat tracking only. A - deterministic resolver is a possible Phase 13. -- NPC stats are keyed by the schema NPC id; editing a scenario's `npcs` between play - sessions can orphan a live `npc` entry (harmless, ignored on read). -- Cooldown/`max_delta_per_turn` are per-turn heuristics, not a full rules engine. - -## Test checklist - -- Schema with `max_delta_per_turn: 30`: a delta of `-50` applies as `-30`, clamps at `min`. -- `cooldown: 2` on a stat: two consecutive changes → second is rejected until 2 actions pass. -- Counter (`day`): a negative delta is rejected; `+1` advances. -- Milestone: `true` marks it reached with `at`; a second set is a no-op; `false` ignored. -- Each NPC instantiates its own stats at `initial`; `npc..` resolves per-NPC. -- Malformed / missing `state` block → turn still completes, delta `{}`, no crash. -- A turn where nothing changes emits no `state` block (and an empty `{}` is a no-op). -- Undo after a stat change restores the prior value; retry doesn't double-apply. -- Empty `stat_schema`: no World State section injected, no `world_state` writes. diff --git a/plan/13-memory-embedding-cost.md b/plan/13-memory-embedding-cost.md deleted file mode 100644 index a693f3a..0000000 --- a/plan/13-memory-embedding-cost.md +++ /dev/null @@ -1,317 +0,0 @@ -# 13 — Memory-bank embedding cost (round three of the egress work) - -**Goal:** stop every turn fetching the entire memory bank's embeddings. Measured at -**3,024 KB per turn** on adventure 25 against **129 KB** for everything else a turn -reads — the memory bank is ~96% of a turn's database traffic, and it is fetched fresh -every single turn to pick `memory_top_k = 5` memories. - -Found 2026-08-16 while designing the story tree (see `14-phase-story-tree.md`), because -memory retrieval is the one read a tree **cannot** window — it is long-range recall by -design, so it always spans the full path. That makes this the cost floor of a turn under -the tree, which is why it lands first. - -## The measurement (production, Neon SQL editor) - -```sql -SELECT count(*), avg(json_array_length(embedding))::int, - avg(length(embedding::text))::int, - pg_size_pretty(sum(length(embedding::text))::bigint) -FROM memories WHERE embedding IS NOT NULL; --- 134 memories | 1536 dims | 30,971 bytes each | 4,053 kB total -``` - -| adventure | active | fetched | per turn | -|---|---|---|---| -| 25 | 100 | 100 | **3,024 kB** | -| 21 | 18 | 18 | 545 kB | -| 12 | 12 | 12 | 363 kB | -| 20 | 4 | 4 | 121 kB | - -Break-even against the rest of a turn is **4.2 memories**, i.e. about action 25. Every -adventure past that is dominated by this. - -`active == fetched` everywhere: nothing has been evicted yet, so the Python-side -`forgotten` filter currently costs nothing. It becomes a real leak the moment eviction -starts. - -## Why round two missed it - -`retrieve_memories` needs `settings.embedding_model`, and embedding providers are -**BYOK-only by construction** — they never touch the demo key. So the public demo never -embeds anything, and the round-two stress harness (which had no embedding model -configured) measured the turn loop with its heaviest read switched off. The 23.0 MB -figure for a 200-action playthrough is the memory-bank-**off** number; with it on, -adventure 25 is closer to 300 MB. - -**Rule going forward: any egress measurement must run with an embedding model set.** - -## Root cause - -`memorybank.py:208` - -```python -candidates = [m for m in adventure.memories if not m.forgotten and m.embedding] -``` - -Walks the relationship, so every memory row for the adventure loads with its embedding. -`embedding` is `Mapped[list]` on a `JSON` column — 1536 floats serialised as text is -~31 KB. Cosine ranking happens in Python (a deliberate choice, documented at -`models.py:144`), so all of it must cross the wire. The comment sized it by **count** -("fine at a few hundred") rather than by **bytes**. - -Not the same bug as migration 36/37 — the column is not a repeating group and there is -no denormalisation to fix. It is a *format* problem plus a *fetch-frequency* problem. - -## Decisions (settled 2026-08-16) - -- **Packed float32, keep 1536 dimensions.** 31 KB → 6 KB, a straight 5x, with **zero - retrieval-quality risk**. Explicitly rejected dropping to 512/768 dims: the size fix - plus the cache makes the extra 3x unnecessary, and it would have meant re-embedding. -- **No re-embedding.** Dimensions are unchanged, so the migration is a pure format - conversion of the 134 existing rows — read the JSON, write packed bytes, no API calls. - One-time 4 MB read. -- **In-process cache alongside the size fix**, not sequenced after it. Turns for one - adventure arrive back-to-back, so a dict keyed by adventure id takes steady-state cost - to ~0. 100 vectors as float32 is 600 KB of RAM — negligible. -- **`memory_bank_capacity` 200 → ~80.** Taken on *quality* grounds as much as cost: - ranking 200 memories to pick 5 dilutes retrieval. Note this will start evicting on - adventure 25 immediately (it sits at 100). -- **Not pgvector.** It is the structural answer and would keep vectors in the database - entirely, but it breaks SQLite dev parity — which the codebase protects deliberately - (`context/history.py:42`, the `replace()`/`trim()` dialect dance). Revisit only if the - bank grows past what Python cosine can handle. - -## Work, in order - -1. **Rebuild the byte-meter harness — in the repo this time.** `backend/tools/dbmeter.py` - + `stress_session.py`. The originals lived outside the repo and are gone. **Default it - to running with an embedding model configured**, since that omission is precisely what - hid this finding. Do this first so every item below is measured, not assumed. - **Done 2026-08-16** — see the baseline below. -2. **Migration 38 — `memories.embedding_blob` (`LargeBinary`).** **Done.** Backfill in Python - (`struct.pack(f"<{n}f", *vec)`); the conversion cannot be expressed in portable SQL, so - unlike migration 36/37 this one does pay a one-time 4 MB read. Drop the old JSON column - in a follow-up migration once verified, not in the same one. -3. **Read path.** **Done.** `retrieve_memories` queries `memories` directly with - `forgotten = false AND embedding_blob IS NOT NULL` in **SQL**, not Python. Unpack with - `struct`/`numpy`. Same for `_evict_over_capacity` and `_embed_pending`, which walk the - same relationship for a count and for the unembedded rows (see the baseline above). -4. **Vector cache.** **Done.** Keyed by `adventure_id`, bounded to 8 adventures. It - turned out to need no invalidation callbacks at all — see below. -5. **Capacity default 200 → 80.** **Done** (migration 41, only rows still on the old - default). Eviction checked at scale first: trimming a 100-memory bank to 80 costs - 0.8 kB and eight statements, and reads no vectors at all. -6. **Infinite scroll upward in `Play.jsx`** for the remaining 423 KB page load of a - finished adventure — the last open item from round two. Load the newest turns, fetch - older ones as the reader scrolls up. - -## Baseline from the harness (2026-08-16) - -`python -m tools.stress_session`, 200 actions × 4 KB, 100 memories × 1536 dims: - -| shape | fetched | memories' share | -|---|---|---| -| `GET /adventures` (index) | 0.7 kB | — | -| `GET /adventures/{id}` (page load) | 426.7 kB | — | -| `POST /adventures/{id}/actions` (one turn) | **3,258.7 kB** | 96% | -| `GET /adventures/{id}/context` (Insights) | 3,223.7 kB | 97% | -| `run_post_turn` (background) | **3,139.1 kB** | 100% | - -It reproduces both production figures independently — 426.7 kB against the measured -423 KB page load, 3,258.7 kB against 3,024 + 129 kB for a turn, and 31.4 KB per -embedding against 31.0 KB. `--no-embeddings` reports 112.0 kB for the same turn, so -the round-two blind spot is now a **29x** gap anyone can see in one flag. - -**Two findings the SQL measurement could not have shown**, both the same root cause -in a different caller: - -- **`run_post_turn` fetches the whole bank again**, every turn. `_evict_over_capacity` - walks `adventure.memories` to count the active ones, and `_embed_pending` walks it to - find the unembedded ones. So a played turn actually costs ~6.4 MB, not 3.2 — the - original estimate was half the real number. -- **Insights pays it a third time**, on a page the player can open repeatedly without - spending a turn. - -So step 3 below is not just `retrieve_memories`: **every walk of -`adventure.memories` has to go**. `_evict_over_capacity` wants a count and an ordering, -`_embed_pending` wants rows where `embedding IS NULL` — neither needs a single vector, -and both are pure SQL. - -## After (2026-08-16, same harness, same fixture) - -| shape | before | after (cold) | after (warm) | -|---|---|---|---| -| `POST .../actions` (one turn) | 3,258.7 kB | 723.4 kB | **122.3 kB** | -| `run_post_turn` | 3,139.1 kB | 0.7 kB | 0.7 kB | -| `GET .../context` (Insights) | 3,223.7 kB | 117.9 kB | 117.9 kB | -| `GET .../memories` (drawer) | ~3,138 kB | 23.6 kB | 23.6 kB | - -A played turn is turn + post_turn: **6,398 kB → 123 kB** once warm, a 52x cut. The -targets above were ~700 kB cold and ~130 kB warm, so both were met. - -Steps 2–5 landed together, because they are one deployable unit: the columns are no -use unless something reads them, and deferring them breaks the old readers. Two -additions the plan did not anticipate: - -- **`memories.embedded`, a boolean beside the blob** (migration 39/40). Once the - vectors are deferred, every "does this have an embedding?" check becomes a lazy - load — an N+1 of 6 KB reads down the Memories drawer. Same shape as - `actions.variant_count` beside `actions.variants`, and the same reason. -- **The cache needs no invalidation callbacks.** Vectors only ever change through - `set_vector`, which drops the one entry; everything that *removes* a memory from - play leaves the catalogue query, and entries missing from the catalogue are dropped - on the next read. So eviction, deletion and pruning need no hooks and cannot be - forgotten. Vectors are held as `array("f")` — 6 KB each, matching the column; - a list of Python floats would have been eight times the plan's RAM estimate. - -**Step 6 landed on 2026-08-17**, along with everything else this plan left open — see -"Closing the plan" at the end. The page load is a window of 60 actions now: 62.6 kB on -a 600-action fixture, down from 606.0 kB, and no longer a function of the story's -length. - -## Guardrails to add with this work - -- **Query-count / byte assertions per endpoint**, extending the `test_egress.py` idea: - assert an endpoint issues at most N queries and fetches under X KB against - production-sized fixtures. This class of bug is invisible at ten rows. -- **Explicit column projections on read paths.** List endpoints name the fields they - need rather than loading whole entities, so the next heavy column is opt-**in**. This - is the structural version of what `deferred=True` does by hand. -- **Row-width review rule.** Any new large column justifies itself or goes out-of-line. - `actions` now carries five JSON columns. - -Deliberately **not** taken: moving `context_snapshot` out of the database. It costs -nothing on reads now that it is deferred, and storage is ~$0.02/mo. Revisit only if -backups or storage start to hurt. - -> **Revisit it.** That call weighed egress and got egress right, but it never weighed -> the free tier's *storage* ceiling — see "Storage, which this plan did not cost" below. - -## Verification - -- Harness: `python -m tools.stress_session`, memory bank **on**, before and after, - against the baseline table above. Target for the turn shape is 3,258 kB → ~700 kB - cold, ~130 kB warm (the cache leaves only what a turn reads besides the bank). - `run_post_turn` should fall to roughly nothing: neither of its two walks needs a - vector at all. -- Re-run the round-two shapes with an embedding model configured, so the 200-action - playthrough number is finally honest. -- The existing `test_egress.py` guard must still pass — nothing here should touch the - deferred action columns. - -## Verified on production, 2026-08-17 - -Two things were still taken on trust when this shipped: every measurement had run on -SQLite, and every number came from a synthetic fixture. Both are now checked. - -### The migration landed on real Postgres - -`schema_version` reads **41**, matching the repo's `LATEST_VERSION`. The live schema has -`embedding_blob bytea` and `embedded boolean`, so the `{dialect: sql}` map in migration -38 spells BYTEA correctly against a real server — the one thing tests could not prove, -since `test_migration_38_is_spelled_for_both_dialects` only inspects the SQL string. -The backfill is complete: 134 memories, `embedded = 134`, `embedding_blob = 134`, no -stragglers and no rows skipped as malformed. - -### The 5x is real, on real vectors - -| | bytes | per memory | -|---|---|---| -| `embedding` (JSON) | 4,150,121 | 30,971 | -| `embedding_blob` (float32) | 823,296 | 6,144 | - -**5.04x**, against the plan's predicted ~31 KB → 6,144 B. The largest real bank is 100 -memories = 614,400 B of vectors, so the old code fetched **~3.10 MB per retrieval** on -that adventure — which is where the 3,153 kB measured on production came from. That -figure is now fully accounted for. - -### SQLite and Postgres agree - -`tools.stress_session` gained an `AIDND_STRESS_DATABASE_URL` escape hatch and was run -against a throwaway Neon database at the default fixture (200 actions, 100 memories): - -| shape | SQLite | Postgres | -|---|---|---| -| index | 4.1 kB | 4.1 kB | -| page load | 426.7 kB | 425.0 kB | -| one turn, cold | 723.4 kB | 722.3 kB | -| one turn, warm | 122.3 kB | **121.1 kB** | -| Insights | 117.9 kB | 116.7 kB | -| Memories drawer | 23.7 kB | 21.7 kB | -| `run_post_turn` | 0.7 kB | 0.6 kB | - -Within 0.5% everywhere. The dialect caveat in the harness docstring is real but small: -what dominates is which columns get asked for, and the ORM decides that identically. -The warm turn spends **1.7 kB on `memories`, 1% of the read** — the cache behaves on -psycopg exactly as it does on SQLite. - -### The page load is worse than modelled, for a different reason - -The synthetic fixture is **~2x heavier per action than production**: 994 B/action real -against ~2,133 B/action synthetic, so a real 200-action adventure is ~194 kB, not 427. -But the largest real adventure is **607 actions**, not 200, and costs **589.5 kB** in -one response. Step 6 is more urgent than this plan assumed, and for the opposite -reason to the one modelled — stories get *longer* than the fixture, not heavier. - -Worth fixing the fixture's narration size when step 6 lands, so the harness stops -flattering the per-action figure while understating the length. - -### Storage, which this plan did not cost - -`context_snapshot` is **150.8 MB of uncompressed JSON across 944 actions** — ~163 kB a -row on average, and ~232 kB a row in the largest adventure, against the ~74 KB/row the -comment in `models.py` claims. TOAST compresses it to ~89 MB on disk, but -`octet_length` is what would cross the wire, because Postgres decompresses before -sending. Deferral is the only thing standing between a bulk read and a 137 MB query. - -The database is **99.6 MB total**, of which `actions` is **88.9 MB**. Neon's free tier -is 512 MB. At ~94 kB of disk per action that ceiling arrives at roughly **5,400 -actions**, and 944 are already stored. So the "~$0.02/mo, leave it in the database" -call above is wrong for the tier this actually runs on — not because reads cost -anything, but because the free tier meters *storage*, and that is the constraint with -a cliff. Dropping the dead `memories.embedding` column reclaims 4.05 MB (4%), which -helps and does not solve it. - -None of the numbers above required reading a single row of anyone's content: counts, -`octet_length` sums and catalog sizes only. - -## Closing the plan, 2026-08-17 - -Everything above landed the same day the verification did. - -| | before | after | -|---|---|---| -| page load, 600 actions | 606.0 kB | **62.6 kB**, and flat in story length | -| adventures index, 6 adventures | 469.7 kB | **0.3 kB** | -| `context_snapshot` stored | ~89 MB | ~43 MB (after a VACUUM) | -| a played turn | 6.4 MB (2026-08-16) | 123 kB | - -**Step 6, the window.** `GET /adventures/{id}` returns the newest `ACTION_PAGE` -actions and the story's length; `GET /{id}/actions?before_id=` walks back from there. -Anchored on an action rather than an offset — the offset version breaks precisely when -a turn lands mid-scroll, handing the reader one action twice and hiding another — and -that choice is also what makes it survive the story tree, since it compares indices to -order a branch rather than treating them as positions. - -**Byte assertions.** `tests/test_egress.py` now carries per-action budgets as well as -column guards, plus one test whose only job is to fail if the fixture ever gets too -small for the budgets to catch anything. - -**Column projections.** `ACTION_LIST_COLUMNS` and `MEMORY_LIST_COLUMNS` name what a -list response renders, and the adventures index selects four columns instead of the -entity. That one was not just future-proofing: an Adventure carries seven text and JSON -columns the index never shows. - -**The JSON vector column is gone**, and dropping it exposed a live bug — changing your -embedding model had stopped re-embedding the bank the day migration 38 shipped. See -`tests/test_embedding_model_switch.py`. - -**Storage.** `context_snapshot` is zlib-compressed through a TypeDecorator -(`app/compression.py`, migrations 43–45), so the call sites never learned about it. -3.5x on real Postgres. **This does not shrink anything until `VACUUM FULL actions` -runs** — Postgres marks dropped columns rather than reclaiming them, and the backfill -leaves a dead tuple per row. - -Not done: nothing exercises the scroll behaviour in a browser. The frontend has no test -runner, and prepend-and-restore-scroll is the part most likely to feel wrong even when -it is correct. diff --git a/plan/14-phase-story-tree.md b/plan/14-phase-story-tree.md deleted file mode 100644 index 3847cb5..0000000 --- a/plan/14-phase-story-tree.md +++ /dev/null @@ -1,914 +0,0 @@ -# Phase 14 — Story tree (branching adventures) - -**Goal:** replace the linear action list with a **tree**. A retry becomes a sibling -rather than a rewrite; continuing from one makes it a branch. Players can go back to any -turn, take a different path, and keep both — switching between them freely. - -Depends on `13-memory-embedding-cost.md` shipping first: memory retrieval is the one read -a tree cannot window, so it is the cost floor of a turn under this design. Fix the floor -before building on it. - -## Why (beyond the feature) - -Seven bug classes stop existing, and every one traces to the same root — **the story is a -mutable list**: - -| Bug | Why it goes away | -|---|---| -| Deleting a middle action skips a later one forever (`note_action_removed`) | Cursors become node ids, not positions in a shifting list | -| `prune_dangling_memories` orphaning actions behind the cursor | Nothing is ever removed | -| Editing a summarised action leaves its memory stale forever — **currently unfixed** | Editing makes a new node; the old memory stays correct for the old path | -| The one-turn memory holdback (`settled_story_actions`) | Nothing is mutated, so nothing goes stale — the concept is unnecessary | -| The legacy-cursor no-rewind trap found while fixing that | Same | -| "Anything reading `adventure.actions` during generation must exclude the retried action" — leaked into 4 call sites | The replaced turn is not on your path; it cannot leak | -| `variant_count` / mirrored `text` drifting from `variants` | The denormalisation disappears entirely | - -It also resolves the 1NF violation: `variants` as a JSON repeating group becomes rows. - -**Scored, 2026-08-18, with SP1–SP5 shipped.** Six of the seven are gone as described. -The seventh — the one-turn holdback — is gone too, but the reasoning above was wrong -about *why* it could go: siblings share a coordinate, so replacing what a turn says still -invalidates the memory covering it. What made it deletable is that the repair already -existed for undo and delete; see the trap note below. Two rows want a footnote: - -- **Editing a summarised action** is still unfixed. The table says editing makes a new - node; nothing in SP1–SP5 makes it do that, and no subphase is scheduled to. `PATCH - /actions/{id}` still writes over the text in place, and the memory covering it goes - stale exactly as before. What *has* changed is that the machinery to fix it now exists - — an edit could write a sibling and switch to it, which is a retry the player typed — - so it is a small change whenever it is wanted. -- **The 1NF violation** is resolved. Nothing writes a `variants` array any more, in the - database or out of it — SP6 replaced the bundle that was its last producer, and the - array survives only in the v1 *reader*, which exists so files already saved still - import. - -## Design decisions (settled 2026-08-16) - -- **Full branching, with UI.** Not the "tree schema, no branch picker" middle option — - branching ships as a feature people use. -- **`branch_id` + `depth` on every node, not parent pointers alone.** Parent pointers - alone mean walking N links to read a story, which throws away the round-two windowing - work. `depth` replaces `index` as the ordering key. -- **A `branches` table with `parent_branch_id` and `fork_depth`.** The fork point is - **stored at fork time, never inferred**. Nothing is copied on a fork — a branch borrows - its ancestors' turns. -- **Lineage cached on the branch row.** `lineage = [(D,∞), (C,45), (B,30), (A,10)]`, - computed once at fork (parent's lineage + one entry). Reads never walk to reconstruct - it. Each ancestor is capped at the `fork_depth` of the branch beneath it. -- **Read the lineage lazily, windowed.** Query the newest few lineage entries, measure, - fetch more only if the context budget is not covered — the exact shape of - `history.window_covering()`. **Clause count is bounded by the context window, not by - fork count**, so a 200-fork story reads as cheaply as a 2-fork one. -- **Promote to a branch only on continue.** Attempts at the tip stay as sibling leaves; - one becomes a branch the moment a turn is played past it. Keeps the lineage chain to - "divergences I built a story on", not "every retry ever" — the difference between a - handful of entries and fifty. -- **Memories and the summary attach to the node that produced them**, found by walking - up. Shared ancestors are shared automatically, so a fork costs nothing and **nothing - needs recreating**. A memory covering depths 37–42 hangs off that branch's node 42 and - is invisible to any path not through it. Generalise the rule: *anything derived attaches - to the node that produced it* — the summary included, so stop storing it per turn. -- **Full lineage for memories, windowed lineage for the story.** Memory retrieval is - long-range recall and cannot be windowed, but memories are sparse (~1 per 6 actions), so - a long OR-clause returning ~33 small rows is fine. Two queries, one lineage. -- **Never auto-prune.** Nothing is deleted without an explicit user action. **This makes - a branch-management UI a hard dependency, not a nice-to-have** — storage grows without - limit otherwise. -- **Story cards stay adventure-wide.** A card invented on branch B shows on branch A. - Already true for undo today (script card mutations are not reverted), so this is a - documented limit, not a regression. Explicitly rejected event-sourcing card changes onto - nodes. -- **State carries over almost free.** `state_before` / `world_state_before` already - snapshot the script scoreboard and RPG world state per action, and `apply_variant()` - already restores them on a switch — a branch switch is the same move. Wrinkle: those are - *before* pictures; a node wants the *after*. And they are NULL on pre-column rows. - -## Schema sketch - -``` -branches(id, adventure_id, parent_branch_id, fork_depth, lineage JSON, created_at) -actions(id, adventure_id, branch_id, depth, type, text, reasoning, - world_delta, state_after, world_state_after, context_snapshot, created_at) -memories(..., branch_id, depth) -- attached to the node that produced it -adventures(..., head_branch_id, head_depth) -``` - -Reading branch C, tip at depth 7, lineage `[(C,7), (B,5), (A,3)]`: - -```sql -SELECT * FROM actions -WHERE (branch_id='C' AND depth <= 7) - OR (branch_id='B' AND depth <= 5) - OR (branch_id='A' AND depth <= 3) -ORDER BY depth DESC LIMIT 32 -``` - -→ `A0 A1 A2 A3 B4 B5 C6 C7`. Depth is a position along *a* path, not a global turn -number — `A4` and `B4` are alternatives, not duplicates. - -## Work - -1. `branches` table, `branch_id`/`depth` on `actions`, `head_*` on `adventures`. -2. Lineage computation + a **single module that owns the branch clause** — same role - `context/history.py` plays today. Every query must go through it; one forgotten clause - shows the wrong story, quietly. -3. `history.py` rewritten against lineage windowing. `window_covering` keeps its shape. -4. Memories/summary attached to nodes; delete the cursor-position machinery - (`position_of_index`, `settled_*`, `note_action_removed`, `_rewind_cursors_to_index`). -5. Sibling storage for un-promoted tip attempts + the promotion step. -6. Node state moves from *before* to *after* snapshots. -7. Migration: every existing adventure becomes branch A; `index` → `depth`; `variants` - entries become sibling nodes; `variant_index` becomes the head pointer. -8. Frontend: branch picker replacing `VariantPager`, plus branch management (rename, - delete, switch) — required, given no auto-pruning. - -## Open - -Nothing. The last item — **`retry_of.index` reuse**, which stopped the world-state -cooldown clock advancing on a re-run of the same turn — was closed in SP4 by reusing the -retried node's *depth*, and pinned by a test in SP5. Everything else that stood open here -was decided on 2026-08-17; see the next section. - ---- - -# Implementation plan (decided 2026-08-17) - -Four decisions, taken before any code was written: - -| Question | Decision | -|---|---| -| Phased or single migration? | **Structural-first, no runtime flag.** SP1–SP4 migrate to the tree *representation* with behaviour identical to today. Branching features layer on after. | -| How destructive is the migration? | **Keep the legacy columns for one release.** `index`, `variants`, `variant_index`, `variant_count` stay, unread, until the tree is proven live (SP8 drops them). | -| Branch-management UI scope | **Full tree visualisation** — a spatial view of forks, not just a picker. | -| Export bundle | **`ai-dnd-adventure-v2`**, with a v1 legacy reader so existing bundles keep importing. | - -**Why no feature flag.** A linear story *is* a tree with one branch, so the intermediate -states are not half-migrated — they are the same product with a superset schema -underneath. That makes "current adventures are unaffected" a literal, testable pass -condition for every subphase up to SP4, which a flag would have replaced with two live -code paths through the context builder, the memory bank, undo and retry at once. - -## The regression contract - -`tests/test_story_tree_baseline.py` (built in SP0) drives the whole product over HTTP — -create, play, retry, switch, undo, page with `before_id`, memories, summary, export — and -asserts only on API responses, never internals. - -**It must pass unmodified through SP1, SP2 and SP3.** That is the contract those -subphases are verified against. SP4 is the first subphase permitted to change it, and -even there only where the change is deliberate and named below. - -## Traps found while reading (these are the ones that bite quietly) - -- **`history._from_memory()` slices `adventure.actions`.** The "never load twice" - shortcut returns whatever is already in the session — which under a tree is *every - branch's* actions, not the path. It would silently assemble context from siblings. - This is the single highest-risk line in the change; SP2 must make the shortcut - branch-aware or delete it. -- **`scripting/pipeline.py::_history()` and `_info()` read `adventure.actions` directly**, - and hand it to user scripts as the documented history API. Same trap, user-visible. -- **`Adventure.actions` is `order_by="Action.index"`.** Ordering it by `depth` is not - enough — the collection is still every branch. Anything iterating it needs the path. -- **`limits.check_row_cap("actions")` counts every action in the adventure.** With no - auto-pruning, a branched adventure hits `MAX_ACTIONS_PER_ADVENTURE` while its *story* - is far shorter. The cap has to count the tree but be explained as the tree, or move. -- **The holdback cannot die in SP3.** `settled_story_actions` exists because retry - mutates a row in place. Retry stops mutating in SP4, so the holdback is only safe to - delete there — deleting it in SP3 reopens the exact bug it was written for. It survived - SP3 as `memorybank.settled_after`, which is the `- 1` in "how much story is past the - mark"; that subtraction is the whole of it. - - *Closed in SP4, but not for the stated reason.* Siblings share a coordinate and the - mark names the coordinate, so replacing what a turn says still invalidates the memory - covering it — retry mutating a row was never the whole of the problem. What let the - holdback go is that the right repair (`forget_node` plus a rewind) already existed for - undo and delete, and retry and a sibling switch now make it too. **If a mark still - needs correcting when the story changes, correct it; do not decline to make the mark.** - -## Subphases - -Each ships independently, on its own branch, green before the next starts. - -### SP0 — Baseline and regression net *(no product change)* - -- `tests/test_story_tree_baseline.py` — the contract above. 24 tests covering opening a - windowed adventure, the turn engine (do/say/story/continue), script effects, retry and - variant switching, undo, paging up to the start, edit/delete, memories, export/import - round-trip, and world state. - -**Done, 2026-08-17.** 259 existing tests green (17.8s), then **283 green** with the -baseline added, against unmodified `main`. That is the number every subphase below is -measured against. - -A pre-migration (**schema 45**) fixture is needed too, built by a script rather than -committed as a binary — but its only consumer is the SP1 migration test, so it lands -there. - -#### `--rich`: a correctness fixture beside the scale one - -The measuring fixture is sized from production and leaves every column it does not weigh -at its default. Checked against a freshly built one, that is exactly the set of columns a -tree has to migrate: `state_before`/`world_state_before` NULL on all 600 rows, no -scenario and so no RPG layer, no adventure scripts, both cursors 0, and 100 retry -histories whose two attempts carry **byte-identical text with `variant_index` always 0** — -so "which attempt is live?", the one question SP4's migration answers, had no observable -answer. - -`tools.stress_session --rich` fills in precisely those and nothing else: - -``` -cd backend -.venv/Scripts/python.exe -m tools.stress_session --rich --actions 30 --memories 12 -``` - -An RPG scenario built from the real seed schema with a world state played forward (hp -100 → 71, flags flipping partway); per-action `state_before`/`world_state_before` -snapshots that are monotonic, so a bad rollback reads as a wrong number rather than as -nothing; a gold script on the adventure; story cards; non-zero memory and summary -cursors with a real `story_summary`; pinned and forgotten memories; retry attempts with -distinct texts, counts of 2 *and* 3, and a live attempt that is **often not the last one -written**; and a second adventure, so "does this leak across adventures?" is answerable — -a branch clause that forgot its adventure would still look correct on a database holding -exactly one. - -It is a correctness fixture, so prefer it small: what it is for is variety per row, not -rows. **Its byte figures are not comparable to a plain run** and it does not replace the -scale fixture — the plain one still holds the egress ceilings, and was re-measured -unchanged (1.8 kB, actions 1.7 kB) after `--rich` was added. - -The invariant `text == variants[variant_index]["text"]` holds on every retried row, and -is asserted when the fixture is built. SP4's migration reads exactly that to decide which -sibling becomes the head. - -### SP1 — Schema and migration *(no behaviour change)* - -| File | Change | -|---|---| -| `app/models.py` | New `Branch` model. `Action.branch_id`, `Action.depth`. `Adventure.head_branch_id`, `head_depth`. `Memory.branch_id`, `Memory.depth`. Legacy columns untouched. | -| `app/migrations.py` | Migrations 46+: create `branches`; add columns; index `(branch_id, depth)`; backfill. Dialect map wherever BLOB/BYTEA-style spellings diverge. | - -Backfill: one branch per adventure, `lineage = [(A, ∞)]`; `actions.branch_id = A`, -`depth = index`; `adventures.head_*` from `max(index)`; memories take branch A and a -depth derived from `source_end`. - -**Verify:** baseline test unmodified. New `tests/test_tree_migration.py` — every action -carries a branch and `depth == old index`, no row lost, head pointers correct, memories -mapped. Bootstrap run twice is a no-op. `tests/test_egress.py` ceilings unmoved (two -integers a row). **Post-deploy `VACUUM FULL actions;` is mandatory** — this rewrites -every row, which is the 144 MB lesson at the top of `STATUS.md`. - -**Done, 2026-08-17** (branch `sp1-tree-schema`). **297 tests green**, the 283 from SP0 -plus 14 in `test_tree_migration.py`. The baseline contract passes **unmodified**, which -was the pass condition. Five things the plan did not anticipate, all of them found by -building it: - -- **The writes could not wait for SP2.** The file table above lists only `models.py` and - `migrations.py`, but a migration never visits a row written *after* it runs — so - shipping the columns without a writer would leave every turn played between the two - deploys with no branch, and from SP2 on a row with no branch is a row no read can see. - `app/tree.py` is that writer: `root_branch` / `head_branch` (get-or-create), - `place_action`, `place_memory`, `refresh_head`. One module for the same reason SP2 gets - one — a node written without a branch fails by *disappearing*, not by raising. Wired - into create/turn/import/undo/delete/memory, plus `seed_demo.py` and - `tools.stress_session` (a fixture built by `create_all` is stamped LATEST, so no - migration ever runs against it). -- **`adventures.head_branch_id` cannot be a foreign key.** `branches.adventure_id` - already points the other way, and the pair is then a cycle `create_all` refuses to - order; the fix for that is `use_alter`, which SQLite has no ALTER for. It is a plain - integer, documented as a cache, and `head_branch` recovers onto the root if it ever - names a branch that is gone. -- **`lineage` is NOT NULL, because `branches` comes from `create_all`.** The backfill - cannot insert a row and fill the lineage afterwards via a NULL marker, so it inserts - `'[]'` and guards step two on `json_array_length(lineage) = 0` — not `= '[]'`, because - Postgres `json` has no equality operator. -- **SQLite cannot drop a column a foreign key names.** So a current-schema database - cannot be rewound past `branch_id` at all, which broke the two existing tests that - simulate an old database by rewinding only the *stamp*. Fixed properly: - `migrations._column_already_there` makes every `ADD COLUMN` idempotent (the - `IF NOT EXISTS` the module docstring asks for and SQLite has no syntax for), and - `tests/schema_rewind.py` holds the inverse of the migrations that *can* be undone. - The SP1 fixture therefore builds a **genuine schema 45** by dropping the three tables - and recreating them from frozen pre-tree DDL, so the real ALTERs run — including the - one that adds a foreign key. -- **Deleting a branch takes its nodes with it**, and deleting an adventure takes its - branch — both verified, both at the database level via `ON DELETE CASCADE` on the two - `branch_id` columns. SP7's delete-a-branch needs no code of its own for the nodes. - -Measured: `branches` costs **0.1 kB of a 733.5 kB turn (0 %)**, and the page load -(62.6 kB) and index (1.8 kB) shapes are byte-identical to the figures in `STATUS.md`. -One cost that is *not* free: the new table and its index add ~47 ms to every -`create_all`/`drop_all` cycle on SQLite, and the suite does one per test — 20 s → 38 s. -Test-only (DDL fsync), so no model change; if it ever matters, the fix is the test -harness, not the schema. - -### SP2 — The branch clause *(reads move to lineage; still one branch)* - -One module owns the clause; every action read goes through it. A forgotten clause shows -the wrong story, quietly, which is why it is one module and not a convention. - -| File | Change | -|---|---| -| `app/context/lineage.py` *(new)* | Lineage computation and the branch clause. The only place that knows how a path is selected. | -| `app/context/history.py` | `_filters` takes the clause; order by `depth`; `window_covering` keeps its shape. **Fix `_from_memory`.** | -| `app/routers/adventures.py` | `action_window` anchors on the anchor's `depth`; `last_action`, `next_index`→`next_depth`, `_latest_narration`. | -| `app/scripting/pipeline.py` | `_history()`/`_info()` read the path, not the collection. | - -**Verify:** baseline test unmodified. `test_history_window.py`, `test_action_paging.py` -green. New test builds a **two-branch fixture directly in the DB** and asserts the design -doc's own example reads back as `A0 A1 A2 A3 B4 B5 C6 C7`, and that a sibling's nodes are -invisible. Egress: a 20-fork fixture costs within a small factor of a 1-fork one — -clause count is bounded by the context window, not by fork count. - -**Done, 2026-08-17** (branch `sp2-branch-clause`). **317 tests green**, the 297 from SP1 -plus 20 in `test_branch_clause.py`. The baseline contract passes **unmodified**, which -was the pass condition. Six things worth not rediscovering: - -- **The contract forced the write side, not the read side.** The SP0 baseline writes its - actions straight to the database and must pass unmodified — so "every writer calls - `place_action`" could not be the invariant, because the baseline is a writer and does - not. Neither did any of the eleven other test fixtures. The alternative was a read - tolerant of a NULL branch, which is the quiet-wrong-story failure this subphase exists - to make impossible. So the session enforces it instead: `tree.place_new_nodes` runs - from `Session.before_flush` and places anything unplaced, registered in `models.py` so - that importing the models arms it. The call sites keep their explicit calls — a node - placed at the call site is placed *before* the code around it reads the row back. -- **A branch could no longer be created with a flush.** `root_branch` did - `db.add(); db.flush()` to get the id its lineage names, and a nested flush inside - `before_flush` raises. It inserts through Core and reads the row back — same - transaction, three statements, once per adventure ever. -- **The identity map holds weak references, and it cost 25 % of the suite.** Resolving - the head branch per node re-read the `branches` row for every node in the flush, because - nothing held a strong reference between two calls: **201 SELECTs to write 200 actions**, - and 36 s → 45 s on the same 297 tests measured back to back. The head is now resolved - once per adventure per flush — **2 SELECTs**, and the same 297 tests then time within - noise of SP1 (44.2 s each; this machine's load drifts by ~20 % between runs, so trust - the statement count, not the stopwatch). Pinned by a test that counts reads of - `branches`, because the stopwatch is all the symptom there ever was. -- **The index screen is the one read scoped by head branch rather than by lineage.** - `_latest_narration` picks one row per adventure for a hundred adventures at once, and a - lineage clause each would put hundreds of OR-terms on that query. The two answers differ - only for a branch with no nodes of its own, which cannot exist — a branch is created by - playing a turn onto it. -- **Two reads are deliberately left un-pathed**, both documented where they live. - `max_action_index` allocates the legacy `index`, which must stay adventure-wide or two - branches issue the same number; and export is a flat v1 bundle whose reader has no idea - branches exist, which is why SP6 replaces the format rather than widening the query. - A third is a known divergence, not a decision: the index screen's `action_count` counts - the tree, and will overstate a branched story until SP5. -- **`Adventure.actions` was left ordered by `index` on purpose.** Ordering the collection - by depth would not make it a story — it is every branch's actions, and a path is a - selection out of it. What the relationship is still for is ownership and the - delete-orphan cascade. - -Measured: a story forked **20 times reads its newest 32-action window for 3,178 B against -the 2,961 B an unforked story of the same length costs (1.07×)**, naming one branch of its -22 lineage entries. The pre-tree 600-action `--keep` fixture was migrated and then driven -over HTTP end to end: index **1,840 B**, page load **64,149 B** — the same shapes as -before the phase — and scrolling to the start took 9 pages and saw all 600 actions exactly -once. - -### SP3 — Memories and summary attach to nodes - -Cursors stop being positions in a shifting list. `memory_cursor`/`summary_cursor` become -node-anchored `(branch_id, depth)`. - -Deleted: `position_of_index`, `note_action_removed`, `_rewind_cursors_to_index`, -`prune_dangling_memories`, and their call sites in `undo_turn` and `delete_action`. -**Kept until SP4:** `settled_story_actions` and the holdback (see traps). - -**Verify:** baseline test unmodified. `test_memory_settling.py` and -`test_memory_retrieval.py` updated, plus a new branch-isolation test — a memory created -on branch B is invisible from branch A, and shared ancestors are visible from both. -Memory retrieval reads the *full* lineage (it cannot be windowed) but stays sparse: -assert the byte cost on a deep fork. - -**Done, 2026-08-18** (branch `sp3-node-cursors`). **330 tests green**, the 318 the branch -started from plus 12 — 11 in `test_branch_clause`'s new sibling `test_memory_nodes.py` -and one on migration 56. The baseline contract passes **unmodified**, which was the pass -condition. `app/context/cursors.py` is the new module; migrations 53–56 add -`memory_cursor_branch_id/_depth` and `summary_cursor_branch_id/_depth` and translate the -old counts into them. - -Seven things worth not rediscovering: - -- **An anchor is a coordinate, not a pointer, and that is what deleted the machinery.** - Half of this subphase was expected to be rewriting the cursor bookkeeping in depth - terms. None of it needed rewriting: `count_after(41)` is well defined with node 41 - deleted, and deleting node 12 does not change what "past node 41" means. So - `note_action_removed`, `_rewind_cursors_to_index`, `position_of_index` and the - post-turn clamp did not become depth-shaped versions of themselves — they became - nothing. **If a mark still needs correcting when the story changes, it is still a - position.** -- **The clamp had its own trap and it also goes.** `run_post_turn` clamped both cursors - to the story length every pass, deliberately against the *full* count, because - clamping to the settled count rewound a caught-up adventure a step and re-covered an - action. An anchor past the tip is not a broken value: `settled_after` reports nothing - to do, and the story growing back past it resumes exactly where it left off. -- **`prune_dangling_memories` became a lookup, and got stricter by accident.** A memory - hangs off the node its block ends on, so "what did this node produce?" is - `(branch_id, depth)` — `memorybank.forget_node`. The scan it replaces could only ever - notice damage *after* the fact (a covered range past `max(index)`), and could not - notice at all when the node was deleted from the middle of a story that still had - later actions. Withdrawing the memory is half the job: the ground it covered is still - behind the mark, so the mark goes back to `source_start - 1` — a depth, whether or not - a row still sits there. -- **A memory with no node had to be spelled out in the clause.** A hand-written memory - summarises nothing, so it carries a branch and a NULL depth. Every ancestor entry in a - lineage clause is capped `depth <= fork`, and NULL fails that — so a typed memory would - have become invisible at the first fork after it was written, with nothing to see but a - prompt that stopped mentioning it. `Path.clause(unanchored=True)` is that case, and - actions never pass it: an action with no depth is a pre-tree row no read should see. -- **Retrieval reads the whole lineage, and it is free.** Measured on two stories of 84 - actions and 14 memories each, one flat and one forked twenty times: **1,807 B against - 1,823 B**. The clause carries 22 branch terms instead of one, and the clause is not what - crosses the wire. The egress shapes are otherwise byte-identical to SP1's — index - 1.8 kB, page load 62.7 kB, turn 733.8 kB. -- **Three reads stay adventure-wide, deliberately.** Embedding and eviction are facts - about the row and about the bank, not about the path — skipping a sibling's memories - would only mean embedding them at the moment somebody switched to them, and evicting the - memories of a story nobody is reading is the right thing to evict first. The Memories - drawer is management rather than retrieval, and hiding a branch's memories there would - make them unfindable in a phase whose rule is that nothing is removed automatically. -- **The v1 bundle still speaks positions, in exactly two places.** Export counts the - anchor back into a position; import translates the other way, but only after the - actions exist, because that is the one moment the two coordinate systems can be lined - up. `cursors.position_of` and `cursors.anchor_at_position` are the whole of what still - knows about positions, and SP6's v2 format retires them. - -Migrations 53–56 rewrite `adventures`, not `actions` — a few hundred rows against a few -hundred thousand — so **this deploy needs no `VACUUM FULL` of its own**. The one SP1 owes -is still owed. - -### SP4 — Variants become sibling nodes - -Retry stops mutating a row. It writes a sibling leaf at the same depth. - -Deleted: `set_variants`, `variant_of`, `apply_variant`, `VARIANT_SNAPSHOT_KEYS`, and the -holdback. Node state moves from *before* to *after* snapshots (`state_after`, -`world_state_after`) — a sibling needs its own outcome, so this belongs here rather than -in its own subphase. Pre-column rows are NULL and must stay tolerated. - -A migration converts existing `variants` JSON into sibling rows — reading the legacy -column that decision 2 kept, which is the whole reason it was kept. - -`/variants` and `/variant` keep their URLs and response shapes here, re-implemented over -sibling rows, so the frontend keeps working until SP7 replaces it. - -**Verify:** the first subphase allowed to move the baseline test, and only for -`variant_count`/`variant_index` semantics — the retry *outcomes* must not move. -`test_retry_variants.py` rewritten against siblings but asserting the same observable -results, including that the gold script is not double-applied. `test_state_revert.py` -against after-snapshots. Migration test: attempt count preserved, the active attempt -becomes the head. - -**Done, 2026-08-18** (branch `sp4-sibling-nodes`). **347 tests green**, the 330 SP3 -finished with plus 13 in the new `test_attempt_siblings.py`, 8 in `test_tree_migration -.py`, and three holdback tests deleted. `app/attempts.py` is the new module; migrations -57–60 add `live`, `state_after`, `world_state_after`, derive the after-snapshots, and -split every `variants` list into rows. - -**The baseline test did not have to move, and neither did `test_retry_variants.py`.** -Both pass unmodified. SP4 was *permitted* to change the variant-count semantics and it -turned out nothing observable needed changing — which is the strongest form the pass -condition could have taken, and worth knowing before SP6 asks for the same licence. - -Seven things worth not rediscovering: - -- **A coordinate needs a `live` flag, and the branch clause is where it belongs.** - Siblings share `(branch_id, depth)`, so the lineage clause alone returns all of them - and the story tells itself twice. `Path.clause` adds `Action.live` for actions (and - only for actions — memories have no siblings to lose to), which means no read of the - story had to learn that retries exist. `app/attempts.py` is the only code that looks - past it. -- **The prompt has to move with the flag or retry becomes a storage multiplier.** A - `context_snapshot` is ~163 kB of prompt that every attempt at a turn shares, and the - old JSON list existed precisely to store it once. Giving each sibling row a copy would - have undone that. So the invariant is: the assembled prompt lives on the attempt the - story tells, and a superseded one keeps only its own slices (`ATTEMPT_KEYS` — the - world-state delta, the script report, the raw reply). Measured on the pre-tree - 600-action fixture, migrated: **700 rows for the same 600-turn story, and the prompt - archive byte-identical at 0.50 MB.** -- **The holdback was not quite unnecessary — it was the wrong repair.** The plan said - retry stops mutating rows so nothing goes stale. Not so: siblings share a coordinate, - and the mark and the memory both name the *coordinate*, so replacing what a turn says - still invalidates them. What made the holdback deletable is that the right repair - already existed — `forget_node` plus a rewind, which undo and delete have called since - SP3. Retry and a sibling switch now call it too, and the holdback (`settled_count`, - `settled_after`, `settled_story_actions`, `newest_settled`) is gone. **A memory can - now cover the newest action**, which it never could before. -- **`state_after` needed the same flush guard `place_action` has.** The migration - derives every existing row's outcome from the next row's `state_before`, and the tip's - from the adventure's live state — but a row written by a fixture or a script after - that has nobody to derive from, and the failure mode is undo silently leaving the - scoreboard where it was. `tree.stamp_outcome` runs from `before_flush` beside - `place_new_nodes`. It writes the truth as of the flush: a writer that changes no state - between two nodes leaves the same state behind both. -- **Switching attempts hands the caller a different row id**, because that is what an - attempt being a node *means*. The endpoints are addressed by any attempt at the turn - rather than by the live one, so a client holding a stale id still asks about the right - turn — but `Play.jsx` matched the reply against `updated.id` and had to be changed to - match against the action it asked about. One line, and SP7 removes the pager anyway. -- **Deleting a turn deletes its attempts.** A discarded attempt is only reachable - *through* its coordinate, so leaving it behind would leave a row nothing can name and - no read can see. `delete_turn` is that rule in one place, called by undo and by - delete-an-action, and it works whichever attempt's id the caller happens to hold. -- **Siblings share the legacy `index`.** They are takes on one turn, so two rows now - carry one index — which `max_action_index` (a maximum, not a count) survives, and - which is what lets the v1 export fold a group back into a `variants` array. Export is - now the only producer of that shape anywhere; nothing in the database holds one. - -Measured: index **1.8 kB**, page load **62.7 kB**, one turn **734.8 kB** — the first two -byte-identical to SP1's and SP3's, the turn up 1.0 kB (0.14 %) for the `live` column -across a 346-row read. Migrations 57–59 each rewrite every row of `actions` and 60 -inserts one per discarded attempt, so **this deploy owes a `VACUUM FULL actions;`** — -the SP1 one is still owed too, and one vacuum after this deploy settles both. - -### SP5 — Fork on continue - -Playing past a non-head sibling promotes it: a `branches` row with `parent_branch_id`, -`fork_depth`, and a `lineage` computed once from the parent's. Nothing is copied. -`adventures.head_*` moves. **The cooldown clock must not advance on a re-run of the same -turn** — the one item still open above. - -**Verify:** a fork creates exactly one branch row and copies no actions; lineage is -correct and capped at each `fork_depth`; both branches read independently; switching -restores the right script/world state; the cooldown test from `test_worldstate.py` -still holds across a retry. - -**Done, 2026-08-18** (branch `sp5-fork-on-continue`). **365 tests green**, the 347 from -SP4 plus 18 in `test_branch_forking.py`. `tree.fork` is the whole of it; three endpoints -(`GET /branches`, `POST /branches/{id}/switch`, `POST /actions/{id}/fork`) are what SP7's -tree view will be drawn on. - -**Where the promotion happens, and why not where the plan said.** The plan put it on the -*next turn*: attempts stay leaves, and one becomes a branch when a turn is played past -it. Promoting the winner then means moving a row off the branch the reader is standing -on and leaving that branch to pick a new node for the depth — it disturbs a story nobody -asked to change. The same divergence, seen from the other side, promotes the attempt you -are *leaving for*: `POST /actions/{id}/fork` gives the discarded attempt a branch and -moves the head to it, and the line it leaves is untouched. Branch count is identical -either way — one per divergence somebody actually built on — and one of the two never -rewrites a story in place. Without it, the losing attempts would also be unreachable -forever, since only the tip can be switched: fork-on-continue alone is a one-way door. - -Six things worth not rediscovering: - -- **A fork must not move the derived work, and it was about to.** The first cut moved the - memories at the forked coordinate onto the new branch and re-anchored the cursors that - named it. Both are wrong, and for the same reason: a memory describes whichever attempt - was *live* at that coordinate, which is the one staying on the parent. The right answer - needs no code at all — the lineage caps the parent at `fork_depth`, one depth short of - it, so the memory is simply out of range from the fork, invisible to both the retrieval - clause and `Path.depth_on`. The block is summarized again, from the text this branch - actually tells. -- **Depth had to stop following `index`.** They agreed until now. `index` is - adventure-wide (the v1 bundle is keyed on it) so on a story forked at depth 6 after - twenty turns it hands the next node depth 21 and leaves a fourteen-deep hole in the - middle of a path — which every windowing estimate then has to work around. - `next_depth` is `head_depth + 1`; `place_action` still derives depth from index for - fixtures and imports, which is what that default is for. -- **Undo has to stop at the fork.** It reads the newest two nodes on the path and deletes - the turn they make up — and on a fresh branch the second of them is borrowed from the - parent, whose story also contains it. The guard is on the row's own `branch_id`, not on - the fork depth, because that is the fact that decides it. -- **The cooldown clock came out right for free.** It lives in `_meta.last_changed` inside - the world state, and the world state is restored from the tip's `world_state_after` on - every switch — so each branch carries its own clock without anything knowing there is - one. The carried-over open item (a retry must not advance it) is SP4's reused depth, - and both are pinned by tests. -- **The session does not autoflush**, and `fork` read the sibling group after moving the - node out of it — so the move had not been written and the node was renumbered straight - back into the group it had just left. Read the group first. (`autoflush=False` is - deliberate, in `database.py`; anything in this phase that mutates then queries the same - rows has to order itself by hand.) -- **`/fork` has to be idempotent before it is anything else**, because a fork leaves the - promoted attempt alone on its branch: a repeated call — a double click, a retried - request — would otherwise be told the turn it just forked has nothing to fork to. The - "already the story" case is answered before the shape of the turn is looked at. - -Measured, on a 40-turn story forked **twenty** times against the same story flat: -21 branches, 140 rows, an 80-action story, and a page load of **31,652 B against -31,433 B (1.007×)**. A branch costs **103 B** of id, parent, fork depth and cached -ancestry. No migration, no vacuum. - -**One gap, deliberately left for SP6.** A forked adventure has no honest `v1` export — -the format has one story and there are two — so export emits every branch's turns -interleaved by index, which reads as a mangled story rather than as lost data. SP6's v2 -bundle fixes it, and SP7 is where a player first gets any way to fork at all, so the -order those two ship in is the order that matters. - -**Also known, and not fixed here:** the two cursors are one pair on the adventure, so -switching branches makes the mark on the branch being left unreadable from the new one -(`Path.depth_on` answers "nothing covered", which is the safe direction — redo the work, -never skip it). Switching back and forth therefore re-summarizes. Per-branch cursors are -the fix if it ever matters; it costs AI calls, not correctness. - -### SP6 — Export/import v2 - -`ai-dnd-adventure-v2` carries branches and nodes; import accepts v1 and v2, mapping a v1 -bundle's linear actions plus `variants` onto one branch with siblings. -`limits.check_bundle_lists` learns about branches. - -**Verify:** v2 round-trip of a branched adventure is lossless; a v1 bundle still imports; -a bundle claiming more branches than rows is rejected rather than half-applied. - -**Done, 2026-08-18** (branch `sp6-bundle-v2`). **381 tests green**, the 365 SP5 finished -with plus 16 in the new `test_bundle_v2.py`. `app/bundle.py` owns both formats; the two -endpoints in `routers/adventures.py` are a delegation and the shared plumbing, and the -`variants` array now exists nowhere but the v1 *reader*. - -**The rule the module is built on: a bundle carries what was *chosen*, never what is -*derived*.** The head branch, the fork points, the live flags and the anchors are -decisions somebody made, and they are in the file. `lineage`, the head *depth*, `index` -and the variant ordinals are computed from those and are rebuilt on the way in. That is -not tidiness — a bundle is a text file anybody can edit, and every derived field shipped -beside its source is a chance for the file to disagree with itself in a way no read would -report. It is also the answer to "is the round trip lossless?": everything omitted is -reconstructed, and the tests assert the reconstruction rather than the bytes. - -Six things worth not rediscovering: - -- **`depth` cannot be the legacy `index`, and this is where that stops being academic.** - They agreed until SP5, and a bundle is the first writer that has to fill `index` for a - *forked* story — where two branches both have a node at depth 4. `index`'s one - remaining job is handing the next row a number nothing else holds, which is a fact - about the adventure rather than about a path, so the import allocates one per turn in - bundle order: siblings share it, the way SP4 leaves them, and no two coordinates do. -- **Validation happens before the adventure row exists.** Everything a hand-edited file - can get wrong about the shape of a tree — a node naming a branch that is not listed, a - fork with no depth, a branch forking from one listed after it — is a 400 raised by - `plan`, which touches no session. The alternative is an adventure holding a story with - a hole in it, and this whole phase exists because a story with a hole in it fails by - going quiet. -- **A branch may only fork from one listed before it.** That is how the export writes - them, and requiring it buys acyclicity for the price of a comparison — a cycle in the - parent chain would be an import that never returns rather than one that fails. -- **`{}` and absent are different snapshots.** An empty `state_after` means "this node - left an empty scoreboard behind"; a missing one means "nobody knows, leave the live - state alone" (`attempts.restore_state`). Trimming empty dicts on the way out would have - saved eighteen bytes a row and turned an undo that clears a score into one that leaves - it standing. Only `worldDelta`, which is display, is dropped when empty. -- **A file is allowed to be wrong about which attempt is live, and the import corrects it - rather than refusing.** "Exactly one sibling in a group is live" is an invariant of the - *database*, not of the format; a coordinate with none is a turn no read can see, so the - first attempt is made live. That is a different class from a missing branch, which is - structure, and is refused. -- **`limits.MAX_BRANCHES_PER_ADVENTURE` (1000) has no live counterpart.** Forking is a - POST that adds one row and has no cap of its own, so this is the one bundle cap that - does not mirror something creation enforces. Worth closing if branch management ever - makes forking cheap to repeat. - -**The verify line above was slightly wrong, and the code does the honest version.** "More -branches than rows" fails on an adventure with no actions, which legitimately has one -branch and no rows. It became two rules instead: a cap on the branch list, and *every -branch a node names must exist*. - -Measured with `tools/measure_bundle.py` on the 600-action `--rich` fixture — 600 turns, -750 nodes, because 150 of them were retried: - -| | bytes | vs v1 | -|---|---|---| -| v1 shape | 587,475 | — | -| **v2** | **911,229** | **1.551×** | -| v2 without the outcomes | 544,318 | 0.927× | - -**The tree is free; the outcomes are what cost.** Coordinates *save* 57.5 B a node -against v1's turn-and-variants shape, and the entire 1.55× is `state_after` / -`world_state_after` at 489 B a node — which are there because a bundle without them -imports a tree nobody can switch inside. Twenty forks add 660 B to the same file, **33 B -a branch**, so the format is as indifferent to fork count as the reads are. The longest -adventure production holds exports at 4.3 % of `MAX_IMPORT_BODY_BYTES`. - -**Two lines of the SP0 baseline changed, not the one it predicted.** The note in -`test_story_tree_baseline.py` allowed for the `format` assertion; the `variants` array in -`test_export_keeps_retry_attempts` is the same fact from the other side — a bundle with -coordinates has no use for a repeating group. Everything else in that file still passes -unmodified. - -**No migration, no vacuum.** SP6 adds no column and rewrites no row. - -**And one trap, paid for once.** `tools/measure_bundle.py` imported `app` before -`tools.stress_session`, which is what points `AIDND_DB_PATH` at a throwaway file — -`app.database` reads it at module scope. It fails by *working*: the first run seeded a -synthetic user and adventure into the local `backend/data.db` and printed perfectly good -numbers, and only the second run tripped over the unique email. Anything importing that -harness must import it first, and the file now says so where the imports are. - -### SP7 — Frontend: the tree becomes reachable - -`VariantPager` is removed. A branch view replaces it, plus switch, rename and -delete-with-confirm. `api.js` gains the branch endpoints. - -**Verify:** this is where the standing open gap gets closed — **drive the 600-action -`--keep` fixture in a browser by hand**, on the same scroll path that has never been -driven and already hid one bug. A vitest + jsdom harness covers the prepend arithmetic; -jsdom has no layout, so scroll position still needs eyes. - -**Done, 2026-08-18** (branch `sp7-tree-ui`). **396 tests green**, the 381 SP6 finished -with plus 15 in the new `test_branch_management.py`. Driven by hand against the `--keep` -fixture in Chrome, which is how the one bug below was found. - -**Shape chosen: a branch rail, not a spatial node map.** Three mockups were built and -compared before any of it was written, and the deciding argument was not aesthetic. A -node map is a second windowing problem — the fixture this subphase must be verified -against is 600 actions, which is 600 nodes — so building one would have spent SP7 on the -thing that delays the verification SP7 exists to do. The rail ships now; the map is a -later feature and costs nothing extra to add, because both draw from the same -`GET /branches`. The panel sits beside Plot/Memory/Scripts/Insights, which is this app's -existing idiom for a right-hand rail rather than a new one. - -**SP7 was not a frontend-only subphase, and the spec above did not say so.** Of the three -operations it names, SP5 had built exactly one. `switch` existed; `rename` had no column -and no route, `delete` had no route at all. So it opens with migration 61 -(`branches.name`), a `PATCH` and a `DELETE` — worth remembering for any future subphase -whose one-line spec says "plus the UI for X". - -Five things worth not rediscovering: - -- **A name is stored; a label is derived.** `branches.name` is NULL until somebody - chooses one, and the client draws an unnamed branch from its fork depth - (`Fork at moment 547`). A generated "branch 4" in the column would be a lie the moment - branch 3 is deleted and the ordinals shift under it; a fork depth is a coordinate, and - nothing can shift it. The v2 bundle carries the name for exactly the reason SP6 gives - for carrying the fork points — it is a decision, not something computed from one. -- **Refusing to delete the head is only half of it.** The other half is refusing any - branch the head was *forked from*: `parent_branch_id` cascades, so deleting an ancestor - takes the head with it and leaves `head_branch_id` pointing at a row that is gone. One - membership test against the head's own `lineage` covers both, because a lineage already - names itself and every branch it borrows from. -- **A deleted branch's cursor has to be cleared, and the reason is SQLite.** On Postgres - a stale branch id simply never resolves. SQLite hands the freed id to the next fork, at - which point the anchor resolves onto a branch it has never seen and reports a stretch - of story as already summarized — losing it from the memories for good. Same class as - the width-mismatch `cosine` returning 0.0: it reports nothing. -- **`VariantOut` had to grow an `id`.** A fork is addressed by the node being taken. The - group renumbers whenever an attempt is added, so an ordinal held across that points at - a different take — the same reason SP4's note called the pager's index match "one line, - and SP7 removes the pager anyway". -- **Four panels reloaded on the wrong thing, and only a browser could say so.** - `actions.length` was the refresh key for the Branches panel, the Status drawer - (script state), Insights and the Memory Bank. **A branch switch does not change the - length of the story** — it changes which story it is. So the Branches panel drew a - one-branch tree while the reader was already on a second, Insights showed the prompt - for the path just left, and the scoreboard kept the other line's numbers. The server - was correct throughout, and nothing in 396 tests could see any of it. All four now key - on `${actions.length}:${stateKey}`, and `stateKey` is bumped by `adoptWindow` — the two - operations that move the head — plus branch deletion, which removes that branch's - memories without a turn being played. - - `tools/branch_fixture.py` exists because of this: it builds two branches of **equal - path length**, which is the case `actions.length` cannot distinguish at all. The - `--keep` fixture could not have found it, and neither could a fixture whose branches - happened to differ in length. - -**What a switch puts back was checked end to end, not just server-side.** SP5 already -proved `restore_state` in `test_switching_restores_the_script_and_world_state`; what had -never been looked at is whether the *screen* re-reads it. On `tools/branch_fixture.py`, -switching between the two branches moves the World State drawer from **hp 60 to hp 95** -live, redraws the bar, swaps the story to the other take (`hp -5`, not `hp -40`) and -repoints Insights at the other path — "History: 5 of 5 actions", carrying the scratch and -not the beating. - -**Every memory is now a memory of a story, not of an adventure — migration 62.** A -hand-written memory used to carry a NULL depth, described as "belongs to the adventure -rather than to a path". That reads as harmless and is not: a NULL is a coordinate no fork -can cap, so a note typed on one line followed the reader onto branches whose events it -never described. `tree.place_memory` anchors it at the head instead — *the story you were -reading when you wrote it* — and it then obeys exactly the rule a summarised memory obeys. - -The whole `unanchored` escape clause in `lineage.Path.clause` existed for that one case -and is **deleted**, not merely unused. Its docstring argued a capped `depth <= fork` would -drop a typed memory "the moment its branch stopped being the newest entry"; anchoring is -the better answer to the same worry, because the memory is not exempt from the path, it is -*on* one. - -**The drawer shows the path being read, and nothing else.** Same clause as retrieval, so -the bank you can see is the bank the model can see — one question, one answer. The earlier -attempt at this shipped an adventure-wide list with an `on_path` flag and an *another -branch* badge; anchoring makes that redundant, and the field, the badge and its CSS are -gone. Nothing is stranded by hiding: a memory lives on a branch, switching to that branch -shows it, and deleting the branch deletes it -(`test_deleting_a_branch_deletes_the_memories_written_on_it`). - -Pinning is unchanged and still path-scoped: it decides *order*, the path decides -*existence*. A pinned memory on a branch you are not reading is not sent, because the path -clause runs before pinning is considered. - -**Migration 62 anchors existing NULL-depth memories at depth 0 of their branch**, not at -the tip. 0 is at or before every fork point, so every memory stays visible from exactly -the paths it is visible from today — the anchor takes nothing out of anybody's bank on -deploy. Anchoring at the tip would have emptied them out of every branch forked earlier -than they were typed, on a database with real users on it. - -**The scroll path was driven, and it holds.** Three prepends on the 602-action fixture, -60 actions and ~16,200 px each. The same DOM node stayed at viewport top 792 → 787 — a -**5 px drift** across the prepend — and the view stayed 48,174 px from the bottom, so PR -#2's throw-to-the-end does not reproduce. Console clean. One note for anyone measuring it -again: the fixture's prose repeats, so an anchor found by matching *text* lands on an -older copy of the same sentence and reads as a huge jump. Hold the DOM node. - -**Still open, deliberately:** no vitest + jsdom harness. The verify line offers -hand-driving *or* the harness and this took the first. The harness remains the thing that -would catch a prepend regression without a person in the loop, and jsdom's lack of layout -means it would not have settled the 5 px question either way. - -**One user-visible change nobody asked for, recorded here because a script -author would otherwise find it by being surprised.** Moving `_history` onto -`context_history.story_actions` fixed the branch bug it was there to fix, and -carried a second change with it: `story_actions` drops blank-text rows, which -`adventure.actions` did not. So a user script's `history` array and -`info.actionCount` both got shorter for any adventure holding one. It is the -right shape — a textless row is bookkeeping with no AI Dungeon counterpart, and -the prompt never included one — but a script that fires "every N actions" now -fires on different turns. Not compatible with both readings; this is the one -chosen. - -### SP8 — Drop the legacy columns - -Only once the tree is proven live. Migration drops `index`, `variants`, `variant_index`, -`variant_count`, followed by `VACUUM FULL actions;`. **Also `adventures.memory_cursor` -and `summary_cursor`** — unread since SP3, kept only so a rolled-back build resumes from -a real number. They are on `adventures`, so dropping them costs no vacuum. - -**And `actions.state_before` / `world_state_before`**, unwritten and unread since SP4 for -the same reason: a rolled-back build still finds a real snapshot on every row it wrote -itself. They are deferred JSON on `actions`, so they go in the same rewrite as `index` -and cost nothing extra. - -`variant_count` and `variant_index` are the two to check before dropping: SP4 left them -as a maintained cache of the sibling group's shape, because the pager reads both for -every row of a page and must not pay a query per turn to get them. They are dead only -once SP7's tree view has replaced the pager. - -**Verify:** full suite; egress ceilings; a measured before/after size, aggregates only. - -### SP9 — Takes, not chips: one pager, and a fork that makes a new take - -**Why this exists.** SP7 was driven by hand and the tree was unusable. Three things were -wrong, and only the third is a bug: - -* **A chip did two different things.** At the tip it switched; above the tip it only - *previewed*, and taking it needed a second button. One control, two meanings, and the - meaning depended on where the player was standing. -* **Nothing could fork but an AI turn that already had a second take.** - `POST /actions/{id}/fork` answers 400 when the turn has one take, so a player's own - message had no way to become anything else and "branch from here" did not exist. -* **Retry only worked on the newest action.** Mid-story there was no retry at all. - -**The model, in the player's words.** Every action can gain another *take*. On an AI node -that means regenerate; on the player's own it means type something else. Stepping between -takes is free — `3/3` to `1/3` is navigation, and the story below simply empties, because -that take has no children yet. **A branch is created when you write below a take that is -not the live one**, never before. - -That collapses SP5's fork and SP4's retry into one operation and deletes the -tip-versus-past distinction from the UI entirely. It survives only in the implementation, -where it decides whether a write needs a branch at all. - -**Takes are grouped by parent, not by coordinate.** This is the load-bearing change. -`attempts.group()` filters `branch_id == … AND depth == …`, and the player's own example -breaks it: - -``` -B ── C C1 C2 <- three takes, one parent (B) - │ └── D1' D2' <- two takes, parent C2 - └── D1 D2 D3 <- three takes, parent C1 -``` - -Standing on the C2 path at that depth must read `2/2`, not `5`. Coordinate grouping gets -that right by accident — writing under a non-live take forks, so the two sets land on -different branches. It gets `C` wrong: once C is forked onto its own branch it is alone -at its coordinate and reads `1/1`, losing C1 and C2 from a pager that must still say -`1/3`. - -**Decision: add `actions.parent_id`.** The alternative — making a branch's fork point a -*node* instead of a depth, so a promoted take never moves — was rejected. `lineage` exists -precisely so a read is an OR-clause per branch rather than a walk up parent pointers, and -re-pointing the fork at a node changes path resolution itself, which drags in `cursors`, -memory depths and both bundle formats. `parent_id` is read only to group a turn's takes: -one indexed lookup, never a walk, and nothing about how a path resolves changes. Per SP2's -sizing note an integer beside `depth` is cheap, and the backfill rewrites a heap measured -at 1.7 MB on 2026-08-18. - -**`variant_count` / `variant_index` are reprieved, not revived.** SP8 was going to drop -both; the pager needs the group's shape again. It needs it *per parent*, which is not what -either column caches, so SP8 still drops them and SP9 computes the shape from `parent_id`. - -**Verify:** the SP0 baseline passes unmodified — a linear story has one take per parent, -and none of this is reachable without a second one. Plus: `2/2` under one take while its -sibling holds `3/3`; a pager that still reads `1/3` after its take has been forked onto its -own branch; a write below a non-live take forks exactly once; a fork on a player's own -message generates a reply. - -**Owed:** a migration adding one column, and a `VACUUM FULL actions;` after it. - -## Standing constraints - -- **No production data is read at any point in this phase.** Migrations, the e2e - baseline and every egress measurement run on local SQLite and the synthetic `--keep` - fixture. If a real Postgres is ever needed for a write path, it is a throwaway - database whose name contains `stress`/`scratch`, and it is asked for first. -- **Any migration that rewrites `actions` is followed by `VACUUM FULL actions;`** on the - direct endpoint, not `-pooler`. SP1, SP4 and SP8 each rewrite every row. **As of - 2026-08-18 two are owed** (SP1's and SP4's) and neither has been deployed; one vacuum - after the SP4 deploy settles both. diff --git a/plan/15-pokemon-demo-handover.md b/plan/15-pokemon-demo-handover.md deleted file mode 100644 index 7ab8344..0000000 --- a/plan/15-pokemon-demo-handover.md +++ /dev/null @@ -1,117 +0,0 @@ -# Pokémon League Championship demo: playtest handover - -Read this before you resume work on the demo scenario. It covers the current -state of `05-league-championship.json`, what a live 10-turn playtest on -production confirmed, and two bugs the playtest found. - -**Last updated: 2026-08-28.** - -> **Superseded in part on 2026-08-28.** Both bugs below were investigated against the -> real data and **both root causes named here are wrong**. See -> `plan/16-world-state-refusals.md` for what was actually happening and what was -> changed. The playtest record and the "Confirmed working" section still stand. - ---- - -## Where things stand - -The scenario is live at `https://ai-dnd-1gmp.onrender.com` as -**"[Demo] League Championship: Round One"**, seeded from -`backend/app/seed_data/05-league-championship.json`. It replaced an earlier, -weaker draft titled "Road to the Champion" — that old scenario and its stale -test adventure (adventure id 42) are still in the production database. Delete -them by hand from `/scenarios` and `/adventures` when convenient; a delete -click froze the browser tab during this session behind what looked like a -native confirm dialog, so budget time for that if you try again. - -The schema nests all five of the player's Pokémon and Milo's Pokémon under -`npcs`, not flattened into `player._hp` fields. Each npc entry carries -its own `stats` map (`hp`, `status`, and for Milo, `active_pokemon`, -`active_hp`, `active_status`, `pokemon_left`). `player.active_pokemon` is a -text stat that names whichever of the player's Pokémon is currently out. This -design survived a real playtest: see "Confirmed working" below. - -Settings on the test account now point at the user's own OpenRouter key, -endpoint `https://openrouter.ai/api/v1`, model `deepseek/deepseek-v4-flash-0731`, -reasoning budget `-1`. The shared demo key's model -(`google/gemma-4-26b-a4b-it:free`) was hitting persistent 429s from OpenRouter -capacity, not from the app's own rate limiter — switch back to it only after -confirming that model isn't still rate-limited. - -## Confirmed working: a live 10-turn playtest - -Adventure id 43, played turn by turn against DeepSeek V4 Flash on production. -Milo's Graveler and Onix both fainted; Kabutops came out third. Across ten -turns: - -- HP tracked correctly on both sides, including sandstorm chip damage each - turn once `sandstorm_active` flipped on. -- `status` stayed correctly independent per Pokémon (all `none` throughout - this run — no status move was tried). -- `player.active_pokemon` and `npc.milo.active_pokemon` both switched - correctly as Pokémon were sent out or fainted (Pidgeotto → Wartortle → - Machoke; Graveler → Onix → Kabutops). -- `player.potions` decremented correctly on use (3 → 2) and the healed - Pokémon's HP rose by the expected amount. -- The World State sidebar reflected every one of these changes live, without - a manual refresh, after the model's reply finished streaming. - -This confirms the nested-`npcs` redesign from earlier in the session was the -right fix for "why did you flatten then" — parallel entities with independent -stats work as npcs, not as flattened player fields. - -## Two bugs the playtest found - -**1. The model sometimes skips the trailing `state` block entirely.** - -> **Unconfirmed.** Truncation at `max_output_tokens` removes the block too, and it -> was never ruled out here. See `plan/16`. -On the very first turn of this run, DeepSeek V4 Flash narrated a Wartortle HP -drop but never appended the ` ```state ` block the engine parses. The engine -correctly left the state untouched — this is model non-compliance, not an -engine bug — but the drop was silent: no error, no visible sign in the UI -beyond "the numbers didn't move." A `Retry` on that same turn produced the -delta correctly. Confirmed by reading the raw action row over -`/api/adventures/{id}/actions` — `hasState` came back `false` on the first -attempt and `true` on the retry. If this happens often during your own play, -it is worth an authors'-note reminder or a stronger trailing-instruction -nudge in `engine.py`'s prompt scaffolding, not a schema change. - -**2. The model reliably forgets `npc.milo.pokemon_left` and milestones on a -faint, despite an explicit instruction to update both.** - -> **Wrong on both halves.** The model emitted `pokemon_left` at *both* faints; the -> engine clamped it to nothing and reported it as applied. And it never emitted a -> milestone because the milestone ids were absent from the prompt entirely. Neither -> was an attention problem. See `plan/16`. `ai_instructions` in -the scenario file already says: *"decrement `npc.milo.pokemon_left` when one -of his faints"* and *"Mark milestones as they happen."* Across two separate -faints in this session (Graveler, then Onix), the model correctly reset -`active_pokemon`/`active_hp`/`active_status` for the incoming Pokémon every -time, but never once touched `pokemon_left` (stuck at `3/3` through both -faints) and never checked off "Knock out Milo's lead Graveler," even though -that milestone was unambiguously satisfied. This looks like the faint -instruction is buried inside a longer bulleted list the model is only -partially attending to. Worth trying: pull the faint-handling instructions -into their own short paragraph, or add a stat-guide line for `pokemon_left` -and the milestones that makes them as visually prominent as `hp`/`status`. - -**Separately, not necessarily a bug:** `world.turn` (a `counter` stat defined -in the schema) stayed at `0` for all ten turns. `ai_instructions` never tells -the model to increment it — the instructions cover HP, status, potions, -active_pokemon, and pokemon_left, but not turn. If you want the counter to -mean something, add an explicit line telling the model to bump -`world.turn` by 1 every reply. - -## Suggested next steps - -1. Decide whether to patch `ai_instructions` for the two gaps above, then - redeploy and play a few more turns to confirm faints correctly decrement - `pokemon_left` and flip milestones. -2. Clean up the stale "Road to the Champion" scenario and adventure 42. -3. Try a status-condition move (Ivysaur's Poison Powder or similar) — this - playtest never exercised the `status` stat changing away from `none`, so - it is unverified in practice even though the schema supports it. -4. If DeepSeek keeps skipping state blocks more than rarely, consider the - trailing-reminder wording in `engine.py` (`build_state_reminder` or - equivalent) rather than switching models — the schema itself is sound. diff --git a/plan/16-world-state-refusals.md b/plan/16-world-state-refusals.md deleted file mode 100644 index 108b75f..0000000 --- a/plan/16-world-state-refusals.md +++ /dev/null @@ -1,343 +0,0 @@ -# World-state refusals: what the engine throws away, and who gets told - -Read this before you play the Pokémon demo again. It records why two bugs in -`plan/15-pokemon-demo-handover.md` were diagnosed wrongly, what the engine was -actually doing, and what changed. Everything here is merged and green. It **has** now been driven in a browser; -read "Driven in a browser" at the end first, because three of the five changes -did not work and one bug explains all three. - -**Last updated: 2026-08-28.** - ---- - -## The one sentence version - -`apply_delta` records three outcomes for every change the model sends — -`applied`, `clamped`, `rejected` — and everything downstream read only -`applied`. A refused change therefore reached the player as an ordinary chip and -reached the model, on the next turn, as a change that had succeeded. - -## What was actually wrong - -`plan/15` recorded two bugs and named a cause for each. Both causes were wrong, -and the investigation is worth keeping because the same reasoning trap is easy -to repeat: **the visible evidence was "the number did not move", and the natural -reading of that is that the model never tried.** - -### `pokemon_left` was emitted every time - -Adventure 43's action rows carry `milo pokemon_left` in `world_changes` at both -faints, each with `"delta": 0, "value": 3`. The model saw the faint and wrote -the path. It was not forgetting anything. - -`old == new == 3` is reachable only from a **positive** value. `pokemon_left` -was `min 0, max 3, initial 3, max_delta_per_turn 1`, so `+2` capped to `+1`, -reached 4, and clamped back to the ceiling of 3. Net zero. - -So the model sent the remaining count as an absolute — "two left" — instead of -a delta of `-1`. Milo's four stats alternate between the two conventions: - -| stat | convention | -|---|---| -| `active_pokemon` | text, absolute | -| `active_hp` | number, delta | -| `active_status` | text, absolute | -| `pokemon_left` | number, delta | - -HP survives because damage is naturally phrased as a change. A count is -naturally phrased as a state, so it got text semantics. - -**The general rule this produces:** a numeric stat whose `initial` equals the -boundary it moves away from turns every wrong-signed change into a silent -no-op. Every `hp` in the scenario has that shape (`initial == max`). It has -never fired only because damage is phrased as a decrease by luck of language. - -### Milestones were never emitted at all - -Zero milestone changes across nine AI turns. `EMIT_RULE` asks for -`"milestones.": true` and `apply_delta` matches `` against the schema -key, but `render_state_section` printed only the description, and -`render_reference` skipped the milestones section entirely -(`STAT_SECTIONS = ("world", "player")`). The string `graveler_defeated` was -nowhere in the prompt. - -The same playtest is its own control: `sandstorm_active` is a flag, flags -*are* printed by name, and it worked. - -### The replay was teaching the model to repeat itself - -Found while deciding whether to feed refusals forward, and the most damaging of -the three. `_history_text` re-attached each past turn's state block from -`world_delta["delta"]` — **what the model sent**, not what was applied. So the -turn after the faint contained the model's own block claiming -`"npc.milo.pokemon_left": 2`, directly above a live values line reading -`pokemon_left 3/3`, with nothing to say which was true. - -That is a per-turn lesson that sending `2` is correct. The identical mistake at -the second faint is what that lesson predicts. - -## What changed - -Five changes, on `fix-silent-clamps-and-milestone-ids`. - -1. **`Action.world_changes` reports refusals** (`models.py`). Reads `clamped` - and `rejected` beside `applied`. Accepted stats carry `clamped`; refusals - become `kind: "rejected"` entries. The `fix` key is present only when the - engine wrote one — it is empty for every accepted change, and this property - runs for every action of every list response. -2. **The UI distinguishes three outcomes** (`Play.jsx`, `index.css`). Clamped to - a standstill reads `no change — at its limit` on a dashed chip; a partial - clamp is marked `(limited)`; a rejection carries its reason. Dashed and - dimmed rather than red: the rules refusing a change is them working. -3. **Milestones are named to the model** (`engine.py`). The goals line is now - `Goals (mark with milestones.): graveler_defeated — Knock out Milo's lead - Graveler; …`, the same treatment NPCs get with `(npc.milo)`. -4. **Refusals carry a generated correction** (`engine.py`). Each rejection, and - each clamp that moved nothing, builds a `fix` string from the stat definition - at the point of refusal, so it quotes the real limits and lists the real - names. `render_refusals()` renders them into the prompt directly above - `EMIT_REMINDER`, for the previous AI turn only. -5. **History replays what was accepted** (`builder.py`). `applied_delta()` - rebuilds the block from `report["applied"]`, dropping any numeric entry where - `new == old` so a change that moved nothing cannot be copied as a zero. - -Plus the demo scenario: `pokemon_left` became `pokemon_fainted` -(`type: counter`, `initial: 0`), which puts a wrong sign on the counter rule -where it is rejected out loud instead of absorbed. The faint instruction moved -into its own paragraph, and a `world.turn` line was added — it sat at 0 for the -whole playtest because nothing ever told the model to move it. - -### The design call worth not re-litigating - -**A clamp that reduced a change but still moved the value says nothing.** Only -total losses are reported. If you tell a model its 80 damage became 30, it can -treat the shortfall as a debt and send the remaining 50 next turn — which is the -swing `max_delta_per_turn` exists to prevent. A rejection has no partial credit -to chase. `test_a_clamp_that_still_moved_the_value_says_nothing` pins this. - -## How to test it - -532 backend tests pass and the frontend builds. **The UI work has no automated -cover** — this project has no frontend test runner, which is the standing -reason its UI bugs are found by hand. - -Run the backend from `backend/` with -`.venv/Scripts/python.exe -m pytest tests/`. The new file is -`tests/test_change_visibility.py` (23 tests). Each of the three mechanisms fails -its own test when disabled; that was checked by sabotage, not assumed. - -To drive it, re-seed the scenario and play the demo: - -1. **A refusal chip.** Open the World State drawer, use its ✎ edit mode to put - Milo's `active_hp` at full, then play a turn where he takes no damage but the - model tries to heal him. Easier and more reliable: send a deliberately wrong - block by editing an AI turn. What you are looking for is a dashed chip - reading `no change — at its limit`, not a `+0`. -2. **The milestone.** Knock out Graveler. `graveler_defeated` should tick, and - a `✓ graveler defeated` chip should appear. This is the single clearest - pass/fail in the whole change — it never once happened before. -3. **The faint counter.** At the same faint, `pokemon_fainted` should go 0 → 1. - If the model sends an absolute again, it is now refused rather than absorbed, - and the refusal note should appear in the *next* turn's prompt. Read it under - Insights → the turn's context snapshot, section `world_state_refusals`. -4. **The replay.** In the same snapshot, check the replayed history: a past - turn's `state` block should carry only the changes that were accepted. -5. **`world.turn`** should now advance by 1 per reply. - -Check the narrow layout too. The chips grew longer text, and `.chg` is inside -the story column with nothing to scroll sideways — `overflow-wrap: anywhere` is -doing the work, and it was not re-checked at 390 px. Chrome clamps its minimum -window width to ~500 px, so relaunch with `--window-size=` rather than trying to -resize a maximized window. - -## Still open - -- **The missing `state` block from `plan/15` bug 1 is unexplained.** Truncation - at `max_output_tokens` removes the block, which `LENGTH_HEADROOM` exists to - prevent, and it was never ruled out. The distinguishing evidence is whether - the narration ends mid-sentence with `finish_reason: length`. -- **Every `hp` stat still has the `initial == max` shape.** Now visible when it - bites, rather than silent, but not designed out. -- ~~**The stale "Road to the Champion" scenario and adventure 42** are still on - production. Deleting them is hand-work and was deliberately not automated.~~ - Automated on 2026-08-28: `seed.py` now deletes seeded scenarios no seed file - claims. Adventure 42 survives with a NULL `scenario_id`, and losing the cover - art is the whole cost. -- **The Bandit Camp demo (`04-rpg-world-state.json`) was not checked** for the - same milestone problem. Its milestones were equally unnamed to the model - before this change, so it is worth asking whether one has ever fired there. - ---- - -## Driven in a browser, 2026-08-28 - -Adventure 45, four turns, on production against the demo model. Two of the five -changes worked. Three did not reach anyone, for one reason. - -### The bug: the stored column dropped two of the three lists - -`world_delta_of` in `routers/adventures.py` wrote `delta` and `applied` only. -Every consumer that distinguishes outcomes reads the other two: - -- `Action.world_changes` builds `clamped_paths` from `world_delta["clamped"]`, - so every chip carried `clamped: false`. `Play.jsx`'s `blocked = c.clamped && - d === 0` could never be true, and `(limited)` could never render. -- With no `rejected` list, a `kind: "rejected"` chip was unreachable. -- `worldstate.refusals` reads both lists, so `render_refusals` always returned - an empty string and no correction ever reached the next prompt. - -The `fix` string survived only because the engine stores it inside the `applied` -entry. Turn 3 is the record: the snapshot's `report.clamped` held -`npc.ivysaur.hp` and `npc.milo.active_hp` with correct `fix` text, both chips -came back `clamped: false`, turn 4's prompt contained no correction, and the -model repeated the same mistake. - -`world_delta_of` now carries all three lists. - -**Why the 21 tests passed.** The `action()` helper in -`test_change_visibility.py` built the column by hand as `{"delta": delta, -**report}`, with every list present. The write path was never exercised. The -helper now fills the column through `world_delta_of`. Removing the two lines -again fails 9 of the 23 tests; that was checked, not assumed. - -### The two that were not code faults - -`graveler_defeated` never fired and `world.turn` never moved. Both instructions -are present and correct in the assembled prompt: the goals line reads `Goals -(mark with milestones.): graveler_defeated — …`, and the scenario says to -add 1 to `world.turn` every reply. The demo model ignores both. Change 3 -landed; the model is the limit. - -### The absolutes were coming from the scenario's own wording - -The model sent a total, not a change, for almost every number: -`npc.ivysaur.hp: 96`, `npc.milo.active_hp: 65` then `88`, `player.potions: 2`. -Because every `hp` has `initial == max`, each one clamped back to where it -started. `potions` went **up** when the player spent one. - -`EMIT_RULE` does say "CHANGES ONLY, as deltas (not new totals)", and -`EMIT_REMINDER` repeats "deltas only". The scenario contradicted both at closer -range. `milo.active_hp.desc` said "**Reset this to** the newcomer's full HP", -which is an instruction to send an absolute, and that desc is injected every -turn. The bullets said "**Drop** the HP … and **raise** it", naming a direction -but never a sign. The only line that said "(not a delta)" was -`player.active_pokemon`, so naming the exception made the rule look optional. - -The one stat whose desc used delta wording, `pokemon_fainted` ("Add 1 each -time"), is the one that worked. - -Fixed in `05-league-championship.json`: every `hp` desc and the potion desc now -state the sign, the bullets do too, and the lead-in says plainly that every -number is a change with a worked example. `milo.active_hp.max_delta_per_turn` -went 65 → 98, because a switch legitimately moves that stat a full bar and the -old cap made the reset unreachable in one turn. - -**Not fixed:** the `initial == max` shape itself. It is now loud rather than -silent, and the wording removes the usual cause, but the shape is still there. - ---- - -## Driven against Claude, 2026-08-28 - -The four turns above ran on the free demo model, which ignored two instructions -that were present and correct in the prompt. To separate model behavior from -code, the same demo was played again through `backend/tools/claude_shim.py`, an -OpenAI-compatible endpoint backed by the local `claude` command line tool. The -model was `sonnet`. See the README for how to run it. - -Every one of the five changes worked: - -| Turn | Chips | -|---|---| -| 1 | `turn +1`, `active pokemon → Wartortle`, `wartortle hp -8`, `milo active hp -46`, dashed `type advantage used refused — no such flag` | -| 2 | `turn +1`, `milo active hp -52`, `milo pokemon fainted +1`, `milo active pokemon → Onix`, `✓ graveler defeated` | -| 3 | `turn +1 (limited)`, dashed `milo active hp no change — at its limit` | -| 4 | `turn +1`, `milo active hp +32`, `✓ type advantage used` | - -`graveler_defeated` fired for the first time in five playtests, `world.turn` -moved every turn, `pokemon_fainted` went 0 to 1 at the faint, and every HP number -was a signed change rather than a total. The two failures left open above were -the demo model, not the instructions. - -**The refusal loop is verified end to end.** Turn 3 clamped to nothing. Turn 4's -assembled prompt, read from `GET /api/adventures/1/actions/9/context`, carried -the correction verbatim: - -``` -[Part of your last state block was not applied. Correct it in this turn's block: -- `npc.milo.active_hp` did not move. It is already at its minimum of 0 (it runs - from 0 to 98; it moves at most 98 per turn).] -``` - -The model's next delta was `{"world.turn": 1, "npc.milo.active_hp": 32, -"milestones.type_advantage_used": true}`. That is the loop the whole change -exists for, and it had never been observed running. - -### The one bug that is still a schema fault - -At the faint the model set `npc.milo.active_pokemon` to `Onix` and sent no -positive `active_hp` change, so Onix arrived on the field at 0 of 98 and every -later hit was refused. Sonnet followed the rest of the scenario closely, so this -is the schema rather than the model: `active_hp` is one stat with one `max` of -98, shared by Graveler at 98, Onix at 90, and Kabutops at 88. A switch has to -raise it, and nothing in the schema can raise it on the referee's side. - -The fix is to give Milo's three Pokemon their own NPC entries, which also removes -the `initial == max` shape from this demo. Not done: the session's remaining time -went to the guest starter instead. - -### Cost - -About $0.04 a turn, billed against the Claude subscription rather than a card. -Roughly 13k of each request's 20k prompt tokens is the command line tool's own -overhead; the app's prompt is about 7k. - ---- - -## What shipped alongside it, 2026-08-28 - -**A permanent local test rig.** `backend/tools/claude_shim.py` is now in the -repo. It finds the `claude` binary through `AIDND_CLAUDE_BIN`, then `PATH`, then -the per-user install, and takes `--port` and `--claude`. - -**The demo says Pokemon in its title, and has a Pokeball for cover art.** -`05-league-championship.json` is now `[Demo] Pokemon League Championship: Round -One`. Renaming a seed exposed a fault: `seed.py` matches a seed to its row by -title, so a rename inserted a second public scenario and stranded the first, -which is the same failure this file records for "Road to the Champion". Seeds -now carry `previous_titles`, and `_find_renamed` lands the rename on the -existing row. The scenario kept its id, and no orphan appeared. - -The cover art is a 3.2 kB PNG data URI. An SVG one was tried first and stored as -an empty `image_url`: `app/images.py` accepts raster formats only, on purpose, -because SVG can carry script and these bytes are served from the app's own -origin. `backend/tools/make_pokeball.py` draws the ball with `zlib` alone, so -regenerating it needs no image library. - -**Seeded scenarios no seed file claims are deleted.** `previous_titles` stops a -rename stranding a row, but the rows already stranded still needed deleting by -hand on every deployment. `_sweep_unclaimed` removes them on the next boot. Only -a NULL owner with `is_public` is reachable, so nothing a player made can be -touched, and `adventures.scenario_id` is `ON DELETE SET NULL`, so an adventure -started from a deleted demo keeps its story and loses only the artwork. The -sweep is skipped when a seed file fails to parse, and an empty seed directory -never reaches it: neither reads as an instruction to delete live content. - -**Every new guest gets the played adventure.** `app/starter.py` copies a shipped -export bundle into each new guest account, from the guest mint in -`routers/auth.py`. The bundle is this session's adventure trimmed to its first -two exchanges, which ends on the knockout and shows an applied change, a refused -one, and a milestone. It stops before the Onix bug above, and the state it -leaves has Onix at its own 90 HP so a guest can play on from it. - -An adventure has no cover art of its own and inherits its scenario's, and a -bundle carries no scenario id, because an id is local to one database. The -starter file names its source under `scenarioTitle` instead, and -`starter._link_scenario` looks it up, so the copy shows the Pokeball rather than -a monogram tile. - -The row building that `POST /adventures/import` did inline moved into -`bundle.materialize`, which both callers now use. The limit and rate checks -stayed in the endpoint: the starter writes a file the server ships, so it has no -untrusted list to cap. `tests/test_starter_adventure.py` covers the file, the -copy, the chips, the playable end state, and the two failure paths. diff --git a/plan/17-refactor.md b/plan/17-refactor.md deleted file mode 100644 index 564edab..0000000 --- a/plan/17-refactor.md +++ /dev/null @@ -1,582 +0,0 @@ -# Phase 17: refactor for readability - -The code works and it is tested. It is also hard to read, because four files hold -most of it, one schema migration is half finished, and the published guide is a -hand-maintained copy of another file. This phase fixes those three things without -changing what the app does. - -## How to use this file - -This file is both the plan and the running status. Update the progress table at -the end of every stage. Read the table first when you pick the work back up. - -Each stage lands as its own commit on `refactor-17`. No stage starts until the -stage before it is green. - -## Progress - -| Stage | Work | Status | Landed | -|---|---|---|---| -| 0 | Hygiene: worktrees, branches, undocumented settings | done, except the branch deletion | 2026-08-29 | -| 1 | Split the four largest files | done: one test setup, and all four files split | 2026-08-29 | -| 2 | Remove duplication | done: all seven items | 2026-08-29 | -| 3 | SP8: drop the legacy columns | code done, all eight columns gone; the deploy and `VACUUM FULL` remain | 2026-08-29 | -| 4 | Documentation | not started | | -| 5 | A frontend test runner | not started | | - -## Baseline - -Record these numbers before you start. They are how you tell a refactor from a -rewrite. - -- 549 backend tests pass in 103 seconds. -- `backend/app` holds 13,486 lines across 45 files. -- `backend/tests` holds 12,221 lines across 38 files. -- `frontend/src` holds 9,171 lines across 21 files. -- The app exposes 70 endpoints and applies 64 migrations on boot. - -Run the suite with `cd backend && .venv/Scripts/python.exe -m pytest tests/ -q`. - -## What the review found - -### The code is clean at the statement level - -A scan for unreferenced functions across `backend/app` returned only FastAPI route -handlers, which the decorator references rather than the name. A scan of -`index.css` found 5 unused class names out of 373, and every one of the 5 is either -CodeMirror's or built from a template string. There is no dead code to delete and -no `TODO` or `FIXME` anywhere in the tree. - -Read that as a constraint. The gains in this phase come from moving code, not from -finding rot. - -### Four files hold most of the complexity - -| File | Lines | What it holds | -|---|---|---| -| `backend/app/routers/adventures.py` | 2353 | Nine concerns: the turn engine, branches, takes, import and export, adventure scripts, refresh from scenario, insights, memory CRUD, and action CRUD | -| `frontend/src/pages/Play.jsx` | 2280 | 27 components, plus a 722-line `Play()` holding 18 `useState` calls and 7 `useEffect` calls | -| `frontend/src/index.css` | 2865 | One stylesheet. The `max-width: 720px` block at line 2655 overrides rules written 2000 lines above it | -| `backend/app/worldstate/engine.py` | 918 | Four jobs: parse a delta, apply a delta, render the context sections, and instantiate a schema | - -### The schema is half migrated - -`plan/14-phase-story-tree.md` defines SP8, which drops the columns the tree -replaced. SP8 was gated on the tree running in production. It now does. Eight -columns are still written on every turn and read by nothing: - -- `actions.index`, `variants`, `variant_index`, `variant_count` -- `actions.state_before`, `world_state_before` -- `adventures.memory_cursor`, `summary_cursor` - -Until they go, `models.py` carries two vocabularies for one idea, and every reader -has to be told which one is live. - -### The published guide is a hand-written copy - -`docs/guide.html` is 84 KB of hand-written HTML covering the same material as the -63 KB `docs/GUIDE.md`. Nothing generates one from the other. They have already -drifted: the HTML last changed on 2026-08-20 and the Markdown on 2026-08-28, and -section 3.6, "Counting visits", exists only in the Markdown. GitHub Pages publishes -the HTML, so readers get the stale copy. `docs/architecture.html` is 87 KB of -hand-written HTML with no Markdown source at all. - -### Duplication the tests already protect - -- `worldstate.apply_delta` and `apply_override` each re-implement the same path - routing for `flags.`, `milestones.`, and `npc..`. That is six parallel - branches at `engine.py:573`, `587`, `617`, `675`, `697`, and `740`. -- 35 of the 38 test files repeat the same 8-line temporary-database prologue. There - is no `conftest.py`. -- `ScriptedProvider` or `FakeProvider` is defined 12 times, once per test file that - needs a fake model. -- The debounced autosave helper is copied into `Play.jsx:204`, - `ScenarioEditor.jsx:23`, and `ScriptEditor.jsx:22`. -- `get_adventure_or_404` is called by hand in about 20 handlers. It is a function, - not a dependency, so every handler repeats the call. -- `routers/chat.py:22` imports `SSE_HEADERS` and `sse` from `routers/adventures.py`. - One router reaches into another for shared plumbing. - -### One setting is documented nowhere - -`AIDND_TRUSTED_PROXY_HOPS` is read at `limits.py:55`. It does not appear in -`backend/.env.example`, `README.md`, or `render.yaml`. It sets how many proxy hops -`client_ip()` trusts, which is what stops a rotated `X-Forwarded-For` header from -buying a fresh rate-limit bucket. If a deployment adds a proxy hop and nobody sets -this variable, the bypass comes back silently. - -The unprefixed `DATABASE_URL` that `database.py:24` also accepts turned out to be -documented already, inside the `AIDND_DATABASE_URL` entry in -`backend/.env.example`. No change needed there. - -### `Settings.stream` is dead - -`models.py:622` defines the column, and `schemas.py:452` and `499` expose it. -Nothing reads it in the backend or the frontend. This is item S1 in -`docs/self-review.md`. - -### Local clutter - -Everything here is gitignored, so it costs repository weight nothing. It costs disk -and attention. - -- Two abandoned worktrees, each a full copy of the tree: - `.claude/worktrees/simplify-comments` and `.claude/worktrees/sp7-tree-ui`. Both - branches landed on `main` as squashes. -- 21 local branches. 13 read as unmerged to `git branch --no-merged`, and - `plan/STATUS.md` already records that they landed as squashes. -- Three stale SQLite files in `backend/`: `data.backup-2026-07-06.db`, - `data.backup-pre-phase8.db`, and `scroll_fixture.db`, 2.9 MB together. The live - local database, `backend/data.db`, is not one of them and is not touched. - -## Stage 0: hygiene - -No source changes. Do this first, so later stages run against a quiet tree. - -1. Remove both worktrees with `git worktree remove`, then delete the branches that - landed as squashes. Verify each one first: a branch is safe to delete when - `git log main..` names only commits whose message already appears in - `git log main`. -2. Delete the three stale `.db` files. Leave `backend/data.db` alone. -3. Add `AIDND_TRUSTED_PROXY_HOPS` to `backend/.env.example`, with the reason it - exists. Add it to the deploy section of `README.md` too, because that is where - somebody putting the app behind another proxy looks. - -**Check:** the suite still passes, and `git status` is clean. - -### What Stage 0 actually did, 2026-08-29 - -Both worktrees are gone, which freed about 104 MB. Removing `sp7-tree-ui` needed one -extra step: a Vite dev server had been running out of that worktree since -2026-08-18, holding `frontend/.vite` open and owning port 5173. It was serving a tree -54 commits behind `main`. Stopping it freed the directory. Restart the real one with -`start.ps1`. - -The three stale `.db` files are deleted. `backend/data.db` is untouched. - -`AIDND_TRUSTED_PROXY_HOPS` is now in `backend/.env.example` and in the README deploy -section. - -**Still owed.** The 19 squash-landed branches are still there. `git branch -D` is -blocked by the permission classifier, which is a reasonable guard on a destructive -command. All 19 are verified safe by the rule above. Run this to clear them: - -``` -git branch -D bugfix-code-review docs-story-tree fix-prepend-autoscroll \ - fix-silent-clamps-and-milestone-ids fix-store-refusals-and-delta-wording \ - handover-2026-08-17 measure-post-vacuum-sizes phase-14-story-tree \ - phase-7-public-repo phase-8-accounts sp1-tree-schema sp10-memory-bank-eviction \ - sp2-branch-clause sp3-node-cursors sp4-sibling-nodes sp5-fork-on-continue \ - sp6-bundle-v2 sp7-tree-ui sp7b-take-pager worktree-keep-fixture-and-status \ - worktree-simplify-comments -``` - -`worktree-keep-fixture-and-status` is the one that needed checking by hand. Its -commit subject appears nowhere in `main`, but the `--keep` flag it adds is on `main` -at `tools/stress_session.py:764`, along with both gotchas the message describes, at -`:686` and `:692`. Only the message differs. - -The remote branches are left alone. Deleting those is a separate decision. - -## Stage 1: split the four largest files - -Every change in this stage moves code. None of it changes behavior. The 549 tests -are the check, and they must pass without being edited, except where this section -says otherwise. - -### `routers/adventures.py` becomes a package - -Split it into `backend/app/routers/adventures/`: - -| Module | Holds | Source lines | -|---|---|---| -| `__init__.py` | The `APIRouter`, the shared dependencies, and re-exports | | -| `paging.py` | `ACTION_LIST_COLUMNS`, `ACTION_PAGE`, `action_window`, `annotate_takes`, `current_window` | 27-146, 1203-1261 | -| `crud.py` | List, create, get, patch, delete, plus the script-state and world-state readers | 225-580 | -| `turns.py` | The turn engine: `generate_turn`, `_generate_turn`, `run_player_turn`, the turn lock, and the SSE helpers | 581-1004 | -| `takes.py` | Retry, variants, takes, forking, and undo | 1005-1192, 1503-1778 | -| `branches.py` | The branch endpoints | 1193-1502 | -| `bundle_io.py` | Export and import | 1779-1837 | -| `scripts.py` | The per-adventure script endpoints | 1838-1941 | -| `refresh.py` | Refresh from scenario | 1942-2152 | -| `insights.py` | The context dry run and the per-action context | 2153-2189 | -| `memories.py` | Memory bank CRUD | 2190-2279 | - -Target no file above 450 lines. - -**The tests couple to the module, so read this before you start.** Twelve test -files call `monkeypatch.setattr(adventures, "OpenAICompatibleProvider", ...)`. -`monkeypatch` replaces a name in the module where the calling code looks it up, so -re-exporting from `__init__.py` does not keep those patches working. Once -`_generate_turn` lives in `turns.py`, the target becomes -`adventures.turns.OpenAICompatibleProvider`. - -Do Stage 2's `conftest.py` work first if you want that to be a one-line change -instead of twelve. Otherwise retarget all twelve here. The other patched names are -`adventures.limits`, `adventures.check_demo_cap`, `adventures.generate_turn`, and -`adventures._active_turns`, each used once. - -Keep these importable from the package root, because tests import them by name: -`ACTION_PAGE`, `SNIPPET_MAX`, `_snippet`, `world_delta_of`, `acquire_turn_lock`, -`retry_action`, `undo_turn`, and `_active_turns`. - -`_active_turns` is module-level mutable state guarded by a lock. It must live in -exactly one module, `turns.py`, and every other module must import the module and -reach through it. If two modules import the set by value, the lock guards two -different sets and the turn lock stops working. - -### What the router split actually did, 2026-08-29 - -`backend/app/routers/adventures.py` is now a package of 14 modules. The largest -is `turns.py` at 443 lines. Four modules exist that the table above does not -list, because the plan's eleven still mixed unrelated work: - -| Extra module | Why it exists | -|---|---| -| `deps.py` | Holds the `APIRouter` and the ownership check. It imports nothing else in the package, so every endpoint module can import the router without importing its siblings. | -| `scenario_text.py` | Copying a scenario's text and cards has two callers, `crud.create_adventure` and `refresh`. Leaving it in either one made the other import an endpoint module. | -| `nodes.py` | Story-tree navigation that four modules use: `last_action`, `next_index`, `next_depth`, `stand_on`, `db_tip`, `delete_turn`. | -| `actions.py` | The three action endpoints. They page and delete rather than play a turn, so they do not belong in `crud.py`. | - -Two decisions differ from the plan above. - -**The package root does not re-export `acquire_turn_lock` or `_active_turns`.** -The plan said to keep them importable, but that makes a broken patch look like a -working one. Rebinding `adventures.generate_turn` changes the alias and leaves -every caller reading the original, and the test still passes. Leaving those names -off the package root raises `AttributeError` instead. Eighteen test call sites and -four in `backend/tools/` now say `adventures.turns.`. The package root still -re-exports the pure helpers, so `adventures.ACTION_PAGE`, `adventures.undo_turn`, -and `chat.py`'s `from .adventures import SSE_HEADERS, sse` are unchanged. - -**`world_delta_of` lives in `turns.py`.** It reads as a world-state helper and sat -beside the world-state endpoints, but `_generate_turn` is its only caller. - -`_active_turns` behaved as the plan warned. Every module reaches it as -`turns._active_turns`, and a check confirms the four modules see one set object -and one lock. - -One test coupled to the router for an unrelated module: it called -`adventures.worldstate.instantiate`. It imports `app.worldstate` directly now. - -The split moved text rather than retyping it. An AST comparison against the -pre-split file confirms all 86 definitions are identical, once the `turns.` -prefix is normalized away. The 549 tests pass, and the OpenAPI schema still lists -the same 35 operations. - -### `worldstate/engine.py` becomes a package - -Split `backend/app/worldstate/engine.py` into four modules under -`backend/app/worldstate/`: - -- `schema.py`: `has_schema`, `instantiate`, `reconcile`, `band_label`, `npc_name`, - `npc_triggers`, `_initials`. -- `parse.py`: `extract_delta`, `_tolerant_load`, `render_delta_block`, - `applied_delta`, `refusals`, `render_refusals`. -- `apply.py`: `apply_delta`, `apply_override`, and the shared path resolver Stage 2 - introduces. -- `render.py`: `render_state_section`, `render_reference`, `_stat_line`, - `_describe_stat`. - -`worldstate/__init__.py` re-exports every public name it exports today, so no call -site changes. The engine is imported as a module, not monkeypatched, so this split -carries none of the router's test coupling. - -### `pages/Play.jsx` becomes a directory - -Split it into `frontend/src/pages/Play/`: - -- `index.jsx`: the page component. -- `usePlaySession.js`: the 18 `useState` calls and 7 `useEffect` calls that drive - one adventure, behind one hook. -- `panels/`: `PlotPanel`, `MemoryPanel`, `ScriptsPanel`, `InsightsPanel`, - `BranchPanel`. -- `drawers/`: `StatusDrawer`, `WorldStateDrawer`, and the `StatRow`, `StatGroup`, - `StateTree`, and `StateValue` parts they use. -- `reports/`: `StateChangeChips`, `WorldStateReport`, `ScriptReport`, - `CacheReport`, `TokenBreakdown`. -- `TakePager.jsx`, `RefreshModal.jsx`. - -There is no frontend test runner until Stage 5, so this split is verified by -`npm run lint`, `npm run build`, and by driving the Play screen in a browser. Drive -it. `plan/STATUS.md` records that the last two Play bugs were both found by hand and -were unreachable from any test that existed. - -### `index.css` becomes a directory - -Split it into `frontend/src/styles/`, imported in order from `index.css`: -`tokens.css`, `base.css`, `nav.css`, `cards.css`, `play.css`, `drawers.css`, -`schema-editor.css`, `insights.css`, `modals.css`, and `theme.css`. - -Move each `@media` block next to the rules it overrides, rather than leaving one -`max-width: 720px` block at the end. Keep the source order identical when you move -rules, because CSS resolves ties by order and the file relies on that in at least -two known places, recorded in `plan/STATUS.md` and in the comments at -`index.css:1011` and `1082`. - -**Check:** 549 tests pass, `npm run lint` and `npm run build` are clean, and the -Play screen works in a browser at desktop and at 500 px wide. - -### What Stage 1 actually did to the frontend, 2026-08-29 - -`Play.jsx` is now `frontend/src/pages/Play/`, twelve files. The largest is -`index.jsx` at 781 lines. `index.css` is now an `@import` list over -`frontend/src/styles/`, eighteen files. - -Two decisions differ from the plan above. - -**`usePlaySession.js` does not exist yet.** The page component still owns all of -the session state. Moving eighteen `useState` calls and seven `useEffect` calls -is a rewrite, not a move, and no frontend test would catch a mistake in it -today. It waits for Stage 5. - -**The `@media` blocks stayed where they were.** `responsive.css` still holds one -`max-width: 720px` block, at the end of the import order. Moving a `@media` block -next to the rules it overrides moves it earlier in the cascade, which changes -which of two equal-specificity rules wins. Nothing in the test suite would catch -that. Do this after Stage 5. - -Each split is verified by a different proof, because neither one has a test: - -- CSS: the parts rebuild `index.css` byte for byte, and the built bundle is - identical before and after at 56686 bytes. -- JSX: every non-blank line of the original appears exactly once, in order, across - the twelve files. A name-resolution check confirms every identifier each file - references is defined or imported there, with no unused imports. - -The line split stranded a comment at six of the boundaries. A leading comment -sits above the section it describes, so each boundary cut one loose and left it -at the end of the file before it. All six moved to the section they describe. - -`npm run lint` and `npm run build` are clean, and 549 tests pass. Driving the -Play screen covered the story view, all five panels, both drawers including the -world-state edit form, the branch map, the refresh dialog, and the take pager, -which stepped onto a take that lives on another branch and switched to it. The -console reported no errors. The extension cannot resize the render viewport and -the app sends `X-Frame-Options: DENY`, so the narrow-width check ran by setting -the `max-width` media queries to `all` in the live stylesheet. All 71 narrow -rules found their elements: the nav collapses to one button, the panel tabs move -onto the title row, a panel fills the screen, the composer stacks, and both -drawers become edge tabs. - -## Stage 2: remove duplication - -Each item here is small and is covered by tests that already exist. - -1. **One path resolver in `worldstate`.** `apply_delta` and `apply_override` route - `flags.`, `milestones.`, `npc..`, `world.`, and - `player.` with parallel code. Extract a resolver that returns the - container, the stat definition, and the kind, then let the two functions differ - only in the write rule. Their rules genuinely differ, so do not merge the - functions themselves. `apply_override` ignores `cooldown`, - `max_delta_per_turn`, and the rule that a counter cannot decrease, and it lets a - milestone be un-set. `tests/test_worldstate.py` covers both. -2. **Add `backend/tests/conftest.py`.** Move the temporary-database prologue there, - so the other 35 files drop 8 lines each. The prologue has to run before - `from app.main import app`, because `main.py` calls `bootstrap(engine)` at import - time. A `conftest.py` runs before any test module, which satisfies that. -3. **One fake provider.** Move `ScriptedProvider` and `FakeProvider` to `conftest.py` - as one fixture, replacing 12 copies. -4. **Move the SSE helpers out of the router.** Put `sse`, `SSE_HEADERS`, and - `turn_error` in `backend/app/sse.py`, so `chat.py` stops importing from - `adventures`. -5. **Make `get_adventure_or_404` a dependency.** About 20 handlers repeat the call. - A `Depends` removes the line from each one and puts the ownership check in the - signature, where a reader looking for it expects it. -6. **One `useDebouncedSave` hook** in `frontend/src/hooks/`, replacing the three - copies. -7. **Delete `Settings.stream`.** Remove the column, the two schema fields, and add - the migration that drops it. This is item S1 in `docs/self-review.md`. Mark it - applied there. - -**Check:** 549 tests pass. Test count may drop if consolidating fixtures removes a -duplicate case. If it does, say which case and why in the commit message. - -### What Stage 2 actually did, 2026-08-29 - -All seven items landed. Items 2 and 3 went in early, with the test setup in -`32cd7c1`, because the conftest had to exist before the router split could move -any test. Items 1, 4, and 5 are `2c57b1c`. Items 6 and 7 are `e0bf2b6`. - -**Item 1, the path resolver.** `_resolve` returns a `_Target` naming the kind, -the section, the key, the stat definition, and the character, or a rejection. -The two callers now differ only in the write rule, which is what the item asked -for. Building the container is a method on `_Target` rather than part of -resolving, because a rejected path must not leave an empty section behind. - -A refactor here is hard to check by reading, so it was checked by running. A -differential harness fed 3960 generated payloads through the old and the new -implementation and compared the state and the report. Ignoring `fix`, there are -zero differences. 674 rejections gained a `fix` string and none lost one, all of -them in `apply_override`, which had been the terser of the two. That is an -improvement rather than a regression: `apply_delta` already worded those -strings, and the world-state editor renders them. - -**Item 5, the ownership dependency.** All 32 handlers converted, not the 20 the -review estimated. Two proofs, because tests alone would not catch a change in -the order of the checks or in the public HTTP surface: - -- An AST pass confirmed the `get_adventure_or_404` call was the first statement - in every one of the 32 handlers. If it were not, hoisting it into a dependency - would move work that used to run after something else. -- The generated OpenAPI document was diffed against one built from a `git clone` - at `HEAD`. The only difference is that `rename_branch` now lists `branch_id` - before `adventure_id`, which is parameter order in the spec and not a route - change. - -Seven tests in `test_state_revert.py` needed updating. They call handlers as -plain functions rather than over HTTP, so they have to pass `adventure=` now. -That file is the only one that does this. - -**Item 7 needed a migration guard.** Migration 65 drops `settings.stream`, and -it is the first migration that drops a column. `_column_already_there` already -existed for the `ADD COLUMN` case. Dropping needs the mirror, `_column_already_gone`, -because `create_all` builds the current schema, which is already missing the -column, and fixtures like `test_tree_migration.pre_tree` stamp an old version -against a database built that way and replay. Verified by running migration 65 -twice, once against a database that still had the column and once against one -that did not. - -**One duplicate was left alone.** `FakeProvider` in `test_chat.py` is not a copy -of `ScriptedProvider`. It records the key, model, and endpoint it was -constructed with, which is how the chat tests assert on what would have gone -over the wire. `fakes.ScriptedProvider` streams replies and records prompts. -Merging them would give one class two unrelated jobs. - -## Stage 3: SP8, drop the legacy columns - -This is the only stage that touches the production database. Follow -`plan/14-phase-story-tree.md`, which specifies it. - -1. Confirm nothing reads the eight columns. `ACTION_LIST_COLUMNS` in the adventures - package lists `index`, `variant_count`, and `variant_index` today, so that tuple - changes here. `bundle.py:37` and `context/history.py:424` both describe `index` - as unread; verify that rather than trusting the comment. -2. Add the migration that drops them. Remove the columns from `models.py`, and - remove the fields from `schemas.py` and from `ActionOut`. -3. Deploy, then run `VACUUM FULL actions;` on the direct Neon endpoint, not the - `-pooler` one. Dropping a column rewrites toasted values, and nothing reclaims - that space on its own. `plan/STATUS.md` records what happened the last time - nobody ran it: the database reached 144.2 MB against a 512 MB tier. -4. `VACUUM FULL` takes an `ACCESS EXCLUSIVE` lock, so the app blocks on `actions` - for the duration. It took 5.5 seconds at 144 MB. - -**Check:** 549 tests pass, `/api/health` answers `{"ok":true}` after the deploy, and -one existing adventure opens, takes a turn, retries it, and pages back through the -transcript. - -### What Stage 3 actually did, 2026-08-29 - -Steps 1 and 2 landed. Step 3, the deploy and the `VACUUM FULL`, is still open, -because it runs against production and belongs with the release, not with the -branch. - -**Step 1, the read audit.** Nothing outside the migrations reads the eight -columns. `TakePager.jsx` reads `action.take_count` and `action.take_index` only, -which is what makes dropping the three payload fields safe. The comments in -`bundle.py` and `context/history.py` were accurate. - -**Step 2, the drop.** Migrations 66 to 73 drop one column each. `index` is a -keyword in SQLite, so migration 71 quotes it. `models.py`, `schemas.py`, and -`ACTION_LIST_COLUMNS` lost the same eight, `Adventure.actions` now orders by -`id`, and `attempts.renumber`, `context/history.max_action_index`, and -`nodes.next_index` are deleted. - -Two problems came out of the migration passes rather than the DDL: - -- `_split_variants_into_siblings` wrote through `Base.metadata.tables["actions"]`, - the live ORM table, so it stopped compiling the moment migration 66 removed - five of its columns. It now writes through `_ACTIONS_AT_60`, a frozen `Table` - carrying its own `MetaData`. That declaration is a snapshot of a past schema - and must not be updated to track `models.py`. -- Five passes read columns that migrations 66 to 73 drop. A `create_all` - database replays every migration against the current schema, so each pass now - calls `_has_columns` and returns early when the columns are absent. This is - the rule `_column_already_there` applies to DDL, applied to the passes. - -`bootstrap` gained a `through` argument. A migration test that asserts on -something a later migration removes stops at the version it is about, rather -than reading a schema several versions newer. - -**One pre-existing bug found and not fixed.** `bundle._write_nodes` never sets -`Action.parent_id`, and `paging.annotate_takes` groups on `parent_id`, so an -imported adventure's take pager reads 1 of 1. The frontend has read only -`take_count` since SP9, so this predates Stage 3 and is not a regression from -it. `test_retry_variants.py` documents it. - -**Check:** 555 backend tests pass, up from 549. `test_tree_migration.py` gained -eight parametrized cases asserting each column is gone after a real schema-45 -database migrates all the way. A `create_all` database would pass those without -running the migration, which is why the fixture is a frozen pre-tree one. - -## Stage 4: documentation - -1. **Generate `docs/guide.html` from `docs/GUIDE.md`.** Write a small build script - that renders the Markdown into the existing hand-written HTML shell, keeping the - current styles and metadata. After that, one edit updates both. Note in - `README.md` that the HTML is generated and that you edit the Markdown. -2. **Decide what `docs/architecture.html` is.** It has no Markdown source. Either - give it one and generate it the same way, or state at the top of the file that - it is hand-written, so the next person does not look for a source that does not - exist. -3. **Split `plan/STATUS.md`.** It is 63 KB, and most of it is dated session logs. - Keep "Pick up here", "Things worth remembering", and "Running things" in - `STATUS.md`. Move the dated entries to `plan/history/`, newest file first. -4. **Add `docs/DEVELOPING.md`.** Record how to run the app, how to run the tests, - where each subsystem lives, and the invariants a newcomer breaks first: - - Anything that reads `adventure.actions` during generation takes - `exclude_action_id`. It leaks into four places, not one. - - `memory_cursor` and `summary_cursor` are positions into `story_actions()`, and - `Memory.source_start` and `source_end` are `Action.index` values. The two - spaces diverge as soon as anything is deleted. - - `/auth/me` must not raise. It is the SPA's bootstrap call, so anything it - touches that can raise takes the whole frontend down. - - The global field rule in the stylesheet keys off `input[type=...]`, so a bare - `` and any new input type get browser default styling until you add - them. - - A modal opened from `.side-panel` needs `createPortal`, because the panel's - filling transform animation makes it the containing block for - `position: fixed`. - -**Check:** the generated HTML matches the Markdown section for section, including -section 3.6. Every link in `README.md` and `docs/index.html` resolves. - -## Stage 5: a frontend test runner - -`plan/STATUS.md` names the missing runner as the reason this project keeps finding -UI bugs by hand. Two shipped bugs were unreachable from any test that existed. - -1. Add Vitest, React Testing Library, and jsdom. Add a `test` script to - `frontend/package.json`. -2. Add the step to the existing `frontend` job in `.github/workflows/ci.yml`, - between lint and build. -3. Write the first tests against the two bug classes that already recurred: - - The take pager renders when a retry's reply arrives over SSE. The stream builds - its own `ActionOut`, so it was the one payload that never carried the - annotation. - - Retaking a player turn does not produce `> You > You`. The editor is seeded - with stored text that is already formatted. -4. Add a test for `usePlaySession` from Stage 1, since that hook now holds the state - the page used to hold inline. - -**Check:** `npm test` passes locally and in CI. - -## Risks - -| Risk | Where | How you catch it | -|---|---|---| -| A monkeypatch silently stops patching, and a test passes while calling a real provider | Stage 1, `routers/adventures` split | After retargeting, break the fake on purpose and confirm the tests that use it fail | -| `_active_turns` ends up imported by value into two modules, so the turn lock guards two sets | Stage 1 | `tests/test_state_revert.py` reaches for `adventures._active_turns`. Keep that import path working, and confirm a second concurrent turn still returns 409 | -| A CSS rule changes meaning because the split reorders it | Stage 1, stylesheet split | Compare the built CSS before and after. Order within each section must not change | -| The column drop rewrites the table and nobody reclaims the space | Stage 3 | Run `VACUUM FULL actions;` on the direct endpoint, and read sizes from `sum(octet_length(col))` rather than `n_live_tup` | -| Splitting `Play.jsx` breaks something no test covers | Stage 1 | Drive the Play screen by hand at both widths. Stage 5 exists to shrink this risk for next time | - -## What this phase does not do - -- It does not replace the hand-rolled migration runner with Alembic. 64 migrations - run correctly, and `docs/GUIDE.md` section 2.6 already explains the choice. -- It does not move `bootstrap(engine)` out of import time in `main.py`. The current - behavior means a failed migration means no service, which is deliberate. Stage 2's - `conftest.py` removes the boilerplate that import-time bootstrapping forces on - tests, which is the part that actually hurts. -- It does not change any API shape, any database content, or any prompt. diff --git a/plan/18-appendix-memory-ab-run-2.md b/plan/18-appendix-memory-ab-run-2.md deleted file mode 100644 index 3d1b11f..0000000 --- a/plan/18-appendix-memory-ab-run-2.md +++ /dev/null @@ -1,145 +0,0 @@ -# Appendix: the memory-prompt A/B, run 2 — a fresh story - -An independent replication of `plan/18-appendix-memory-ab-run.md`, produced by -`backend/tools/memory_ab.py` through `tools/claude_shim.py` after -`MEMORY_MAX_WORDS` was added. The story is **newly generated**, not the one run 1 -used, so this tests the prompts against different prose rather than re-scoring -the same text. - -## What replicated, and what did not - -| | run 1 before | run 2 before | run 1 after | run 2 after | -|---|---|---|---|---| -| memory 1 | 34 w, "you" | 89 w, "the player" | 72 w, named | 54 w, named | -| memory 2 | 105 w, "the player" | 107 w, "the player" | 68 w, named | 55 w, named | - -**Naming replicated cleanly.** Four control memories across two runs, and not one -of them names the protagonist. Four treatment memories, and all four do. That is -the change working, and it is the finding to rely on. - -**Length replicated, and the ceiling holds.** The control ran 34, 89, 105, 107 -words — a four-fold spread with no stated budget. The treatment after -`MEMORY_MAX_WORDS` ran 54 and 55. - -**The person-drift did not recur, and the claim about it should be read -narrowly.** Run 1 produced two control memories in two different persons — "You -crept low" and "The player asked Gwen" — which is exactly the reported -complaint. Run 2's controls were both "the player", consistently. So drifting -between *second* and *third* person is a real thing a model does, observed once, -not something it does every time. What is consistent across both runs is that -the control never reaches for the character's name, because it has never been -told one. - ---- - -*Everything below is the raw run, unedited.* - -One story, generated through `build_context` against `sonnet` at `http://127.0.0.1:8787/v1`. Both prompts then summarize the same blocks, so the prompt is the only variable. The control is `MEMORY_SYSTEM_PROMPT` as of `9cdcb55`. - -| memory | arm | words | framing | -|---|---|---|---| -| 1 | before | 89 | "the player" | -| 1 | after | 54 | named | -| 2 | before | 107 | "the player" | -| 2 | after | 55 | named | - -## Memory 1 - -**Before:** The player and Gwen scouted a bandit camp at dawn, agreeing on a quiet approach with Gwen circling the right flank (targeting the spear-carrying guard) while the player advanced from the left; the strongbox sat hidden under an oiled tarp in the camp's center. The player searched an empty bedroll undetected, taking a belt knife, a handful of copper and silver coins, and a charcoal-marked parchment map of the camp, then began moving low toward the strongbox while the guard remained oblivious and Gwen stayed ready with her bow. - -**After:** Kaelen and Gwen infiltrated the bandit camp at dawn, splitting flanks with Gwen covering right from the tree line, bow ready, while Kaelen approached left. Kaelen looted a bedroll near an overturned cart, taking a belt knife, coins, and a charcoal map of the camp, then began moving low toward the tarp-covered strongbox undetected. - -## Memory 2 - -**Before:** The player crept through the bandit camp toward a tarp-covered iron strongbox, reflexively reached to pull Gwen down beside him despite her being forty feet away at the tree line, and the resulting half-second lapse nearly caused a second bandit (a wood-gatherer) to spot him at the woodpile; Gwen almost loosed an arrow but held on his signal, and the bandit left unaware. The player then waited for the spearman to turn toward the fire and moved undetected to crouch beside the strongbox, where Gwen signaled approval and pointed to the simple hasp-and-padlock lock before he asked her whether she still trusted his read on the situation. - -**After:** Kaelen infiltrated the bandit camp toward the tarp-covered strongbox while Gwen covered him from the tree line; he instinctively reached to pull her down beside him but she was forty feet away, nearly exposing him to an unnoticed second bandit gathering wood, which frayed Gwen's trust before he safely reached the strongbox's simple hasp-and-padlock lock. - -## The story both arms summarized - -**start:** Dawn breaks grey over the treeline as you and Gwen crouch at the edge of the bandit camp. Smoke curls from a dying fire; three bedrolls lie empty. Somewhere ahead, the stolen caravan strongbox waits. - -Gwen checks her bowstring and looks to you. "Quiet, or loud?" - -**do:** go quiet and signal Gwen to circle around the right flank - -**ai:** You lean close, voice barely a breath. "Quiet. Take the right flank, stay in the tree line — I'll come at them from the left when you're set. Don't loose until I move first, unless someone spots you." - -Gwen's mouth quirks, half approval, half habit. "Try not to trip over anything this time." She melts sideways into the brush without a sound, her passage barely stirring a fern, and angles wide around the camp's edge, bow already loose in her hand. - -You give her a slow count — enough time to get into position — watching the camp in the meantime. A lone bandit paces near the fire, spear resting on his shoulder, yawning like the night watch had been long and dull. Beyond him, tents sag in a loose ring, and past those, half-hidden under an oiled tarp, a squat iron-banded shape sits guarded by nothing more than habit and complacency. The strongbox. - -You catch a flicker of movement at the tree line opposite — Gwen, settling into a crouch behind a fallen log, bow half-drawn, patient as stone. She lifts two fingers, then points toward the spear-carrier: hers if it goes loud. - -You ease forward through the underbrush, testing each footfall before committing your weight, keeping the dying fire's smoke between you and the pacing guard's sightline. Your pulse is steady, senses sharpened — the cool mana still humming faintly under your skin from old habit, ready if you need it. No alarm yet. No eyes turned your way. The camp sleeps in its false security, and for now, the quiet is yours to keep or break. - -You reach the shadow of an overturned cart at the camp's rim, close enough now to hear the guard mutter something to himself. Gwen is in position, arrow nocked, waiting on your signal. - -**do:** search the nearest bedroll for anything useful - -**ai:** You glance toward the nearest bedroll — one of the three empty ones, rumpled blankets still holding the shape of whoever slept there, maybe ten feet from the cart's shadow. The guard's back is turned, spear-shoulder rotating slowly as he paces his short loop. You judge the distance, the timing, and decide it's worth the risk. - -You slip low across the open ground, boots finding bare dirt instead of dry leaves, and drop to a crouch beside the bedroll. Your fingers move quick and quiet through the folded blanket, a stiff leather pack shoved half-underneath. Coins clink faintly — you cup your palm over the pouch before it can rattle again — and your hand closes on a stubby belt knife, a handful of copper and silver, and a folded scrap of parchment that might be a rough map of the camp's layout, marks scratched in charcoal. - -The guard's pacing falters. He half-turns, spear dipping, some animal instinct prickling at the edge of his awareness. You freeze mid-motion, breath held, willing yourself into the cart's shadow. Across the clearing, you catch the barest shift from Gwen — her draw hand tightening, ready to loose if this goes wrong. - -The moment stretches. Then the guard grunts, spits into the dying fire, and resumes his slow circuit, oblivious. You let your breath go slow and silent, tucking the coin pouch and parchment into your jerkin, the knife into your belt beside your own. - -It's not much, but it's something — coin for later, and maybe a map worth reading twice before you commit to a route through this camp. You ease back from the bedroll, retreating to the cart's cover, eyes flicking to Gwen. She gives the faintest nod: *still clear, still yours to call.* - -The guard yawns again, spear drooping, utterly unaware how close he came to dying loud. The strongbox waits under its tarp, patient as ever. The quiet is still yours — for now. - -**do:** move toward the strongbox, keeping low - -**ai:** You keep to the cart's shadow a moment longer, judging the guard's rhythm — three steps out, a pause, three steps back, spear dipping low on the turn. When his back rotates fully away, you move. - -Low and fast, you cross the gap between the cart and the nearest tent, one hand brushing the canvas to steady yourself rather than grip it — no rustle, no tug on the guy-lines. You thread between two more tents, close enough now to smell tallow and old woodsmoke, and drop into a crouch behind a stack of crated supplies not ten feet from the tarp-covered strongbox. - -From here you can see it properly: iron bands dark with rust, a heavy lock through a hasp, the oiled tarp thrown over it more to keep off dew than to hide it. Careless. Whoever's running this camp trusts their numbers more than their diligence. - -You risk a glance back toward the tree line. Gwen hasn't moved, but her head has tracked your progress the whole way, bow still half-drawn, patient. She catches your eye and gives the barest tilt of her chin toward the strongbox — *go on, I've got you* — before her gaze slides back to the pacing guard, who is still oblivious, still yawning his way through a boring watch. - -Closer now, you can make out voices from one of the tents — low, unhurried, two men talking about nothing in particular. Not alarmed. Not aware. The camp's quiet holds, fragile as it is, and the strongbox sits within reach, close enough that you could touch the tarp if you stretched out a hand. - -Your fingers itch to check the lock, to see if it needs force or finesse — but that's a decision for the next breath, not this one. For now you crouch in the crate-shadow, heartbeat steady, senses stretched wide across the camp: the pacing guard, the murmuring tent, Gwen's stillness at your back. Nothing has broken yet. The strongbox is yours to reach for. - -**do:** grab her wrist and pull her down behind the woodpile - -**ai:** Your hand moves on instinct before thought catches up — reaching sideways for Gwen's wrist to haul her down out of sight — but your fingers close on nothing but cool air. She isn't beside you. She's forty feet off across the clearing, still nested behind her log at the tree line, exactly where you left her. - -The mistake costs you half a second, and half a second is enough. Boots crunch on the far side of the crates — close, closer than the pacing guard should be. A second bandit, one you hadn't accounted for, rounds the supply stack with a coil of rope over one shoulder, heading for the woodpile stacked just past your cover. - -You drop flat behind the crates instead, breath locked in your chest, willing your travel-stained leathers to melt into the shadow. The bandit hums tunelessly, kneels at the woodpile — near enough you could count the notches in his boot heel — and starts loading split logs into his arms, utterly unaware he's sharing six feet of dirt with an exiled ranger and a very recent, very foolish reflex. - -Across the camp, Gwen has gone rigid, bow lifting a fraction — she saw the second man appear, saw you freeze, and for one taut moment she's clearly bracing to put an arrow in his throat if he so much as glances your way. You catch her eye through the crate-gap and give the smallest shake of your head. *Not yet. Hold.* - -The wood-gatherer straightens, arms full, and ambles off toward the fire without ever knowing how close he came. Your heart hammers against your ribs. Gwen eases her draw, but her jaw stays tight, and even at this distance you can feel the flicker of irritation under her patience — not at the bandit, at you, for a half-second of carelessness that nearly bought you both a fight you hadn't chosen. - -The strongbox still waits behind its tarp, closer now than ever. But the quiet feels thinner than it did a moment ago. - -**do:** wait for the bandit to turn, then move - -**ai:** You settle back against the crates, still as the wood itself, and let your breathing fall into the same slow rhythm as the camp around you. The wood-gatherer's footsteps fade toward the fire pit, and a moment later you hear the clatter of logs dropped beside the flames, a muttered word to the pacing spearman, then silence again but for the crackle of embers catching new fuel. - -You count another ten heartbeats before you risk a look. The spearman has stopped to warm his hands, back fully to you now, attention on the fire instead of his rounds. No one else stirs between you and the tarp. - -This is the gap. - -You rise from the crate-shadow in one smooth motion, low and quick, closing the last stretch of open ground between the crates and the strongbox in a handful of silent strides. The tarp is close enough to touch now — you drop to a crouch beside it, back pressed to the iron-banded corner, and finally let yourself breathe. - -Across the clearing, Gwen's stance has eased a fraction. She tracked every step of that crossing, and though her jaw is still set from the wood-gatherer's near miss, there's the faintest approval in how she holds herself now — the tension of someone watching a mistake corrected rather than repeated. She lifts two fingers again, then points toward the lock hasp: *your move.* - -The lock itself is heavy, old iron, a simple hasp-and-padlock rather than anything clever — the kind of thing built to deter opportunists, not a trained hand. You could pick it quiet, or put a boot to it loud. The voices still murmur from the nearby tent, unconcerned. The spearman still warms his hands at the fire. The quiet holds, thinner than before, but yours. - -**do:** ask her whether she still trusts my read on this - -**ai:** You crouch there a moment longer, one hand still braced on the strongbox's cold iron corner, and risk a glance across the clearing. Gwen's eyes are already on you — they usually are — and you mouth more than whisper the question, tilting your head just enough to ask it: *still with me on this?* - -For a beat she doesn't answer, just watches the spearman warming his hands, the murmuring tent, the whole fragile balance of the camp's false calm. Then she gives a small, economical shrug, the kind that means *mostly*, and taps two fingers against her bow stave — a gesture you've learned means *your instincts, not your luck.* It isn't a full yes. The wood-gatherer's near miss still sits behind her eyes, and you can read it plainly enough: she'll follow your read, but she's watching it closer than she was an hour ago. - -Her hand shifts, index finger drawing a small circle in the air, then pointing at the lock — *pick it, don't break it. Loud is still your last resort, not your first.* It's about as much reassurance as you're going to get from a ranger who's had one scare too many this morning, and honestly, it's fair. You nod once, slow, so she can see you take it. - -The spearman shifts his weight at the fire but doesn't turn. The tent's murmur continues, unbroken. Whatever trust frayed at the woodpile hasn't snapped — it's just drawn tighter, watchful, waiting to see what you do with the lock in front of you. The strongbox sits solid and patient at your back, and beyond the thin canvas walls, the camp sleeps on in its dangerous, borrowed quiet. - -You turn back to the hasp, letting your fingers find the mechanism, senses still split between the metal under your hands and the fire-lit shape of the spearman thirty feet off. The moment is yours to use well — or not. - diff --git a/plan/18-appendix-memory-ab-run.md b/plan/18-appendix-memory-ab-run.md deleted file mode 100644 index 23f97ba..0000000 --- a/plan/18-appendix-memory-ab-run.md +++ /dev/null @@ -1,134 +0,0 @@ -# Appendix: the memory-prompt A/B, run 2026-08-31 - -The evidence behind `plan/18-persona-and-memory-quality.md`. Reproduce it with: - - python tools/claude_shim.py --port 8787 & - python tools/memory_ab.py --out ab.md - -One story, generated a turn at a time through the app's own `build_context`. -Both prompts then summarize the **same** blocks, so the story is held constant -and the prompt is the only variable. Every call is a separate request, so -neither arm sees the other's output, and the model is never told what is being -measured. The control is `MEMORY_SYSTEM_PROMPT` as of commit `9cdcb55`, read -from git rather than pasted, so it cannot drift from what shipped. - -The model was a Claude model, reached through `tools/claude_shim.py`. See -"What this does not show" in plan/18 before generalising from it. - -## What it showed - -| memory | arm | words | how it names the protagonist | -|---|---|---|---| -| 1 | before | 34 | second person — "**You** crept low…" | -| 1 | after | 72 | "**Kaelen** and Gwen crouched…" | -| 2 | before | 105 | "**The player** asked Gwen to…" | -| 2 | after | 68 | "**Kaelen** and Gwen infiltrated…" | - -Two things came out of this, and only one of them was the thing being tested. - -**The reported fault reproduced.** Two consecutive memories, from one story, -written minutes apart, in two different persons. That is the complaint, observed -rather than argued from the prompt text. Both after-memories name Kaelen. - -**A fault nobody had noticed.** "1-2 plain sentences" is not a length: 34 words -for one block, 105 for the next. `memory_top_k` injects five memories every -turn, so the bank's running cost was set by a number never stated. -`MEMORY_MAX_WORDS = 50` now states it. Re-run over this same story, the two -blocks came back at **32 and 58 words**, still named, still third person, still -carrying the camp map, the strongbox behind the second tent, and the strap -frayed near through: - -> Kaelen crept low toward the strongbox, now known from the map to be behind -> the second tent, with the dozing watchman still unaware and Gwen holding her -> covering position on the flank. - -**A likely accuracy win, at n=1.** The player's move was `grab her wrist and -pull her down behind the woodpile`. The before-memory recorded "The player asked -Gwen to grab her wrist and pull her down", inverting who acted. This is the -failure the brief predicts — with no cast there is nothing to say whose wrist -"her wrist" is — but one sample is an observation, not a result. - ---- - -*Everything below is the raw run, unedited.* - -## Memory 1 - -**Before:** You crept low through the mist toward the second tent, following the map's mark toward the strongbox while Gwen held her flanking position, the sleeping watchman still dozing on his spear near the lean-to. - -**After:** Kaelen and Gwen crouched at dawn outside the bandit camp; Kaelen signaled Gwen to circle the right flank while a drowsy watchman kept post near the lean-to, and Gwen took position unseen near the strongbox tent. Kaelen searched an abandoned bedroll, finding a whetstone, a few copper coins, dried meat, and a charcoal-scrawled camp map revealing the stolen strongbox was hidden behind the second tent, then began moving low toward that location. - -## Memory 2 - -**Before:** The player asked Gwen to grab her wrist and pull her down behind the woodpile just before a second bandit—half-dressed, carrying a piss-bucket—stumbled out, relieved himself, and returned to bed in the first tent; Gwen lingered a beat before freeing her wrist and murmured "Good eyes." After waiting five heartbeats to confirm the bandit was asleep, the player and Gwen returned to the strongbox tent, where they found the old iron caravan lock too sturdy to pick and instead began testing the straps, discovering one frayed near through at the fold; the player then asked Gwen whether she still trusted their read on the situation. - -**After:** Kaelen and Gwen infiltrated the bandit camp to reclaim a stolen strongbox, sheltering behind a woodpile while a second bandit relieved himself nearby before returning to sleep; afterward Kaelen returned to the strongbox tent, found its lock too sturdy to force by hand but noticed one strap frayed near through, and paused there with Gwen covering him to ask whether she still trusted his read on the situation. - -## Summary - -**Before:** Under cover of mist, the player and Gwen scouted a bandit camp, aiming to reach a strongbox marked on their map while a watchman dozed near a lean-to. Approaching the second tent, the player pulled Gwen down behind a woodpile just in time to avoid a half-dressed bandit who stepped out to relieve himself before returning to sleep in the first tent; Gwen lingered a moment before pulling free, murmuring "Good eyes." After waiting to confirm the camp was settled, the two returned to the strongbox tent, finding its old iron caravan lock too sturdy to pick. They shifted to testing the straps securing it instead, discovering one frayed nearly through at the fold—a possible way in. The player then turned to Gwen to ask whether she still trusted their read on the situation, leaving the moment of decision unresolved. - -**After:** At dawn, Kaelen and Gwen scouted a bandit camp holding a stolen strongbox. Kaelen sent Gwen to circle the right flank near the strongbox tent while a drowsy watchman kept post, then searched an abandoned bedroll himself, finding a whetstone, a few copper coins, dried meat, and a charcoal map marking the strongbox's location behind the second tent. Moving low toward it, the pair sheltered behind a woodpile while a second bandit briefly woke and relieved himself before returning to sleep. Reaching the strongbox tent, Kaelen found its lock too sturdy to force by hand, but noticed one securing strap frayed nearly through. With Gwen covering him, he paused to ask whether she still trusted his read on the situation before attempting to exploit the weak strap. - -## The story that was summarized - -**start:** Dawn breaks grey over the treeline as you and Gwen crouch at the edge of the bandit camp. Smoke curls from a dying fire; three bedrolls lie empty. Somewhere ahead, the stolen caravan strongbox waits. - -Gwen checks her bowstring and looks to you. "Quiet, or loud?" - -**do:** go quiet and signal Gwen to circle around the right flank - -**ai:** You melt back from the ridge line without a sound, boots finding moss instead of twigs, and catch Gwen's eye. Two fingers point right, then curl in a half-circle — *swing wide, come at the flank*. She reads it instantly, dips her chin, and peels off through the bracken, bow already half-raised, moving low and quiet along the tree line until the mist swallows her. - -You hold position at the camp's edge, breath slow, watching the dying fire spit its last embers. A bandit shifts on watch near the lean-to, spear butt planted in the dirt, more bored than alert. No one else stirs. Somewhere off to the right you catch the faintest whisper of disturbed leaves — Gwen, further along than you'd expect, patient as ever. She's given herself a clean angle on the strongbox tent from the flank, out of the watchman's sightline entirely. - -The plan is good. Quiet holds. All you need now is the first move — and the choice of who falls first, and how. - -The watchman yawns, oblivious, close enough that a single silent step would put you in reach of his throat before he ever draws breath to shout. - -**do:** search the nearest bedroll for anything useful - -**ai:** You crouch by the nearest bedroll — a heap of stitched furs still holding the sleeper's warmth, though its owner is long since up and yawning by the fire. You keep one eye on the watchman as your fingers work through the folds. A whetstone, a handful of copper coins that clink softly against your palm before you still them with a fist, a scrap of oilcloth wrapped around dried meat gone hard at the edges. Nothing worth the risk so far. - -Then your fingers catch on something stiffer, tucked beneath the bedroll's edge — a folded scrap of parchment, half-torn, charcoal-scrawled. A rough map of the camp, crude but legible: the lean-to marked with an X, a second mark deeper in camp labeled in shorthand you recognize as bandit cant for *strongbox*. It's tucked behind the second tent, not the first — further from the watchman than you'd guessed, closer to where Gwen is now waiting in the mist. - -You fold it away inside your jerkin, pulse ticking a little faster. Small find, real value — it just turned guesswork into a plan. The watchman still hasn't stirred, spear planted, chin dropping toward his chest. Gwen's silhouette is a smudge of shadow at the flank, patient, waiting on your signal. - - - -The camp holds still around you — watchman drowsing, fire dying, Gwen coiled and ready on the flank. The map sits warm against your ribs, the strongbox's true location now known. Your move. - -**do:** move toward the strongbox, keeping low - -**ai:** You drop low, weight rolling from heel to ball of each foot, threading between the empty bedroll and a stack of split firewood. The watchman's chin has sunk near to his chest, spear listing in his slack grip. You give him a wide berth anyway — no need to gamble breath on a man already halfway to sleep. - -The second tent is smaller than the first, its flap laced shut with a crude leather cord. Just as the parchment promised, its bulk sits wrong for a tent meant to sleep anyone — square-cornered, iron-banded, too heavy for the ground it rests on. You crouch beside it, easing the lacing open one loop at a time, and the flap parts on a dull gleam of banded oak and iron: the strongbox, lock intact, straps still buckled from the caravan raid. - -A shift of cloth behind you — not the watchman, too far behind you for that. You go still, hand dropping to your knife hilt, eyes cutting sideways through the tent's shadow toward the sound. Nothing resolves yet, just a settling, maybe wind through canvas, maybe not. Across the clearing, Gwen has gone motionless too, bow half-drawn, watching the same dark gap between tents that's caught your attention. - -The strongbox is here, within reach, real weight under your palm instead of a rumor on a scrap of parchment. But something in that gap between the tents hasn't declared itself yet, and the watchman is still only a few careless steps away from waking to find you crouched over his camp's whole reason for being. - -**do:** grab her wrist and pull her down behind the woodpile - -**ai:** Your hand shoots out and closes around Gwen's wrist before she's fully turned toward the gap between the tents. She goes rigid for half a heartbeat — a killer's instinct, ready to break the grip — then reads your face and lets herself be pulled. You drag her down behind the woodpile together, shoulders pressed to split logs, breath shallow. - -The shape that had been shifting in the shadow resolves itself: a second bandit, roused early, stumbling out from behind the tents with his breeches half-laced and a piss-bucket in hand, utterly uninterested in anything but the treeline. He doesn't look toward the strongbox tent. Doesn't look toward the woodpile. He shuffles past, empties the bucket against a stump, and turns back the way he came, already yawning. - -Gwen's breath eases out slow beside you. She doesn't pull her wrist away immediately — a beat longer than she needs to — before easing it free herself. "Good eyes," she breathes, barely sound at all. Her gaze flicks to your hand, then back to your face, something unreadable and warm passing behind it. Watching your back instead of charging in — that's the kind of thing she remembers. - -Through the gap in the logs you track the second bandit's path: he ducks into the first tent, not the watch line, and doesn't reemerge. Three accounted for now — the drowsing watchman, this one bedding back down, and whoever else the camp is hiding. The strongbox tent is still open behind you, lock waiting, straps waiting. Gwen settles back into her crouch, bow across her knees, eyes on you for the next signal. - -**do:** wait for the bandit to turn, then move - -**ai:** You go still as stone, eyes fixed on the tent flap the second bandit vanished into. Seconds crawl. Then the canvas stirs one last time — a grunt, a shifting weight settling onto a bedroll, the particular silence of a man who's decided the day can wait a while longer. You count five heartbeats to be sure, watching for any second shadow, any sign he's only feigning sleep. Nothing. Even the watchman by the fire hasn't stirred, spear still planted, chin still low. - -You ease up from the woodpile, moving on the balls of your feet, and slip back to the strongbox tent. Gwen catches the motion and rises with you, silent as a drawn breath, bow angled to cover the gap between tents while you work. The lock is old iron, caravan-make, not bandit work — it'll need proper tools or proper force, not fingers. - -You crouch and test the straps first, feeling for a weak buckle, a give point. One strap is frayed near through at the fold. With steady pressure it might part without the sharp crack of metal — quieter than forcing the lock, if slower. Gwen shifts her weight beside you, patient, trusting you to read the moment right, her eyes flicking once toward the dozing watchman and back. - -The camp holds its breath around you both. The strongbox waits under your hands, and the next move is yours to make. - -**do:** ask her whether she still trusts my read on this - -**ai:** - diff --git a/plan/18-persona-and-memory-quality.md b/plan/18-persona-and-memory-quality.md deleted file mode 100644 index 5ba1bed..0000000 --- a/plan/18-persona-and-memory-quality.md +++ /dev/null @@ -1,655 +0,0 @@ -# The protagonist has no name, and the summarizer is told nothing - -Two changes, in order. Phase 1 gives the adventure a persona. Phase 2 uses it, -along with the cast, to fix the memories. Phase 1 is worth shipping on its own; -Phase 2 depends on it and is much smaller once it lands. - -**Both phases are built and green (631 backend tests). Phase 1 was driven in a -browser (21/21 checks). Phase 2 was run end to end against a real model, as a -controlled A/B on one story — see "Run with a real model". A bank written under -the old prompt can be rewritten in place — see the last section.** - -**Last updated: 2026-08-31.** - ---- - -## The one sentence version - -Memories come back inconsistent and vague because the summarizer is handed six -actions of second-person prose and nothing else — no protagonist, no cast, no -setting, no instruction about what person to write in — so it cannot say who -"you" is or who "she" is, and neither can any memory it writes. - -## The evidence - -This is the entire prompt that writes a memory (`memorybank.py:391`): - -``` -system: You compress interactive-fiction story excerpts into memories. Respond - with 1-2 plain sentences in past tense stating the concrete facts and - events (names, places, items, promises, injuries). No preamble, no - commentary. - -user: Story excerpt: - - <6 actions, joined by blank lines, truncated to 2000 tokens> - - Memory: -``` - -Nothing else is passed. Not `adventure.memory` (the plot essentials), not the -NPC names and descriptions the scenario already defines, not the world state, -and not the protagonist — because until Phase 1 there is no protagonist to pass. - -Two failures follow from that, and they are the two complaints: - -**Inconsistent framing.** The prompt never says what person to write in. The -model picks one per call. Across a single bank you get "You entered the crypt", -"The player entered the crypt", and "He entered the crypt" describing the same -kind of event. - -**No idea who anyone is.** Given `You push the door open. She grabs your arm.` -the only honest memory is *"You entered a room and she stopped you."* Retrieved -forty turns later into a scene with three women in it, that memory is worse than -nothing. - -The summary inherits both problems, because `_update_story_summary` builds from -the memory bullets. - ---- - -# Phase 1 — the persona - -## What already exists, and what does not - -`stat_schema` already gives the player a stat block, and it is already -namespaced beside the NPCs: - -``` -world.day -player.hp <- the player's stats, already sectioned -npc.gwen.trust -flags.alarm_raised -milestones.escaped -``` - -NPCs carry `name`, `keys`, and `desc` (`schema.py`, `render_reference`). The -`player` section carries none of those. That asymmetry is the whole gap: the -player has stats but no identity, so the block renders as `You: hp 100/100` and -reads to the model as a floating global rather than a character. - -## The paths do not change - -`player.hp` stays `player.hp`. It does not become `kaelen.hp` or `user.hp`. - -The reason is `_history_text()` in `context/builder.py`. It replays every past -turn's stored delta back into the prompt, and those stored blobs contain literal -strings like `{"player.hp": -15}`. A path that carries the persona's name breaks -the moment a player renames their character: every replayed block in history -then names a path the schema no longer defines, `applied_delta` renders it -anyway, and the model copies the broken form. A rename would also require -migrating every stored `world_state`, every seed scenario, and the `EMIT_RULE` -example text, for no functional gain. - -What changes is the **label**, using the trick NPCs already use — print the -display name and the path together: - -``` -before: You: hp 100/100, mana 30/50. -after: Kaelen (player): hp 100/100, mana 30/50. -``` - -The name is visible to the reader, the path stays copyable by the model. - -## Where the persona lives - -Three plain columns on `adventures`, alongside `memory` and `authors_note`: - -```python -persona_name: Mapped[str] = mapped_column(String(80), default="") -persona_pronouns: Mapped[str] = mapped_column(String(40), default="") -persona_desc: Mapped[str] = mapped_column(Text, default="") -``` - -**Not in `stat_schema`.** Two reasons. It has to work for an adventure with no -RPG layer at all, which is most of them, and `_initials()` in -`worldstate/schema.py` treats every dict inside a section as a stat definition — -a `persona` key dropped into `stat_schema.player` would be instantiated as a -stat, rendered as a stat line in the guide, and handed an `initial` value. - -**Adventure-level, not scenario-level.** Two people playing the same scenario -are different characters. A scenario steers the protagonist through its plot -essentials, which it already can. Keeping personas off `scenarios` also avoids -having to decide what "Update from scenario" does to a persona the player has -edited: the answer is nothing, because the scenario never had one. - -**Not per-branch.** Branches are alternative futures within one adventure; the -protagonist is the same person down all of them. - -Empty `persona_name` means the feature is off and behavior is exactly what it is -today. That is the entire backward-compatibility story — no backfill. - -## Pronouns get their own field - -One short string: `he/him`, `she/her`, `they/them`. It exists because Phase 2 -will tell the summarizer to write in third person. Without a stated pronoun the -model infers one from the name, and once it infers wrong that error is baked -into every memory it writes from then on and into the summary built from them. -A field is cheaper than a wrong guess repeated forever. - -Blank is a valid value. When it is blank nothing is rendered, and the Phase 2 -prompt tells the summarizer to use the name rather than a pronoun. - -## Where it enters the prompt - -A new static section in `build_context`, between `ai_instructions` and -`plot_essentials`: - -``` -Player character: -You are Kaelen (he/him). A half-elf ranger, exiled from the northern holds. -``` - -**It goes in the system block on purpose.** The description is user-only and -never changes during a turn, so it sits inside the cached prefix and costs -nothing after the first turn. This is why "only the user can change it" is not -just a product choice — it is what keeps the section free. Anything the AI could -rewrite would have to move below the history with the other live sections, and -would re-price the prompt on every change. - -It is emitted whether or not the adventure has an RPG layer. It is not gated -behind `has_ws`. - -## Where it enters the world state - -`worldstate/render.py`, two edits: - -- `render_state_section` takes the display name and prints - `Kaelen (player): …` in place of the hardcoded `You: …`. Falls back to `You:` - when no persona is set. -- `render_reference` gains one line tying the name to the path, mirroring the - NPC header it already writes: - `- Protagonist Kaelen — stats are addressed as player..` - Without this the model reads "Kaelen" in the prose and invents `kaelen.hp`. - -The persona description is **not** repeated in the stat guide. It already has -its own section, and the guide is about paths. - -## The AI cannot edit it - -Nothing to build. `_resolve()` in `worldstate/apply.py` rejects any path it does -not recognize, so a delta of `{"persona.name": "Bob"}` already falls through to -"`persona.name` is not a tracked value", and that refusal already reaches the -model through `render_refusals`. Worth one test to hold the behavior in place. - -## Setting it, and changing it - -**At the start.** `PlaceholderModal` (`components.jsx`) already exists and -already opens before an adventure begins — but only when the scenario contains -`${...}` tokens. It becomes a "Begin adventure" modal that always opens, with -the three persona fields at the top and any placeholder fields below. - -**Placeholders stay independent of the persona.** A scenario using `${Name}` -will ask for a name twice. That is accepted: no scenario in the repo uses -placeholders at all (grepped across `seed_data/` and `starter_data/` — zero -occurrences), so the collision is hypothetical. If it ever shows up in practice, -pre-filling the `Name` field from the persona is a few lines in the modal with -no backend change. - -**Later.** `AdventureUpdate` gains the three fields, and `PlotPanel` gets a -Protagonist block at the top. The debounced-save wiring is already there. -`WorldStateDrawer` shows the name as the character-sheet heading but does not -edit it — one edit surface, not two. - -## Files - -| File | Change | -|---|---| -| `models.py` | three columns on `Adventure` | -| `migrations.py` | 74, 75, 76 | -| `schemas.py` | `AdventureCreate`, `AdventureUpdate`, `AdventureOut` | -| `context/builder.py` | `persona` section after `plot_essentials` | -| `worldstate/render.py` | state-section label, reference line | -| `worldstate/__init__.py` | export whatever `render.py` newly exposes | -| `routers/adventures/crud.py` | store persona at creation | -| `bundle.py` | export key + import read | -| `frontend/components.jsx` | modal gains persona fields | -| `frontend/pages/Scenarios.jsx`, `Home.jsx` | always open the modal | -| `frontend/pages/Play/panels/PlotPanel.jsx` | Protagonist block | -| `frontend/pages/Play/drawers/WorldStateDrawer.jsx` | heading | -| `frontend/api.js` | pass the fields through | - -## Tests - -| File | What | -|---|---| -| new `test_persona.py` | create with persona; update; renders in the system block; works with no `stat_schema`; empty name is a no-op; AI delta at `persona.*` refused | -| `test_worldstate.py` | state line reads `Kaelen (player):`; `player.hp` still resolves | -| `test_prompt_caching.py` | persona is in the static prefix, above history | -| `test_bundle_v2.py` | round-trips; a bundle without the key still imports | - -## Bundle format - -No `FORMAT` bump. The export gains a `persona` key and the import reads it with -`.get()`, so a v2 bundle written before this change imports with an empty -persona — which is the same as not having one. - ---- - -# Phase 2 — what the summarizer is told - -Depends on Phase 1 only for the protagonist's name. Everything else it needs is -already in the database and simply never passed. - -## The cast brief - -Build one short block and prepend it to both the memory prompt and the summary -prompt: - -``` -Cast: -- Kaelen (he/him) — the protagonist. A half-elf ranger, exiled from the - northern holds... -- Gwen — a loyal ranger and Kaelen's ally. Quick with a bow, dry-humoured. - -Setting: - -``` - -Sources, in order: - -1. **Protagonist** — the Phase 1 persona. Falls back to "the player" when unset, - so the change still helps adventures with no persona. -2. **NPCs** — `stat_schema.npcs`: `name` + `desc`. Already there, never used - outside the turn prompt. -3. **Story cards** — for adventures with no RPG layer, run the existing - `_match_cards()` over the block being summarized and take the matched cards' - names and entries. This reuses the trigger code and yields exactly the - entities that appear in *that block*, not the whole world. - -## Two rules for the brief - -**Fixed descriptions only. No live stats.** It is tempting to include -`Gwen: trust 40 (wary)`. Do not. It makes every memory prompt different, which -loses prompt caching, and worse, it makes the same event summarized at two -different times come out framed differently — which is the problem being fixed. - -**The summary gets it for free.** `_update_story_summary` builds from the memory -bullets, so better memories produce a better summary with no further change. -Pass the brief there too, because that function falls back to raw story text -when memory-writing has fallen behind. - -## The prompt changes - -`MEMORY_SYSTEM_PROMPT` gains an explicit framing rule: - -- write in third person, never "you" -- refer to the protagonist by name -- name characters rather than using bare pronouns - -Expected effect: - -``` -before: You entered a room and she stopped you. -after: Kaelen bribed Gwen with fifty silver to hold the north door while - he went down alone. -``` - ---- - -# Phase 1 as built - -Everything above describes what shipped. Three notes on where it differs from -the sketch, and what has not been verified. - -## Decisions taken during the build - -**The section reads as one paragraph.** `render_persona` joins the sentence and -the description with a space rather than `SEPARATOR`. A blank line inside a -two-sentence section about one character reads as two unrelated notes. - -**`Field` gained a `maxLength` prop.** The persona name is `VARCHAR(80)` and the -schema rejects 81 characters with a 422. Without the attribute the player only -learns that from a toast after typing, so the cap is now enforced in the input -as well. Every other `Field` is unaffected — the prop is optional. - -**The blank-adventure button opens the modal too.** It used to create the -adventure immediately. It now collects a persona first, and passes -`title: 'Blank Adventure'` explicitly, because there is no scenario to take a -title from. - -**`test_prompt_caching.py`'s fixture now sets a persona**, so every test in the -file that guards the static block runs with one present. - -## Verified - -- Migration 73 → 76 on a real pre-existing SQLite database: columns added, the - existing row preserved, persona empty, `render_persona` returns `""`. -- 593 backend tests pass, including 26 new ones in `test_persona.py` and 3 in - `test_bundle_v2.py`. -- `npm run build` is clean. - -## Driven in a browser - -Chromium via Playwright, against a fresh database with the demo scenarios -seeded. No API key needed: Insights assembles the prompt without calling a -model, so every check below runs on the real assembled context rather than on -a unit-test stub. 21/21 checks passed. - -What was confirmed on screen and in the live `/context` payload: - -| | | -|---|---| -| The modal opens for a scenario with **no** `${...}` placeholders | it is now the only way to name a character, so it can no longer be conditional | -| The section order is real | `narrator, world_state_guide, world_state_rule, ai_instructions, **persona**, plot_essentials, …` | -| The persona is in the system half | `You are Kaelen (he/him). A half-elf ranger…` present in `prompt.system`, absent from `prompt.story` | -| The live stat line carries the name and the path | `Kaelen (player): hp 100/100 (full health), mana 30/50 (brimming)…` | -| The stat guide ties the name to the path | `Protagonist Kaelen … player.` | -| The drawer heading follows the name | rail read `KAELEN`, not `You` | -| A rename propagates | renamed to Aria in the Plot panel → drawer read `ARIA` after a reload, and the prompt read `You are Aria (he/him).` / `Aria (player):` | -| Clearing every field restores the old behavior exactly | no `persona` section, stat line back to `You:`, no `Protagonist` line in the guide | -| The blank-adventure button collects a persona and keeps its title | `title='Blank Adventure' persona='Wren'` | -| **A persona works with no RPG layer at all** | a blank adventure's sections were `['narrator', 'persona', 'length_hint']` — the case this feature was added for | - -Two request failures in the run were the sandbox rather than the app: Google -Fonts is blocked by the egress policy, and the analytics beacon is aborted when -the page unloads. Neither appears with normal network access. - -The driver script is not in the repo. There is no frontend test runner yet -(plan/17 stage 5), and one Playwright script is not the place to start one. - -## A note for whoever runs the tests - -`tiktoken` downloads `cl100k_base` from `openaipublic.blob.core.windows.net` on -first use, and 182 tests fail with a proxy error where that host is blocked. -The encoding is reconstructable offline from the npm package `js-tiktoken`, -whose `dist/ranks/cl100k_base.cjs` holds the same ranks in a compressed form — -decode it, write ` ` per line sorted by rank, and the result -matches the SHA-256 that `tiktoken_ext/openai_public.py` hardcodes, so the -reconstruction is verifiable rather than trusted. Drop it at -`$TIKTOKEN_CACHE_DIR/`. - ---- - -# Phase 2 as built - -## The cast comes from the story cards, not from `stat_schema` - -This is the one thing the sketch above got wrong, and it made the change much -smaller. `scenario_text.scenario_card_specs` already turns **every schema NPC -into a story card** on the adventure at creation, deduplicated against the -hand-written cards by name. So the cards are a single unified cast source that -covers schema NPCs, an author's own cards, and an adventure with no RPG layer, -through one path instead of three. `memorybank` never reads `stat_schema`. - -## Keyword matching alone was not enough - -The sketch said to run `_match_cards` over the block. Built that way first, and -it failed the exact case the change exists for. - -The block `You push the door open. She grabs your arm.` matches **no** card -keyword, so the brief listed the protagonist and nobody else — leaving the -summarizer guessing at precisely the moment it was handed a brief to stop -guessing. Seed 04 gives Gwen the trigger keys `"Gwen, ranger, her"`, and even -that does not save it: the text says "she", not "her". - -So the roster is **matched cards first, then topped up with the other -`character` cards** to `MAX_CAST_MEMBERS`. Places and items are not topped up — -an unmentioned tavern is not who "she" was — but a place that *is* mentioned -still matches normally. - -That asymmetry with the turn prompt is deliberate. Including an untriggered card -as lore would be wrong: it is not relevant to the next sentence. Including an -untriggered character in a roster is right: the question the roster answers is -"who could these pronouns be", not "what is on stage". - -## `_match_cards` became `match_cards` - -Two callers now run the same rule, so it is public and exported from -`app.context`. One rule, one implementation. - -## What actually gets sent - -Verified against the seeded Bandit Camp scenario, with the real -`_create_due_memories` path and a stub provider: - -``` -Cast: -- Kaelen (he/him) — the protagonist. A half-elf ranger, exiled from the - northern holds for a killing he still won't explain. -- Bandit Camp — A rough camp of bandits in a forest clearing, holding a - stolen caravan strongbox. -- Gwen — A loyal ranger and the player's ally. Quick with a bow, - dry-humoured, fiercely protective. … -- Bandit Leader — The scarred leader of the bandit camp, guarding the - strongbox. … - -Setting: -The player and Gwen, a loyal ranger ally, are raiding a bandit camp to -recover a stolen strongbox. … - -Story excerpt: - -… You are six paces from the strongbox when she hisses a warning. … - -Memory: -``` - -That last line is the case in miniature: "she" is now resolvable. - -**With no persona set**, the roster still lists the NPCs and the setting, and -the system prompt tells the model to call the protagonist "the player". Phase 2 -therefore improves adventures that never set a persona at all. - -**With no persona, no cards and no plot essentials**, the user message is byte -for byte what it was before this change — `Story excerpt:` first, no stray blank -lines. A test holds that. - -## Decided, from the sketch's open questions - -**Existing memories: left alone at first, and now rewritable on demand.** The -original answer was to let eviction age them out at `memory_bank_capacity` (80), -on the grounds that re-summarizing would duplicate whatever was still in the -bank, because nothing deletes the old rows. That reasoning was wrong about the -only option: a memory can be rewritten in place rather than written again. -See "Rewriting a bank written under the old prompt", at the end of this file. - -**Retrieval framing mismatch: watched, not fixed.** `retrieve_memories` still -embeds the last 4 actions raw, in second person, while new memories are third -person and named. Embeddings handle paraphrase well, so this is speculative. -If retrieval quality visibly dips, prepending the same brief to the query text -is the first thing to try. - -## Run with a real model, and what it changed - -Run end to end against a real model. One story generated through the app's own -`build_context` a turn at a time, then **both** memory prompts run over the -**same** blocks, so the story is held constant and the prompt is the only -variable. Neither arm sees the other's output, and the model is never told what -is being measured. The control is `MEMORY_SYSTEM_PROMPT` as of commit `9cdcb55`, -read out of git rather than pasted, so it cannot drift from what shipped. - -The harness is `backend/tools/memory_ab.py`, and it goes through -`OpenAICompatibleProvider` rather than calling a model directly, so the run -exercises the provider, the streaming path and `complete()`. Pointed at -`tools/claude_shim.py` it spends a Claude subscription instead of API credit: - - python tools/claude_shim.py --port 8787 & - python tools/memory_ab.py --out ab.md - -**The full transcript, with every memory, both summaries and the story they were -written from, is in `plan/18-appendix-memory-ab-run.md`.** - -### The reported fault reproduced, and the fix held - -| | words | framing | -|---|---|---| -| memory 1, before | 34 | second person — "**You** crept low through the mist…" | -| memory 2, before | 105 | third person — "**The player** asked Gwen to…" | -| memory 1, after | 72 | "**Kaelen** and Gwen crouched at dawn…" | -| memory 2, after | 68 | "**Kaelen** and Gwen infiltrated the bandit camp…" | - -Two consecutive memories in one bank, written from the same story minutes apart, -in two different persons. That is the complaint, reproduced under controlled -conditions rather than argued from the prompt text. - -**A second run, on a freshly generated story, qualifies that.** Its two control -memories were both "the player", consistently — the second-to-third person drift -did not recur. So the drift is a real thing a model does, seen once, not -something it does every time. Read the person-drift row as one observation. - -What holds across both runs is the thing the change is actually for: **four -control memories, and not one names the protagonist. Four treatment memories, -and all four do.** The control has never been told a name. See -`plan/18-appendix-memory-ab-run-2.md`. - -The control also got a fact wrong that the treatment did not. The player's move -was `grab her wrist and pull her down behind the woodpile`; the before-memory -recorded "The player asked Gwen to grab her wrist and pull her down behind the -woodpile", inverting who acted. One sample, so this is an observation rather -than a claim — but it is the failure mode the brief predicts, since without a -cast there is no way to tell whose wrist "her wrist" is. - -### It also found a real problem, which is now fixed - -"1-2 plain sentences" is not a length. The same model wrote 34 words for one -block and 105 for the next. A 105-word memory is a paragraph, and -`memory_top_k` injects five of them every turn, so the bank's running cost is -set by a number nobody had ever stated. - -`MEMORY_MAX_WORDS = 50` now states it, and the prompt says which details to keep -when trimming: the ones a later scene could turn on. Re-run over the identical -story, the same two blocks came back at **32 and 58 words**, still naming -Kaelen, still third person, and still carrying every load-bearing fact — the -camp map, the strongbox behind the second tent, the strap frayed near through. -The second run, on different prose, came back at **54 and 55** against controls -of 89 and 107. Overshooting 50 slightly is expected: models exceed word budgets, -which is why `builder.length_hint` carries a `LENGTH_BUFFER` for the same reason. - -Across both runs the control ran 34, 89, 105 and 107 words — a four-fold spread -with no budget stated anywhere. That is the number this found. - -The variance is the real gain. Before, across both runs: 34 to 107. After: 32 -to 58. - -### What this does not show - -The model behind the run is a Claude model. The app talks to an -OpenAI-compatible endpoint, and the parser in `worldstate/parse.py` exists -because weaker free models emit trailing commas and leading `+`. So this shows -the prompt is followable and that the brief supplies the missing information. -It does not show that a weaker production model complies as well. An explicit -framing rule is usually worth *more* on a weaker model, but that is an -expectation, not a measurement. - -Two side observations from the same run, both pre-existing behavior working -correctly: the model sent `{"milestones.strongbox_found": false}`, `apply_delta` -refused it ("a milestone is sticky, so only true is accepted"), the refusal -reached the model through `render_refusals`, and the next turn sent `true`. The -Phase 1 persona also held across all six generated turns. - ---- - -# Rewriting a bank written under the old prompt - -An adventure played before this change keeps a bank of unnamed, second-person -memories, and those are exactly the rows `memory_top_k` injects into every turn -from now on. Ageing them out only works for an adventure that is still being -played, and only after another 80 memories have been written. So there is a -backfill: `backend/tools/rewrite_memories.py`. - -``` -cd backend -python -m tools.rewrite_memories # what would change -python -m tools.rewrite_memories --write --limit 3 # try three of them -python -m tools.rewrite_memories --write --embed # the whole backfill -``` - -Without `--write` it makes no model calls and spends nothing. It reads whichever -database the app reads — `AIDND_DB_PATH`, or `DATABASE_URL` on a hosted deploy. - -**In place, not delete-and-regenerate.** The obvious alternative is to drop the -bank and rewind `cursors.MEMORY`, letting the post-turn pass write it again. -That loses everything the row carries besides its text: whether it is pinned, -how often it has been retrieved, and the node it hangs off, which is what makes -a fork inherit the right memories and no others. It would also trickle the bank -back at `MAX_MEMORIES_PER_RUN` per turn, so an adventure nobody is playing would -never recover. Rewriting `text` keeps the row and costs one call per memory. - -**One prompt assembly, not two.** `memorybank.summarize_block` is now the single -place a memory prompt is built, and both the post-turn pass and the tool call -it. A backfill that assembled its own prompt would be writing memories with a -prompt that never shipped, and nothing would report the difference. - -**Reading the block back is the one genuinely new part.** A memory records -`source_start`, `source_end` and `branch_id`, and `memorybank.source_block` -turns those back into actions. The subtlety is the branch: the read has to use -the lineage of *the branch the memory was written on*, not the branch the -adventure is playing now. After a fork, the same depths hold different actions -on each side, so a read through the adventure's current path would summarize the -wrong story and say nothing about it. `test_memory_rewrite.py` builds that fork -and holds the rule. - -**What it will not touch:** - -- A memory with no source range — hand-written, or migrated by 62 from before - memories had coordinates. There is no block to rewrite it from, and the player - may have typed it. -- A memory whose actions have since been deleted. -- An adventure whose owner has no API key in Settings, because summarization - spends the user's own key by construction and never the shared demo key. - `--api-key`, `--model` and `--endpoint` override that — the last of these - points a run at `tools/claude_shim.py`, so a backfill can spend a Claude - subscription instead of API credit. - -**The vector is cleared for every memory it rewrites**, because the stored one -describes wording that no longer exists. That takes the memory out of the ranked -bank until something embeds the new text: the app's own post-turn pass does it -`MAX_EMBED_BATCH` at a time, or `--embed` does it in the run. Re-embedding always -uses the owner's own embedding model and endpoint, never `--endpoint`, because a -vector is only meaningful against the vectors it is ranked beside. - -**Stop the app before running with `--embed`.** A running process caches vectors -by memory id and expects to be the only writer (`memorybank._vector_cache`), so -a vector written from outside it can sit behind a stale cached copy until it -restarts. Clearing alone is safe at any time: an unembedded memory leaves the -catalogue, which is what the cache invalidates on. - -## Running it against the hosted deploy - -Two things make production different from a local database, and both are easy -to get wrong quietly. - -**It holds other people's stories, and each adventure is summarized with its -owner's key.** An unfiltered `--write` would spend other people's money on -memories they did not ask to have rewritten. `--email` restricts a run to named -accounts and `--adventure` to single adventures; the dry run costs nothing and -prints the owner of each. Guests have no email and are reachable only by id, -which is the right amount of friction for rewriting a stranger's bank. - -**The stored API keys are encrypted with `AIDND_SECRET_KEY`.** Render generates -that value and holds it for the web service, so a run from a checkout has to -carry the same one. With a different secret, `decrypt_secret` returns "" rather -than failing, and every adventure is reported as having no key — a run that -looks like it worked and did nothing. - -``` -AIDND_DATABASE_URL= \ -AIDND_SECRET_KEY= \ - python -m tools.rewrite_memories --email you@example.com -``` - -Run it from a checkout rather than from a shell on Render. The image copies -`backend/app` alone, so `tools/` is not on the box, and the free plan has no -shell anyway. The database is the same one either way. - -Two smaller notes for that environment. The Neon URL to use is the direct -endpoint, not `-pooler`, for the same reason the sizing queries in STATUS use -it. And an adventure owned by a visitor playing on the shared demo key is -skipped, because summarization has never spent that key. - -**The story summary is not rewritten.** It is one text per adventure rather than -a bank, and `_update_story_summary` hands the model the whole of it and asks for -an updated version under the new framing rule, so the next scheduled update -should carry it over to third person by itself. If it does not, that is a -separate and much smaller fix than this one. diff --git a/plan/STATUS.md b/plan/STATUS.md deleted file mode 100644 index 5eb94ca..0000000 --- a/plan/STATUS.md +++ /dev/null @@ -1,1162 +0,0 @@ -# Where things stand - -Read this first when picking the project back up. Updated at the end of a working -session; the per-phase plan files hold the detail, this holds the thread. - -**Last updated: 2026-08-31.** - ---- - -## The live URL is not the one in render.yaml - -**Production is `https://ai-dnd-1gmp.onrender.com`.** Render appended a suffix to the -`ai-dnd` service name in `render.yaml`, and plain `ai-dnd.onrender.com` belongs to a -different, suspended service that answers 503 with "suspended by its owner" — which is -easy to mistake for this deploy being down. The authoritative link is the one the -project page points at (`docs/index.html`), not the service name in the blueprint. -`GET /api/health` on the real host returns `{"ok":true}`. - ---- - -## Shipped and live, 2026-08-17 - -PRs #1 and #2 are merged and deployed. Verified against the running service: -`/api/health` 200, the adventure payload carries `action_count`, `/actions` returns -`{actions, total, has_more}` and accepts `before_id`. The app booting at all is proof -migrations 42–45 ran — `bootstrap()` executes at import, so a failed migration means no -service. The deployed JS bundle hashes to `index-4vjcKxkv.js`, which is what this tree -builds, so the frontend live is exactly this code. - -**Measured 2026-08-17, and then vacuumed again.** The earlier `VACUUM FULL actions;` -predated migrations 42–45, which rewrote every row — so by the time anyone looked, the -database was **144.2 MB**, well above the 99.6 MB it started from and nowhere near the -~53 MB projection. Nothing was wrong with the compression. Nothing had reclaimed the -space it freed. - -``` -before: database 144.2 MB, actions 131.4 MB -VACUUM (FULL, ANALYZE) actions -- 5.5s -after: database 65.0 MB, actions 52.1 MB -``` - -**79.2 MB reclaimed in 5.5 seconds**, against a 512 MB tier. `actions` now occupies -52.1 MB holding 50.4 MB of live column bytes, so there is essentially no bloat left. -`/api/health` answered `{"ok":true}` immediately after. The projection was right all -along. - -**Read the sizes from the column sums, not from `n_live_tup`.** The chunk count on the -TOAST relation said ~88 MB live and implied ~40 MB was reclaimable; the actual figure was -double that. `n_live_tup` is an estimate left over from the last ANALYZE, and after a -migration rewrites the table it is stale in the direction that makes bloat look smaller. -`sum(octet_length(col))` per column is the honest number. - -**Where the bytes are in `actions`** — one column, and it is not close: - -| column | rows | live bytes | per row | -|---|---|---|---| -| `context_snapshot` | 467 | **47.5 MB** | 104.1 kB | -| `variants` | 91 | 1.1 MB | 12.9 kB | -| `text` | 945 | 0.8 MB | 0.9 kB | -| `world_state_before` | 787 | 0.7 MB | 1.0 kB | -| `world_delta` + `reasoning` + `state_before` | — | 0.2 MB | — | -| total | | **50.4 MB** | | - -`context_snapshot` is 94% of the table. The per-action state columns a tree would touch -are rounding errors, which is the useful thing to know before phase 14 adds more of them: -**adding columns beside `state_before` is cheap; adding anything shaped like a context -snapshot is not.** - -`VACUUM FULL` takes an ACCESS EXCLUSIVE lock — the app blocks on `actions` for the -duration. 5.5 s at this size, but it grows with the table. Run it on the **direct** -endpoint, not `-pooler`: through transaction pooling it is unreliable. - -**Aggregates only, and ask first.** Real users are on this database. Counts, -`octet_length` sums and catalog sizes answer every sizing question this project has -needed; nothing requires reading a row of anyone's story. - ---- - -## What happened on 2026-08-31, part two — one action of settling slack - -**A block is no longer summarized while it ends on the newest action.** -`memorybank.SETTLE_SLACK` (1) asks for one action past a block before the block is -written. The block is still `MEMORY_INTERVAL` actions; only the moment moves. - -**This is not the SP4 holdback coming back, and the reasoning is worth keeping -straight.** The holdback existed because a retry rewrote `Action.text` in place, so a -memory could describe narration that no longer existed and nothing reported it. Sibling -attempts plus `forget_node` settled that, and SP4 deleted the holdback on the grounds -that "nothing is left to hold back". True — about staleness. Nobody priced the other -half. Withdrawing a memory and writing it again is correct and it is also two calls for -one block. - -**Retry and take-switching both refuse anything but the newest action** -(`takes.retry_action`, `takes.switch_take`), so the exposure was exactly one memory: the -one whose block ends on the tip. A block closes every 6 actions and a normal turn writes -2, so that was one turn in three, and each further retry of that turn paid again. The -summary pass was never the expensive half — `rewind_all` takes the mark to the same -depth however many times a player retries. - -**The slack buys nothing back, which is why it is worth having.** The block that just -closed is still inside the 16384-token history window in full, so a memory of it tells -the model what it can already read. Memories earn their place once the raw text has -scrolled out, and that is never the turn the block closed. - -**`forget_node` stays, and its path is still reachable.** Undo and delete reach any node, -and either can carry a summarized node back to the tip, where a retry of it lands on the -withdrawal again. Rare now instead of routine. - -632 tests green. Three test files moved with it: `test_memory_settling.py` asserts the -new rule (and a retry at the tip finding nothing to withdraw), `test_memory_rewrite.py` -builds 13 actions instead of 12 so both of its blocks settle, and the one path test in -`test_memory_nodes.py` sets the slack to 0, because it is about which actions a block is -read from rather than about when a block forms. - ---- - -## What happened on 2026-08-31 — the persona, and what the summarizer is told - -**`plan/18-persona-and-memory-quality.md` is the writeup. Both changes are on `main`** -— the note here that said they were sitting unmerged on -`claude/ai-dnd-memories-summarization-3muo98` is out of date; `main` is at `71b24b6`, -the tip of that work. Both green at 631 tests, plus the backfill below. - -**The protagonist now has a name.** An adventure carries `persona_name`, -`persona_pronouns` and `persona_desc` (migrations 74-76), and the player's stat block -renders as `Kaelen (player): hp 100/100` instead of `You: hp 100/100`. The paths do not -change — a path carrying the persona's name would break the moment a player renamed -their character, because `_history_text` replays stored deltas holding literal -`player.hp` strings. Empty name means the app behaves exactly as before, so no backfill. -Driven in a browser, 21/21 checks. - -**The summarizer used to be told nothing.** It got six actions of second-person prose -and no cast, no setting, and no instruction about what person to write in. It now gets a -cast brief built from the story cards — which already cover the schema NPCs, because -`scenario_card_specs` turns every one of them into a card at adventure creation. - -**Keyword matching alone was not enough, and only running it showed that.** Built to the -plan first, the brief for "She grabs your arm" listed the protagonist and nobody else: -the block that most needs a cast is exactly the one written in bare pronouns. Matched -cards now come first and the rest of the roster is filled with the other `character` -cards. - -**Running it against a real model found a second fault.** "1-2 plain sentences" is not a -length — the same model wrote 34 words for one block and 105 for the next, and -`memory_top_k` injects five every turn. `MEMORY_MAX_WORDS = 50` states it; the same -blocks then came back at 32 and 58, and a second run on a fresh story came back at 54 -and 55 against controls of 89 and 107. The A/B harness is `backend/tools/memory_ab.py`, -it drives the real provider through `tools/claude_shim.py`, and both transcripts are in -`plan/18-appendix-memory-ab-run.md` and `-run-2.md`. - -**Read one of those findings narrowly.** Run 1 produced two control memories in two -different persons, which is the reported complaint exactly; run 2's controls were both -"the player", so that drift is something a model sometimes does, not always. What holds -across both runs is that none of the four control memories names the protagonist and all -four treatment memories do. - -**Still unmeasured: whether a weaker model complies.** The run used a Claude model -through the shim. The app talks to an OpenAI-compatible endpoint, and -`worldstate/parse.py` tolerates trailing commas because free models emit them. Point -`memory_ab.py --endpoint` at the real provider to find out. - -**A bank written under the old prompt can now be rewritten.** The earlier answer was to -let eviction age those memories out, on the grounds that re-summarizing would duplicate -rows. It does not have to: `backend/tools/rewrite_memories.py` rewrites a memory's text -in place from the same actions it was written from, keeping the pin, the use counts and -the node it hangs off. Run it from `backend/`; without `--write` it makes no model calls -and only reports what would change. - -``` -python -m tools.rewrite_memories # what would change -python -m tools.rewrite_memories --write --limit 3 # try three of them -python -m tools.rewrite_memories --write --embed # the whole backfill -``` - -It reads whichever database the app reads (`AIDND_DB_PATH`, or `DATABASE_URL` on the -hosted deploy), so **take a copy first** — the old text is overwritten and kept nowhere. -`--endpoint`/`--model`/`--api-key` point the summarizer somewhere else, `claude_shim.py` -included. - -**Against production, name whose adventures you mean.** That database holds other -people's stories and each adventure is summarized with its owner's key, so `--email` -(or `--adventure`) is what keeps a run to your own. Reach it from a checkout, not from -a shell on Render — the image copies `backend/app` alone, so `tools/` is not on the box: - -``` -$env:AIDND_DATABASE_URL = Read-Host 'Neon URL' # direct endpoint, not -pooler -$env:AIDND_SECRET_KEY = Read-Host 'Secret key' # the web service's value -.venv/Scripts/python.exe -m tools.rewrite_memories --email you@example.com -``` - -`Read-Host` keeps both out of the PowerShell history file. Use the venv's Python: the -system one on this machine is 3.10 and has none of the dependencies. - -`AIDND_SECRET_KEY` is not optional there: stored API keys are encrypted with it, and -with the wrong one `decrypt_secret` returns "" and every adventure is reported as having -no key — a run that looks fine and does nothing. Hand-written memories, and memories whose actions have been deleted, are left -alone; so is an adventure whose owner has no API key, because summarization spends the -user's own key and never the demo key. - -**Two things it made necessary in the app.** `memorybank.summarize_block` is now the one -place a memory prompt is assembled, so a backfilled memory cannot be written by a prompt -that never shipped. And `memorybank.source_block` reads a memory's block back out of the -story — on the lineage of the branch the memory was written on, not the branch being -played, because after a fork the same depths hold different actions on each side. - -**The vector is cleared for every memory it rewrites**, since the stored one describes -the old wording; the app's post-turn pass re-embeds them `MAX_EMBED_BATCH` per turn, or -`--embed` does it in the run. Stop the app before using `--embed`: a running process -caches vectors by memory id and expects to be the only writer. - ---- - -## Pick up here - -**`plan/17-refactor.md` is the active phase.** It carries its own progress table, which -is the first thing to read when you pick the work back up. It runs in six stages, and -SP8 below is stage 3 of it. Nothing in it changes what the app does. - -**`plan/14-phase-story-tree.md`, SP8 — drop the legacy columns.** SP8 was gated on the -tree being proven live, and it now is: SP9 merged, and production answers `/api/health` -with the tree schema in place. SP8 drops `index`, `variants`, `variant_index`, -`variant_count`, the two legacy cursors and the two `*_before` snapshots. Check that -nothing still reads `variant_count`/`variant_index` before dropping them, and note this -is the migration shape that rewrites toasted values, so it owes one `VACUUM FULL actions;` -on the direct (non-`-pooler`) endpoint afterwards. - -Ahead of that, `plan/16-world-state-refusals.md` is closed. Its fixes were driven in a -browser on 2026-08-28, first on the demo model and then on Claude Sonnet through the -local shim, and all five changes work. One item in it is still open and is the next -world-state job: **Milo's three Pokemon share one `active_hp` stat**, so a switch leaves -the newcomer at 0 HP and every later hit is refused. Give them their own NPC entries, -which also removes the `initial == max` shape from that demo. - -**SP9 and SP10 are both on `main`, despite what earlier notes here said.** The branches -`sp7b-take-pager` and `sp10-memory-bank-eviction` still exist and still read as unmerged -to `git branch --no-merged`, because the work landed as squashes. Check the code, not the -branch list: `actions.parent_id` in `models.py` is SP9, and commit `c0cd6fa` is SP10. - -**Driving it found two bugs the suite could not have.** The pager did not appear until the -page was reloaded — a retry's reply is the second take of its turn, and the SSE stream -builds its own `ActionOut`, so it was the one payload that never got the annotation. And -retaking a player turn gave `> You > You ...`, because the editor is seeded with the stored -text, which is already formatted. Both fixed, both with regression tests. **Neither was -reachable from any test that existed, and the frontend still has no test runner** — which -is the standing reason this project keeps finding UI bugs by hand. - -**State was checked against the running app, not just asserted.** On the HP-script demo, a -path crossing *three* branches carried exactly the damage of the nine AI nodes on it and -none of the 66 points sitting on takes those branches never told. `test_take_state.py` -pins the same guarantee with a script that adds ten gold a turn; three of its five tests -fail if `roll_back_before` is removed. - -**What SP7 got wrong, in one line: a chip meant two different things.** At the tip it -switched; further back it only previewed, and taking that line needed a second button. The -meaning depended on where the reader was standing. SP9 replaces it with `‹ 2/4 ›` that only -ever steps, a fork button on every turn, and a rule that decides everything else: - -> Reading a take is free and tells the server nothing. **Writing below one is what makes -> the branch.** - -Three things came out of building it that outlive the subphase: - -- **Takes are grouped by parent, not by coordinate.** A take forked onto its own branch - leaves the (branch, depth) its siblings are at and would read `1/1` beside their `1/3`. - `actions.parent_id` fixes it, and is read for nothing else — one indexed lookup, never a - walk, no read of the story changed. See SP9 in the phase plan for why the alternative - (fork points as nodes rather than depths) was rejected. -- **`delete_turn` meant "every take at this coordinate".** Once the group spans branches, - undo reached onto another branch and deleted a take belonging to a line nobody asked - about. Anything that reads a take group and then *writes* needs to ask whether it means - the turn or the coordinate. -- **The adventure GET does not build `ActionOut`.** It hands the window to the - relationship with `set_committed_value` and lets Pydantic walk it. Patching every place - that builds `ActionOut` therefore misses the one path every page load takes — worth - remembering for the next field added to a page. - -**The vacuum ran on 2026-08-18, and the projection was wrong in the useful direction.** - -``` -before: database 82 MB, actions 69 MB (heap 5184 kB, indexes 184 kB) -after: database 71 MB, actions 58 MB (heap 1760 kB, indexes 96 kB) -``` - -**11 MB reclaimed**, against a "mid-hundreds of MB" guess. The heap is 1.7 MB and -everything else is TOAST: `context_snapshot` is 94% of the table and lives out of line, so -when SP1's and SP4's migrations updated small per-row columns each UPDATE wrote a new heap -tuple and **reused the existing TOAST pointer** — Postgres only copies a toasted value when -that value itself changes. Four rewrites bloated a 1.7 MB heap, not a 58 MB table, and the -arithmetic closes (3.3 heap + 0.1 index + 7.6 TOAST = the 11 MB the database gave back). - -**So the rule keeps its cost estimate, not its size.** *After a migration that rewrites -`actions`, one `VACUUM FULL`* still stands — but bloat scales with **the heap**, whenever -the migration only touches small columns. The 144 MB incident was different because that -rewrite genuinely moved every toasted value. **SP8 will be the 144 MB shape, not this one:** -it drops `variants` and the two `*_before` snapshots, which are the toasted kind. And note -`ALTER TABLE … DROP COLUMN` is metadata-only in Postgres — it frees nothing by itself, and -the space comes back only at the next `VACUUM FULL`. - -Growth since the previous vacuum (65.0 MB / 52.1 MB) is real: migrations 61–62 and a day -of play, not bloat. - -**SP8 is gated on the tree being proven live, and it is not.** It drops `index`, -`variants`, `variant_index`, `variant_count`, the two legacy cursors and the two -`*_before` snapshots. `variant_count` / `variant_index` are the ones to watch: SP7's -attempt chips still read both, so SP8 has to move the chips onto the sibling group before -it drops them. Everything else has been unread since SP3/SP4. - -**Deploy before SP8, not after.** SP7 is a natural release: the phase is usable from the -screen for the first time, and dropping columns is the one step that cannot be rolled -back by redeploying the previous build. - -**The schema is live in code but not on production.** When this ships, the deploy needs -one `VACUUM FULL actions;` on the direct (non-`-pooler`) endpoint afterwards — SP1's -migration rewrites every row and SP4's rewrites it three times more, so **two vacuums are -owed and one run settles both**. SP3's, SP5's, SP6's and SP7's changes need none (SP5 and -SP6 add no migration; SP7's migration 61 touches `branches`, a handful of rows per -adventure). See the 144 MB lesson at the top of this file. - -Three things to carry forward: - -- **The bundle is the one thing here a migration can never reach.** `app/bundle.py` owns - both formats and nothing else knows either. Its rule — *carry what was chosen, never - what is derived* — decided SP7's naming too: `branches.name` is stored because a player - picked it, and an unnamed branch is drawn from its fork depth rather than given a - generated label that would go stale when a branch before it is deleted. -- **A one-line subphase spec can hide a schema change.** SP7 read as "plus the UI for - switch/rename/delete"; two of those three had no backend at all. Check the routes exist - before believing a spec that says "frontend". -- **The spatial node map was deliberately not built.** SP7 shipped a rail instead, on the - grounds that a per-node map is a second windowing problem at 600 nodes. It is a - standalone feature whenever it is wanted, and it needs no new endpoint — the rail and a - map both draw from `GET /branches`. **A branch-level map was built on 2026-08-20** (see - below); the per-node version is still not, and the windowing argument still stands - against it. - -And one known cost, not a bug: the two memory marks are a single pair on the adventure, -so switching branches makes the mark on the branch being left unreadable from the new one -and that ground is summarized again. `Path.depth_on` answers "nothing covered", which is -the safe direction. Per-branch cursors are the fix if it ever matters. - -**After any migration that rewrites `actions`:** one `VACUUM FULL actions;`. That is the -lesson of the 144 MB above — a rewrite doubles the table and only a `VACUUM FULL` gives -it back. SP8's migration rewrites every row. - -**The scroll gap is closed.** It was driven by hand on the 602-action `--keep` fixture -during SP7 — three prepends, 5 px of drift, no throw-to-the-end. What is still missing is -an automated version; see SP7's entry in `plan/14`. - ---- - -## What happened on 2026-08-28 — world state against a real model - -**The refusal loop ran end to end for the first time.** `plan/16` shipped a correction -that is injected into the next turn's prompt when the engine clamps or rejects a change, -and nothing had ever observed it working. Turn 3 of the Pokemon demo clamped to nothing, -turn 4's assembled prompt carried the note verbatim, and the model's next delta was -correct. The full record, with the four turns and their chips, is at the end of -`plan/16-world-state-refusals.md`. - -**Two failures blamed on the code were the model.** `graveler_defeated` never firing and -`world.turn` never moving both survived four playtests on the free demo model, with the -instructions present and correct in the assembled prompt. Both worked on the first try -against Sonnet. When an instruction is provably in the prompt, the next thing to change -is the model, not the wording. - -**There is now a permanent way to test against a real model.** -`backend/tools/claude_shim.py` serves an OpenAI-compatible endpoint backed by the local -`claude` command line tool, so a demo can be played on a real model with no API key. -About $0.04 a turn against the subscription. The README documents it under **Connect a -model**. - -**Renaming a seed scenario used to strand its old row.** `seed.py` matches a seed file to -its scenario by title, so changing a title inserted a second public scenario and left the -first orphaned and public forever. This is the same failure this file already records for -"Road to the Champion". Seed files now carry `previous_titles`, and the rename lands on -the existing row. Verified: the Pokemon demo kept its id. - -**A seeded scenario nobody claims is now deleted on boot.** `previous_titles` -stops a rename stranding a row; the sweep removes the ones already stranded, which -retires "[Demo] Road to the Champion" without a console. Only rows with a NULL owner -and `is_public` are reachable, and `adventures.scenario_id` is `ON DELETE SET NULL`, so -an adventure started from a deleted demo keeps its story and loses only the cover art. -A seed file that fails to parse, or an empty seed directory, skips the sweep. - -**Every new guest is given a pre-played adventure.** `app/starter.py` copies a shipped -export bundle into each new guest account. The point is the first screen: real turns with -their world-state chips, including a refused change and a milestone, before spending any -of the daily demo turns. The row building inside `POST /adventures/import` moved to -`bundle.materialize` so both callers share one writer. The copy is linked back to its -demo scenario by title, because an adventure has no art of its own and a bundle cannot -carry an id that means anything in another database. - -**An SVG data URI is not usable as scenario art.** `app/images.py` accepts raster formats -only, deliberately, because SVG can carry script and the bytes are served from the app's -own origin. A rejected value is stored but yields an empty `image_url`, which fails -quietly. The Pokeball is a 3.2 kB PNG, drawn by `backend/tools/make_pokeball.py` with -`zlib` alone. - ---- - -## What happened on 2026-08-21 — visit analytics - -The hosted demo can now answer whether anyone is using it. `/analytics` is a dashboard — -visitors, pages, referrers, countries, devices, which shared scenarios get played, turns and -demo-key spend, API and turn errors, and a funnel from *visited* to *played a turn* to -*signed up*. **Built, green (497 tests), driven by hand against a synthetic 90-day fixture. -Committed and pushed to `main` on 2026-08-22 as `041f9e2`, so Render is deploying it.** - -**The one thing left to do is not in the repo.** `AIDND_ANALYTICS_EMAILS` is `sync: false`, -so the blueprint cannot fill it: until it is set in the Render dashboard the dashboard is -invisible to everybody, including the person who built it. Collection runs regardless — only -the view is gated — so the counters are filling in the meantime and nothing is lost by -setting it late. - -**It is gated on its own allowlist, `AIDND_ANALYTICS_EMAILS`** — not `AIDND_POWER_USERS`. -An unmetered tester is not automatically someone who sees the traffic numbers. The route -404s and the nav link is absent for everyone else, same treatment as AI Chat. - -**The counters are anonymous; the access log beside them is not, on purpose.** A visitor in -`analytics_daily`/`analytics_visitor_days` is `HMAC(secret, "visitor:")` truncated -to 32 chars — one-way, so those two tables cannot be joined back to `users`, and keyed, so no -client can compute one. Story content never reaches that module. The operator's own visits -are not counted there (multi-user only — excluding them locally would leave the page -permanently empty on the machine it is developed on). - -**`accesslog.py` is the identifying half, added the same week.** `access_events` records -sessions, sign-ins, registrations and failed attempts with address, email (or `Guest #n`), -country and device, read on an "Access log" tab of the same page and behind the same owner -gate. It is a separate module and a separate table so the anonymity of the counters stays a -property of the code rather than a convention. Three things it leans on: the address comes -from `limits.client_ip` — now public, and the only place that decides which hop to trust, so -a spoofed `X-Forwarded-For` cannot forge a row; `user_id` carries **no foreign key** and -`who`/`is_guest` are snapshots, so a row outlives the guest cleanup that deletes the account; -and session rows are thinned to one per day per address, since `/auth/me` runs on every page -load. Nothing is purged — that was the deliberate choice. The published docs describe the -analytics generally and do not enumerate this. - -**A visit is a write and never a read.** Counts accumulate in a process-local dict and flush -every 60s as UPSERTs into `analytics_daily` — a generic `(day, metric, label) -> hits` -counter — plus one row per visitor per day in `analytics_visitor_days` for the funnel flags. -Every dashboard query is a `GROUP BY` returning tens of rows however much traffic sits -behind it. Deliberate, given §2.5: adding a feature that reads rows per request would have -undone the egress work. - -**No migration was needed.** Both tables are new, and `bootstrap()` calls `create_all` on -existing databases too — the same route `branches` took in Phase 14. Nothing was appended to -`MIGRATIONS`, so `LATEST_VERSION` is still 64. - -Three things worth remembering out of building it: - -- **The funnel counts people, not clicks.** A player who starts six adventures is one person - who started an adventure. That is the entire reason the per-visitor-day table exists; its - flags only ever turn on, and `is_new` is settled by the first write of a visitor's first - day and never updated after. -- **A failed turn is an HTTP 200 with a bad ending.** The status-code middleware cannot see - one, so a demo whose model started refusing every request would look perfectly healthy. - `turn_error` is counted in a new `turn_error()` helper that all five SSE error paths in - `_generate_turn` now go through. -- **The tests run on SQLite; production is Neon.** A flush that raises is caught and logged, - so a dialect mistake in the UPSERTs would have been invisible until the dashboard quietly - stayed empty. `test_the_upserts_compile_for_postgres` compiles both statements against the - Postgres dialect without connecting to one. - -**Still not verified: the narrow-screen layout.** The CSS follows the existing `max-width: -720px` block (single-column grids, funnel label above its bar) but `resize_window` is -ignored on a maximized Chrome, and the app sends `frame-ancestors 'none'` so it cannot be -checked in a sized iframe either. Desktop was driven by hand at 1568px. - -**`docs/guide.html` is now behind `docs/GUIDE.md`,** which gained §3.6. Nothing in the repo -regenerates it. - ---- - -## What happened on 2026-08-20 — the branch map - -The Branches panel gained a **⌗ See the tree** button opening a full-screen map: one -horizontal lane per branch, running from the moment it left its parent to the moment it -ends, joined to the parent by an elbow at the fork. Clicking a lane selects it; the -footer switches, renames or deletes it. **Merged to `main` and pushed on 2026-08-20** -(`c8e081e`), so it is on its way to Render with the rest of the branch. **440 backend tests -pass, unchanged — this is frontend-only.** - -**It is a branch map, not the node map SP7 refused, and that is the whole reason it was -cheap.** Lanes are bounded by branch count, not by node count, so the 600-node windowing -problem never appears: it draws from the same single `GET /branches` the rail already -made, and reads nothing else. - -New: `frontend/src/branches.js` (the tree maths — `branchLabel`, `orderBranches`, -`headLineage`, `layoutTree`, `momentTicks`) and `frontend/src/BranchMap.jsx`. `Play.jsx` -lost its private copies of the first two; the panel and the map now label and order a -branch through the same functions, so a branch cannot be called two things by the two -views. The three operations stayed in `BranchPanel` and are passed down, and `run` now -answers whether it worked so neither view clears a half-typed name on a refusal. - -`tools/tree_fixture.py` is the counterpart to `tools/branch_fixture.py`: four branches at -three different fork depths, one forked off a fork. **It is the whole of the map's -coverage** — the frontend still has no test runner — and it exists to be looked at. - -Three things found by driving it, none of which a test could have seen: - -- **`clientWidth` is not `contentRect.width`.** Seeding the measured width from - `clientWidth` counts the canvas padding the ResizeObserver leaves out, so the first - paint drew an svg 24 px wider than its box. And the observer's *initial* observation - did not arrive at all here, so dropping the seed and keeping only the observer left the - map never drawing. Both halves are needed, and the seed has to subtract the padding. -- **Only the name was being clipped.** The meta line under it was not, so a lane that - forks late ran its text off the right edge. Both are clipped now, and a lane starting in - the right third hangs its labels back over the fork instead — its row is its own band, - so there is nothing to the left to collide with. -- **The delete rule was in the server and in the map, but not in the list.** The panel - offered Delete on a branch the head was forked from and answered with a toast from the - server's 400. `headLineage` is the client's copy of that rule and both views now use it. - The server stays the authority. - -Also worth recording: **the 390 px iframe trick in the older notes no longer works.** The -app serves `frame-ancestors 'none'`, so an in-page iframe has no reachable -`contentDocument`. Narrow widths were checked by constraining `.branch-map` and measuring -`scrollWidth` against `clientWidth` instead, which tests the reflow path that actually -matters. - -## What happened on 2026-08-20, part two — the published docs caught up - -Everything the project publishes had drifted a full phase behind the code. The README, the -project page (`docs/index.html`) and the engineering guide (`docs/GUIDE.md` + its -hand-written `docs/guide.html`) all described a **linear** story: 151 tests, 37 migrations, -no tree, no takes, no branches. On branch `docs-story-tree`. - -**The numbers that were wrong:** 151 → **440** tests, 37 → **64** migrations, "all twelve -phases" → fourteen. Those appear in four places between the README, the project page's stat -tiles, and the guide's results table. - -**What was actively misleading, not merely stale.** The guide's §2.2 was *Two coordinate -systems, and the bug class they create*, and it explained the codebase through -`position_of_index`, `note_action_removed` and `settled_story_actions` — **all three deleted -in SP3**. §2.3 explained retry through `Action.variants` and `state_before`. A reader -following either would have gone looking for machinery that isn't there. §2.2 is now *The -story is a tree*, written at the same depth as §1.2 and §1.3: the seven bugs that were all -one bug, the lineage clause and its two properties, why takes group by `parent_id`, cursors -becoming anchors, and what the design is honest about. §2.3 is rewritten around -`state_after` and takes. - -**Three screenshots**, shot on a new `backend/tools/shots_fixture.py` — the Bandit Camp demo -scenario driven through eight scripted turns, three discarded takes forked onto branches, -one of them off a branch so the map has to nest. It exists for the same reason -`tree_fixture.py` does: the shots have to be reproducible, and there is still no frontend -test runner. `play-world-state.jpg` was reshot (it predated the whole tree UI); -`branch-map.jpg` and `branches-panel.jpg` are new. - -Worth not rediscovering: **`docs/guide.html` is hand-written, not generated from the -Markdown.** Every guide edit is two edits, and the HTML has its own vocabulary -(`.trap`/`.tag` callouts, `.stats`/`.stat`/`.v`/`.k` tiles) that has to be matched by hand. -The two files were checked for tag balance with `html.parser` and both pages were rendered -over a local `http.server` before committing — `file://` URLs are blocked from the browser -tooling, which is worth knowing before trying it again. - ---- - -## What happened on 2026-08-18, part five — the review, and PR #6 - -The stack went up as **one PR (#6)** rather than seven stacked ones: `sp7-tree-ui` was -sixteen commits ahead of `main` and **zero behind**, so the chain was already linear. -Then a `/code-review high` over `origin/main...origin/sp7-tree-ui` raised nine findings, -answered in commit `00feda8`. **409 tests.** Still not merged, still not deployed. - -**The headline finding was rejected, and the reasoning is the part worth keeping.** The -review called it data loss: a memory the player types lands on the head's coordinate, and -a retry withdraws every memory at that coordinate, so the note disappears. Reproduced, -and real. But it is the rule working — *a memory anchored to a node describes that node -and goes when the node goes* — and the product decision is that this is correct. - -**The root node is the exception, and the only one.** Migration 62 parks the entire -pre-coordinate bank on depth 0, because 0 is the one depth every branch can see. That -makes the opening node the one place holding memories it never produced, so withdrawing -it would retire a whole bank in a click. `forget_node` now keeps memories with **no -`source_start`** at `lineage.ROOT_DEPTH` and still withdraws a summary that genuinely -ends there — blanket-protecting depth 0 would have rebuilt the dangling-memory problem -`forget_node` replaced `prune_dangling_memories` to prevent. Both directions are tested. - -Eight repairs, of which three are worth remembering as classes rather than as bugs: - -- **A sibling group breaks every query that assumed one row per depth.** `_latest_narration` - ordered by `(depth desc, id desc)` and got the *newest* attempt, not the live one, so the - index screen quoted a take the player had thrown away. Anything ranking actions by - coordinate needs `live` in the filter. -- **A cap has to count what gets written, not what the file says.** The import counted a v1 - file's turns; each turn expands into a row per saved attempt, so a file inside a - 5,000-action cap could write 50,000 rows. Re-checked after `plan()`, which is pure and - runs before the adventure row exists. -- **`stateKey`, not `actions.length`, again.** SP7 fixed four components that refreshed on - story *length* when a branch switch changes *which story*; the attempt-switch path was - missed by the same reasoning, and `select_variant` restores state, withdraws a memory and - rewinds both cursors. Still nothing automated can see this: **the frontend has no test - runner.** - -Also: forking a live node on a borrowed ancestor promoted a sibling on a branch the caller -never named (now a 400 naming the branch switch, tested); a v1 import gave a typed memory -no depth, rebuilding the NULL migration 62 exists to remove; a retry after switching back -filed the new attempt into the middle of its group; `rename_branch` answered -`own_actions=0`; and `_backfill_cursor_anchors` numbered every action in the table once per -adventure — a window function the planner cannot push a correlation into, running under -lock at boot against live Postgres. It is correlated to the adventure being updated now, -which makes it an index lookup. **That last one is verified on SQLite only** — no -migration was pointed at Postgres to check it. - -**Every new test was checked to fail with its fix reverted** (reverse-apply the app-only -diff, run, restore). A regression test that passes against the unfixed code is not a -regression test, and seven of the eight fail as they should; the eighth — "a summary of -the opening node is still withdrawn" — passes both ways on purpose, because it guards -against over-correcting the root exception rather than against the original bug. - -**One behaviour change recorded rather than repaired.** Moving `_history` onto -`context_history.story_actions` also dropped blank-text rows from a user script's -`history` and from `info.actionCount`. It is the right shape — a textless row is this -app's bookkeeping and never reached a prompt — but a script firing "every N actions" now -fires on different turns, and no reading is compatible with both. `plan/14` says so. - -## What happened on 2026-08-18, part four — the tree, SP7 - -The tree reached the screen. A Branches panel beside Plot/Memory/Scripts/Insights lists -every line the story has taken and switches, renames or deletes one; under a retried turn -the ‹ 2/3 › pager is gone, replaced by attempt chips and a **take this path** that forks -when the story has already moved past. **396 tests green**, 15 new in -`test_branch_management.py`. Branch `sp7-tree-ui`, migration 61, no vacuum owed by it. - -**Three mockups were built before a line of it was written**, because the spec was one -paragraph and the choice was expensive: a per-node spatial map is a second windowing -problem at 600 nodes. The rail won on the grounds that it does not delay the verification -SP7 exists to do, and the map stays available as a later feature at no extra cost — both -read the same `GET /branches`. - -**Two of the three operations SP7 "just needed UI for" did not exist.** `switch` did. -`rename` had no column and no route; `delete` had no route. Migration 61 adds -`branches.name`, and the two endpoints came with it. - -**One bug class, in four places, and only a browser could have found it.** The Branches -panel, the Status drawer, Insights and the Memory Bank all refreshed on `actions.length` -— and **a branch switch does not change the length of the story, it changes which story -it is.** So the tree showed one branch while the reader was on a second, Insights showed -the prompt for the path just left, and the scoreboard kept the other line's numbers. The -server was right the whole time and no test could see any of it. All four key on -`${actions.length}:${stateKey}` now. - -`backend/tools/branch_fixture.py` was written to catch exactly this and is worth keeping: -a small bootable adventure with real stats whose **two branches have equal path length**, -which is the case a length-based key cannot distinguish. `--keep` could not have found -it. Run it, switch branches, and watch hp go 60 ↔ 95 with the drawer open. - -That is now four bugs on this frontend found by exercising it rather than by testing it, -two of them in paths that had just shipped. The pattern is not subtle any more: **this -frontend has no test runner, so anything not driven by hand is unverified.** - -**A memory is now attached to a node, always — migration 62.** Hand-written ones used to -carry a NULL depth ("belongs to the adventure, not to a path"), which is a coordinate no -fork can cap, so a note typed on one line followed you onto branches whose story it never -described. They anchor at the head now, and the `unanchored` escape clause in -`lineage.Path.clause` is deleted rather than left to rot. - -**And the drawer shows only the path being read**, filtered by the same clause retrieval -uses: the bank you can see is the bank the model can see. Nothing is stranded — a memory -lives on a branch, switching to it shows the memory, and deleting the branch deletes it. -Pinning decides *order*, the path decides *existence*. - -Migration 62 lands existing NULL-depth memories at **depth 0 of their branch**, which is -at or before every fork point, so nobody's bank loses a row on deploy. The tip would have -been the tidier-sounding choice and would have emptied them out of every branch forked -earlier than they were typed. - -An earlier pass this session shipped the other design — adventure-wide list, `on_path` -flag, *another branch* badge. Anchoring made it redundant and it was removed. Worth -knowing if the phrase turns up in an older commit message. - -**The scroll path is finally driven.** 602-action fixture, three prepends of ~16,200 px -each: the same DOM node held viewport top 792 → 787, and the view stayed 48,174 px from -the bottom. PR #2's fix holds. Measuring note worth keeping — the fixture's prose repeats, -so an anchor matched by *text* finds an older copy of the same sentence and reports a -16,000 px jump that never happened. Hold the node. - -## What happened on 2026-08-18, part three — the tree, SP6 - -The backup learned the tree. `ai-dnd-adventure-v2` carries branches, the fork point each -one left its parent at, which attempt at every turn is the story, and what each node left -behind — that last one because it is what a branch switch puts back, and a bundle that -imported a tree nobody could switch inside would be a backup of the wrong thing. The v1 -*reader* stays: those files are already on disk. **381 tests green**, 16 of them new in -`test_bundle_v2.py`. Branch `sp6-bundle-v2`, no migration, no vacuum. - -The gap SP5 left is closed — a forked adventure now has an honest export — so the -ordering constraint that has governed the last two subphases is discharged, and SP7 is -free. - -Three things to carry forward: - -- **Carry what was chosen, never what is derived.** The head branch, the fork points, the - live flags and the anchors are decisions, and they are in the file. `lineage`, the head - depth, the legacy `index` and the variant ordinals are computed from those, so they are - rebuilt on import instead. A bundle is a text file anybody can edit, and a derived field - shipped beside its source is a chance for the file to contradict itself in a way no read - reports. It also turns "is the round trip lossless?" into a testable question: every - omitted field is reconstructed, and the tests assert the reconstruction. -- **Check the shape before creating the row.** A node naming a branch the file does not - list is a 400 raised by a pure function, not a half-written adventure. The failure this - phase exists to end is a story that goes quiet, and a half-applied import is exactly - that. -- **The tree is free; the outcomes are what cost.** On the 600-action fixture the bundle - goes from 587 kB to 911 kB, and *all* of it is `state_after`/`world_state_after` at - 489 B a node — the coordinates themselves save 57.5 B a node against v1's - turn-and-variants shape. Twenty forks add 660 B. 4.3 % of the import body cap at - production's longest adventure. - -Paid for once, and worth not repeating: a new measuring script imported `app` before -`tools.stress_session`, which is what redirects `AIDND_DB_PATH` at a throwaway file. It -failed by *working* — the first run seeded a synthetic user into the local `data.db` and -printed good numbers; the second tripped over the unique email. **A harness that decides -where the database lives has to be imported before anything that reads it.** - -## What happened on 2026-08-18, part two — the tree, SP4 and SP5 - -A retry stopped rewriting a row, and a story learned to go two ways at once. - -**SP4** (branch `sp4-sibling-nodes`, **347 tests green**). Every attempt at a turn is now -its own node at the same `(branch_id, depth)`, with a `live` flag naming the one the story -tells; `app/attempts.py` owns the group. The JSON repeating group on `actions.variants` is -read one last time — by migration 60, which writes it out as the rows it always described -— and then goes unread. The state snapshots turned around with it: an action carries what -it left *behind* (`state_after` / `world_state_after`) rather than what it started from, -because attempts at one turn share a starting position and differ exactly in their -outcome. **The SP0 baseline and `test_retry_variants.py` both pass unmodified**, which SP4 -was permitted to change and did not need to. - -**SP5** (branch `sp5-fork-on-continue`, **365 tests green**). Taking the story down an -attempt the line has already moved past gives that attempt a branch of its own, forked at -the depth just before it. One row inserted, one row moved, nothing copied. Measured on a -40-turn story forked twenty times against the same story flat: 21 branches, 140 rows, an -80-action story, page load **31,652 B against 31,433 B (1.007×)**, and a branch costs -**103 B** of cached ancestry. - -Four things to carry forward: - -- **The holdback was the wrong repair, not an unnecessary one.** The plan said retry would - stop mutating rows so nothing could go stale, and that is not quite true — siblings - share a coordinate and the mark names the coordinate, so replacing what a turn says - still invalidates the memory covering it. What made `settled_story_actions` deletable is - that the *right* repair already existed: `forget_node` plus a rewind, which undo and - delete have called since SP3. Retry and a sibling switch make it too. **If a mark still - needs correcting when the story changes, correct it — do not decline to make the mark.** -- **A fork must move nothing derived, and the first cut moved it all.** Memories at the - forked coordinate were being carried onto the new branch and the cursors re-anchored. - Both wrong, for one reason: a memory describes whichever attempt was *live* there, and - that one stays on the parent. The right answer needs no code — the lineage caps the - parent one depth short of it, so it is simply out of range from the fork, and the block - is summarized again from the text this branch actually tells. **When a coordinate system - already answers a question, adding bookkeeping to answer it again is how it gets two - answers.** -- **Storage arrangements have invariants too.** A `context_snapshot` is ~163 kB of prompt - that every attempt at a turn shares — the JSON list existed to store it once. Giving - each sibling row a copy would have made retry a permanent multiplier on the biggest - column in the database. So the prompt moves with the `live` flag and a superseded - attempt keeps only its own few hundred bytes. Migrating the real 600-action fixture: - **700 rows for the same 600-turn story, prompt archive byte-identical at 0.50 MB**, and - index/page-load/turn egress unmoved at 1.8 kB / 62.7 kB / 734.8 kB. -- **`autoflush=False` is set in `database.py`**, and it bit once: `tree.fork` read the - sibling group *after* moving the node out of it, so the move had not been written and - the node was renumbered straight back into the group it had just left. Anything in this - phase that mutates rows and then queries the same rows has to order itself by hand. - -## What happened on 2026-08-18 — the tree, SP3 - -The memory bank stopped counting. `memory_cursor` and `summary_cursor` were positions in -the story — "the first twelve actions are covered" — and a position moves when an action -in front of it is deleted, so it silently starts covering one it has never read. Both are -now node anchors, `(branch_id, depth)`, through the new `app/context/cursors.py`; a memory -hangs off the node whose block it ends on; and retrieval selects through the branch -clause, so a memory made on one branch never reaches a prompt on another. **330 tests -green**, and the SP0 baseline still passes unmodified. Branch `sp3-node-cursors`. - -Three things to carry forward: - -- **Most of the work was deleting, and that was the test of the design.** The plan listed - four pieces of cursor machinery to remove and the expectation was that each would come - back in depth-shaped form. None did. `count_after(41)` is well defined with node 41 - deleted and unchanged by anything deleted in front of it, so `note_action_removed`, - `_rewind_cursors_to_index`, `position_of_index` and the every-pass clamp in - `run_post_turn` all became nothing at all. **If a mark still needs correcting when the - story changes, it is still a position.** The one thing a delete still does is withdraw - what the node *produced* — `memorybank.forget_node`, a lookup on `(branch_id, depth)` - where `prune_dangling_memories` was a scan that could only notice damage afterwards. -- **A NULL is not a small depth, and it nearly cost a feature.** A hand-written memory - summarises no node, so it has a branch and no depth; every ancestor entry in a lineage - clause is capped `depth <= fork`, and NULL fails that test. A memory somebody typed - would have disappeared at the first fork after they typed it, with no error anywhere — - just a prompt that stopped mentioning it. `Path.clause(unanchored=True)` names that case - explicitly, and actions are deliberately not given it. -- **Reading the whole ancestry for recall is free.** Retrieval cannot be windowed — that - is the point of it — so the clause names every branch in the lineage. Two 84-action - stories with 14 memories each, one flat and one forked twenty times: **1,807 B against - 1,823 B**. Twenty-two branch terms cost nothing, because the clause is not what crosses - the wire. Index (1.8 kB), page load (62.7 kB) and turn (733.8 kB) are unmoved. - -## What happened on 2026-08-17, part five — the tree, SP2 - -Every read of an action now goes through one module. `app/context/lineage.py` turns a -branch's stored lineage into the OR-of-ranges that is "this story", and history, paging, -the newest-action lookups, the index screen and the scripting history API all select -through it. Ordering moved from `index` to `depth`. **317 tests green**, and the SP0 -baseline still passes unmodified, which was the pass condition. Branch `sp2-branch-clause`. - -Three things to carry forward: - -- **A read-side invariant needs a write-side floor.** From SP2 a row without a branch is a - row no read can see, and it fails by *disappearing*. Wiring every writer was not enough, - because the SP0 baseline and eleven other fixtures write actions straight to the database - and never call `place_action` — and the baseline may not be edited. `tree.place_new_nodes` - now runs from `Session.before_flush`, so nothing can be written unplaced. That is a - better invariant than the one SP1 shipped, and it was the contract that forced it. -- **The SQLAlchemy identity map is weak, and that is a performance cliff.** Resolving the - head branch once per node re-read the row from the database for every node in a flush — - 201 SELECTs to write 200 actions, and a 25 % slower suite (36 s → 45 s, back to back). - Nothing about the results changed; only a stopwatch could see it. Hoist the lookup out - of the loop and hold the reference for the length of the call: 2 SELECTs, and the suite - back within noise of SP1. Pinned by a test that counts the reads rather than the - seconds — this machine's timings drift ~20 % between runs. -- **Clause count is bounded by the window, and it is now measured.** A story forked 20 - times reads its newest 32 actions naming *one* branch, for 1.07× what an unforked story - of the same length costs. Reading the tail widens the lineage only when a deleted action - leaves the estimate short. - -The 600-action `--keep` fixture — a genuine pre-tree database — was migrated and then -driven over HTTP: index 1,840 B, page load 64,149 B (both unchanged), and scrolling to the -start took 9 pages and saw every action exactly once. - -## What happened on 2026-08-17, part four — the tree, SP0 and SP1 - -No behaviour change, and none intended: a linear story is a tree with one branch, so -every adventure reads exactly as it did. **297 tests green** (259 before the phase -started, 283 with SP0's contract, 297 with SP1's migration tests). - -**SP0** built `tests/test_story_tree_baseline.py` — 24 tests driving the product over -HTTP, asserting only on API responses — and `tools.stress_session --rich`, a correctness -fixture beside the scale one. Both on branch `phase-14-story-tree`. - -**SP1** put the tree in the schema, on branch `sp1-tree-schema`: a `branches` table, -`branch_id`/`depth` on actions and memories, `head_branch_id`/`head_depth` on adventures, -migrations 46–52 with a server-side backfill, and `app/tree.py` for the write side. -Nothing reads any of it yet. Four things worth not rediscovering: - -- **A schema needs its writer in the same subphase.** No migration will ever visit a row - written after it ran, so columns backfilled today and populated-on-write next week leave - a hole exactly the width of one deploy. `app/tree.py` stamps every new node, including - the ones `seed_demo.py` and the stress fixture write — a fixture built by `create_all` - is stamped LATEST and no migration ever touches it. -- **SQLite will not drop a column a foreign key names.** Two tests simulated an old - database by rewinding the *stamp* while `create_all` left the new columns in place; that - works until the next `ADD COLUMN` lands, and then it fails on a duplicate column. Every - `ADD COLUMN` migration is now idempotent (`migrations._column_already_there`), which is - the `IF NOT EXISTS` SQLite has no syntax for. A true pre-migration fixture has to drop - and rebuild the tables from frozen DDL, which is what `test_tree_migration.py` does. -- **Two mutually-referencing tables cannot both carry the foreign key.** `create_all` - refuses to order the cycle, and its escape hatch (`use_alter`) needs an ALTER SQLite - does not have. `adventures.head_branch_id` is a plain integer and a documented cache. -- **The suite went 20 s → 38 s, and it is not the app.** One new table plus one index adds - ~47 ms to a `create_all`/`drop_all` pair on SQLite (DDL fsync), and nearly every test - does one. Measured, not guessed. Egress is unmoved: `branches` is 0.1 kB of a 733.5 kB - turn, and the page-load and index shapes are byte-identical to the numbers above. - -## What happened on 2026-08-17, part three - -No behaviour change. A way to get a long adventure in front of a browser, because the -one open gap needed a subject and there wasn't one. - -**`tools.stress_session --keep PATH`.** The harness already built a production-shaped -600-action adventure and then threw it away with the temp file; `--keep` writes it -somewhere durable and makes the app able to serve it. Two edits are needed for that, and -both are the kind of thing that costs an hour to rediscover: - -- **`create_all()` does not stamp the schema version.** `bootstrap()` reads a - populated-but-unstamped database as ancient and replays every migration against a - schema that already has the columns. `--keep` stamps `PRAGMA user_version` to - `LATEST_VERSION`. -- **The fixture's user is a registered one.** In local mode `get_current_user()` looks - for the row with `email IS NULL` and `is_guest` false, so without clearing the email - the app opens on an empty library and nothing owns the 600 actions. - -`--keep` is read before argparse exists (`_early_keep`), because where the database -lives has to be settled before `app.database` is imported — the same constraint the -`AIDND_STRESS_DATABASE_URL` block at the top of the module already lives under. SQLite -only; combining it with a Postgres target is rejected rather than half-honoured. - -Verified: the fixture boots with no manual step, `action_count` 600, the first payload -carries 60 actions, and `before_id` walks back through 9 more pages to the start — 600 -seen, `has_more` false at the end. 259 tests pass. - -**Snapshots can be shrunk for this.** `--snapshot-bytes 2000` keeps the file at ~2.5 MB -instead of ~140 MB. `context_snapshot` is deferred and never reaches the browser, so it -changes nothing about what scrolling exercises — but do not shrink it when *measuring*, -where it is most of the point. - -**Port 8010, not 8000.** Covered below, and now printed by `--keep` itself. - ---- - -## What happened on 2026-08-16 - -Four commits, all on the egress work that has to land before the tree. - -### 1. A byte meter, in the repo this time — `7ee5cee` - -`backend/tools/dbmeter.py` + `backend/tools/stress_session.py`. - -``` -cd backend -.venv/Scripts/python.exe -m tools.stress_session -.venv/Scripts/python.exe -m tools.stress_session --shapes turn --repeat 2 -.venv/Scripts/python.exe -m tools.stress_session --no-embeddings # the old blind spot -``` - -It counts **bytes at the DBAPI cursor**, not queries — both egress blowouts this project -has had were one query fetching a column nobody read, and a statement count showed -nothing wrong in either. It drives a production-shaped synthetic adventure through the -real routes with only the LLM and the embedding endpoint faked. - -**The memory bank is ON by default and that is the whole point.** The previous harness -ran without an embedding model configured; embedding providers are BYOK-only by -construction, so `retrieve_memories` returned early every time and the heaviest read in -a turn never happened. `--no-embeddings` reproduces that deliberately — the gap is 29x. - -It calibrates against the two figures measured directly on production: 426.7 kB for a -200-action page load against 423 KB, and 3,258.7 kB for one turn against 3,153 kB. -It runs on SQLite, so treat absolutes as production-*shaped* and compare before/after. - -### 2. Packed float32 embeddings — `c568648` - -Migration 38 + `app/vectors.py`. A 1536-dimension vector as a JSON list is ~31 KB; the -same numbers as float32 are 6,144 bytes. **Not a precision trade** — the endpoints -compute in float32 and render that into JSON, so converting back is bit-exact. Nothing -re-embeds, no API calls. - -The backfill is the one in `migrations.py` that cannot be portable SQL, so it comes -through Python, batched. Migration SQL can now be a `{dialect: sql}` map (BLOB vs BYTEA -have no common spelling). - -### 3. Ranking the bank without reading the bank — `b7e53ae` - -`retrieve_memories` walked `adventure.memories`, loading every row *with its vector*. It -now asks SQL which memories are in play (an id and a flag per row), ranks against -vectors held in process, and fetches text only for the top-K it picks. - -Two more callers were doing the same thing, and the production SQL could not see either: -`_evict_over_capacity` walked the bank to count it, `_embed_pending` walked it to find -rows with no vector. A played turn cost **6.4 MB**, not the 3.2 the plan assumed. - -| shape | before | cold | warm | -|---|---|---|---| -| one turn | 3,258.7 kB | 723.4 kB | **122.3 kB** | -| `run_post_turn` | 3,139.1 kB | 0.7 kB | 0.7 kB | -| Insights | 3,223.7 kB | 117.9 kB | 117.9 kB | -| Memories drawer | ~3.1 MB | 23.7 kB | 23.7 kB | - -A played turn is turn + post-turn: **6.4 MB → 123 kB**, 52x. - -Migrations 39/40 add `memories.embedded`, migration 41 drops the capacity default -200 → 80 for rows still on the old default. - ---- - -## What happened on 2026-08-17, part two - -Everything left open in `plan/13` closed, plus two bugs that fell out of doing it. - -| shape | before today | after | -|---|---|---| -| page load, 600 actions | 606.0 kB | **62.6 kB** — and no longer grows with the story | -| adventures index, 6 fat adventures | 469.7 kB | **0.3 kB** | -| `context_snapshot` on disk | ~89 MB | 47.5 MB measured (3.2x) | -| database total | 99.6 MB | **65.0 MB measured**, after the second vacuum | - -**One follow-up after the merge** (PR #2). Prepending older actions changes `actions`, -and the bottom-pinning effect watches `actions` — so unless a scroll had already -un-pinned the view, loading earlier turns jumped to the newest one instead. A prepend -now clears the pin explicitly. Found by re-reading the path, not by running it. - -**The story is a window now.** `GET /adventures/{id}` returns the newest 60 actions and -`action_count`; older pages come from `GET /{id}/actions?before_id=`. Anchored on an -action, never an offset — an offset counted back from the newest shifts every older -position the moment a turn lands, which is exactly when someone is scrolling. `Play.jsx` -prepends and restores scroll position in a `useLayoutEffect`, before paint. - -**`context_snapshot` is compressed** (migrations 43–45, `app/compression.py`) via a -TypeDecorator, so every call site still reads and writes a dict. Verified end to end on -a throwaway Neon database: 720,864 B of JSON to 204,293 B of bytea, every row equal. - -**The JSON vector column is gone** (migration 42) — and dropping it exposed that -changing your embedding model had silently stopped re-embedding the bank since -migration 38. The settings route cleared the dead column and left `embedded` true, so -`_embed_pending` never saw those rows and retrieval kept ranking against the old -model's vectors. Nothing reported it: `cosine` returns 0.0 on a width mismatch. -`tests/test_embedding_model_switch.py`. - -**Byte ceilings exist** (`tests/test_egress.py`), including one test whose only job is -to prove the ceilings would catch something. - -**List responses name their columns.** The index was loading whole Adventure entities — -seven text and JSON columns, ~15 kB a row — to render a title and a snippet. - -## What happened on 2026-08-17, part one - -No new behaviour — a verification pass on what shipped the day before, because every -number in the section above had been measured on SQLite against a synthetic fixture. -Full write-up in `plan/13` under "Verified on production". - -**It holds.** `schema_version` is 41 on the live Postgres with `embedding_blob bytea` -and `embedded boolean` present, so migration 38's dialect map is correct against a real -server. The backfill is complete (134/134). The packed vectors are **5.04x** smaller -than the JSON on real data — 30,971 → 6,144 bytes a memory, as predicted. - -**SQLite was not lying.** `tools.stress_session` can now target Postgres via -`AIDND_STRESS_DATABASE_URL`, and every shape agrees within 0.5% — the warm turn is -121.1 kB on Postgres against 122.3 kB on SQLite, with `memories` down to 1.7 kB of it. -Run it against a **throwaway** database only; the harness writes, so it refuses any -target whose name does not contain `stress` or `scratch`. - -**Two corrections came out of it**, both above: the page-load model has the wrong -shape (too heavy per action, far too short), and the storage ceiling was never costed. - -## Things worth remembering - -**The vector cache needs no invalidation callbacks, and that is why it is safe.** A -stored vector can only change through `memorybank.set_vector`, which drops that one -entry. Anything that *removes* a memory from play — eviction, deletion, pruning, an edit -clearing the vector — falls out of the catalogue query, and entries missing from the -catalogue are dropped on the next read. So there is no hook anyone can forget to call. -It is in-process and assumes one worker, which is what the deploy runs. - -**Weigh new columns in bytes, not rows.** The comment on `Memory.embedding` said "fine -at bank sizes of a few hundred" and was wrong by the only measure that mattered: a few -hundred JSON vectors is ten megabytes, fetched fresh every turn. - -**A deferred column needs a cheap flag beside it.** `memories.embedded` exists because -once the vector is deferred, every "is this embedded?" check becomes a 6 KB lazy load, -once per row down the Memories drawer. Exactly the same shape as `actions.variant_count` -beside `actions.variants`. Expect to need this for any future heavy column. - -**Any egress measurement must run with an embedding model set.** This is the second -time that omission has hidden the biggest number in the room. - -**Production has real users on it now. Measure it without reading it.** Counts, -`sum(octet_length(...))` and `pg_total_relation_size` answer every sizing question -asked so far, and none of them return anyone's story, memory text or email. When a -real Postgres is needed for a *write* path, create a throwaway database beside the real -one and drop it after — never point a harness at the production database. - -**`octet_length` is the egress number, not the on-disk number.** Postgres TOAST -compresses big JSON — `context_snapshot` is 150.8 MB uncompressed but ~89 MB stored — -and decompresses before sending. Size reads with `octet_length`, size the storage bill -with `pg_total_relation_size`, and do not mix them up. - ---- - -## `plan/13` is closed - -All six of its open items landed on 2026-08-17, and are live. What is left is not from -that plan: - -- **Nothing has ever exercised the scroll in a browser** — still true, but there is now - something to exercise it *on*: `--keep` builds a 600-action adventure the app will - serve (see 2026-08-17 part three). The subject is no longer the excuse; only the - looking is left. This is the one real gap, and - it has already cost something: re-reading that path after shipping turned up a bug - where loading earlier turns scrolled *past* them to the end of the story, worst on - the short-window case the button exists for (fixed, PR #2). One bug found by reading - means reading is not a substitute. Either scroll a long adventure by hand, or add a - vitest + jsdom harness — that would have caught this one. jsdom has no layout, so the - scroll-position arithmetic still needs eyes. -- **`ACTION_PAGE = 60` is a guess.** It should be a page or two of reading. If loading - older turns feels like it interrupts, that is the number to move - (`routers/adventures.py`). One data point: at 600 actions it takes the window plus - **nine** more pages to reach the start, which is a lot of button presses for anyone - going back to the beginning. -- ~~Post-vacuum sizes unmeasured~~ — measured 2026-08-17, and the vacuum that mattered - was run then too. 65.0 MB. See the top of this file. -- **Anyone who switched embedding models has a stale bank.** The bug is fixed, but - those memories only re-embed as the post-turn pass reaches them, which costs an - embedding call each. Nothing forces it; playing does. - -Deliberately not taken: moving `context_snapshot` out of the database entirely -(compressing it bought the same runway for a much smaller change), and pgvector (breaks -the SQLite dev parity this codebase protects on purpose). - ---- - -## Running things - -``` -cd backend -.venv/Scripts/python.exe -m pytest tests/ # 549 tests (~180s) -.venv/Scripts/python.exe -m tools.stress_session # egress report (SQLite) - -# Same harness against a real Postgres. The target must be a THROWAWAY database -# — this writes a synthetic adventure, and it refuses any name without -# 'stress'/'scratch' in it. -AIDND_STRESS_DATABASE_URL=postgresql://…/stress_scratch \ - .venv/Scripts/python.exe -m tools.stress_session -``` - -**A long adventure to scroll**, instead of a temp file the report discards. The -snapshots are shrunk because they never reach the browser — 2.5 MB rather than 140 MB — -and `--shapes list` skips the measurement work the fixture does not need: - -``` -cd backend -.venv/Scripts/python.exe -m tools.stress_session \ - --keep ./scroll_fixture.db --snapshot-bytes 2000 --shapes list - -AIDND_DB_PATH=$PWD/scroll_fixture.db \ - .venv/Scripts/python.exe -m uvicorn app.main:app --port 8010 -cd ../frontend && AIDND_API_PORT=8010 npm run dev # → localhost:5173 -``` - -Everything in it is synthetic and no real adventure is read. `*.db` is gitignored, so -the fixture never lands in a commit. - -**A fixture to check correctness against, rather than bytes.** The measuring fixture -leaves every column it does not weigh at its default, which turns out to be exactly the -set a story tree has to migrate — the per-action state snapshots identical on all 600 -rows, no RPG scenario, no adventure scripts, both cursors 0, and retry attempts whose -text is byte-identical with the first always live. `--rich` fills in those and only -those: - -``` -cd backend -.venv/Scripts/python.exe -m tools.stress_session --rich --actions 30 --memories 12 -``` - -Prefer it small — it exists for variety per row, not for rows. **Its byte figures are -not comparable to a plain run**, and it does not replace the scale fixture, which still -holds the egress ceilings. - -On Windows the report's box-drawing characters crash the default cp1252 console; -prefix with `PYTHONIOENCODING=utf-8`. - -**A real model, without an API key.** `tools/claude_shim.py` serves an OpenAI-compatible -endpoint backed by the local `claude` command line tool. Point Settings at -`http://127.0.0.1:8787/v1`, put any string in the API key field, and pick `sonnet`. Set -the reasoning budget to `0` or `-1`: a positive budget sends `reasoning.max_tokens`, -which Claude 5 models reject with a 400. It costs about $0.04 a turn against the -subscription, and it does not serve embeddings. - -``` -cd backend -.venv/Scripts/python.exe tools/claude_shim.py # 127.0.0.1:8787 -``` - -**To exercise the guest path**, which is off in single-user mode: - -``` -cd backend -AIDND_MULTI_USER=1 AIDND_SECRET_KEY=throwaway AIDND_DB_PATH=$PWD/guest_scratch.db .venv/Scripts/python.exe -m uvicorn app.main:app --port 8001 -curl -c jar.txt http://127.0.0.1:8001/api/auth/me # mints a guest and its starter -``` - -Port 8000 is shared with the job-pipeline app, which will squat it and silently shadow -the AI-DnD API — free it before running the backend, or move the vite proxy with -`AIDND_API_PORT`, which is what the `--keep` recipe above does. diff --git a/planning/BUILD-MILESTONES.md b/planning/BUILD-MILESTONES.md index 0ec3b11..5067506 100644 --- a/planning/BUILD-MILESTONES.md +++ b/planning/BUILD-MILESTONES.md @@ -97,8 +97,8 @@ A clean production build can start, open the browser UI, generate and persist st ## Status: COMPLETE -Accepted 2026-09-02. Evidence: `planning/reports/M1-BASELINE-REPORT.md` (run -logs and packet captures) and `planning/reports/M1-IMPLEMENTATION-REPORT.md` +Accepted 2026-09-02. Evidence: `planning/archive/milestone-reports/M1-BASELINE-REPORT.md` (run +logs and packet captures) and `planning/archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md` (review report). A01-A06, H01-H03 and H11 all pass on runtime evidence; 648 backend tests pass, including with no route to the Internet. @@ -168,8 +168,8 @@ The codebase has a narrow single-user/local-only surface and the inherited story ## Status: COMPLETE -Accepted 2026-09-02. Evidence: `planning/reports/M2-BASELINE-REPORT.md` -(measurements) and `planning/reports/M2-IMPLEMENTATION-REPORT.md` (review); +Accepted 2026-09-02. Evidence: `planning/archive/milestone-reports/M2-BASELINE-REPORT.md` +(measurements) and `planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md` (review); verdict *accept with non-blocking debt, proceed to M3*. Implementation is commit `8c65ae9`, and the three defects the review found are commit `8652fe7` — see the closeout note appended to both reports. @@ -193,9 +193,10 @@ commit `8c65ae9`, and the three defects the review found are commit `8652fe7` **Debt carried forward, none of it blocking M3:** `Settings.model` still defaults to `""` with nothing prompting for it (M8); inert legacy tables and columns await a cleanup migration once the schema settles, after M3/M5; there -are still no frontend tests (M8); `docs/*.html`, upstream's project site and not -served by the app, still links Google Fonts. Full table in the implementation -report §P. +are still no frontend tests (M8). `docs/*.html`, upstream's project site, was +listed here as still linking Google Fonts; the whole inherited `docs/` tree was +deleted in the 2026-09-03 documentation pass, which closes that item. Full table +in the implementation report §P. --- @@ -434,7 +435,7 @@ undo semantics and are expected to be rewritten by M3; that is separate from this instrumentation point, and they should likewise be rewritten rather than dropped. -Evidence: `planning/reports/M2-IMPLEMENTATION-REPORT.md` §K.2, §Q. +Evidence: `planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md` §K.2, §Q. ## Note from M3 — three constraints this milestone must satisfy @@ -529,7 +530,7 @@ M6 therefore additionally requires: degraded the storyteller quietly and left the transcript correct, which is the right failure direction — but it must also be a *visible* one. -Evidence: `planning/reports/M2-IMPLEMENTATION-REPORT.md` §A.1, §9.1. +Evidence: `planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md` §A.1, §9.1. --- diff --git a/planning/CODEX-HANDOFF-NOTE.md b/planning/CODEX-HANDOFF-NOTE.md deleted file mode 100644 index 323a615..0000000 --- a/planning/CODEX-HANDOFF-NOTE.md +++ /dev/null @@ -1,14 +0,0 @@ -# Codex Handoff Note - -**Status:** Phase 0B handoff complete; historical only. - -The earlier Phase 0B execution prompts are retained for audit/history: - -- `PHASE-0B-CODEX-BRIEF.md` -- `PHASE-0B-CODEX-HANDOFF.md` - -Do **not** execute either as the next production task. - -Phase 0B has been completed and the resulting recommendation reviewed. AI-DnD is now the selected production base, and the planning package has been revised accordingly. - -The next production Codex prompt has **not** been prepared. It should be created only after the current planning-package revision is reviewed and approved. When authorized, the first implementation prompt should be derived from Production Milestone M1 in `BUILD-MILESTONES.md`, not from the Phase 0B validation briefs. diff --git a/planning/DECISIONS/002-ollama-only-v1.md b/planning/DECISIONS/002-ollama-only-v1.md index fe8b8e5..9a9cd4c 100644 --- a/planning/DECISIONS/002-ollama-only-v1.md +++ b/planning/DECISIONS/002-ollama-only-v1.md @@ -57,4 +57,4 @@ Therefore: `http://…:11434`. M1 implemented this (`backend/app/tlstrust.py`); see -`planning/reports/M1-IMPLEMENTATION-REPORT.md` §G. +`planning/archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md` §G. diff --git a/planning/DECISIONS/011-local-inference-endpoint-policy.md b/planning/DECISIONS/011-local-inference-endpoint-policy.md index 940c349..154767f 100644 --- a/planning/DECISIONS/011-local-inference-endpoint-policy.md +++ b/planning/DECISIONS/011-local-inference-endpoint-policy.md @@ -115,4 +115,4 @@ itself beyond loopback, which remains out of scope for v1. - `SECURITY-THREAT-MODEL.md` §10A, §71A item 5, §77 - `TECHNICAL-DESIGN.md` §5.1 item 4, §5.2 - ADR 002 (Ollama-only, and the TLS consequence), ADR 004 (local-only production) -- `planning/reports/M2-IMPLEMENTATION-REPORT.md` §F, §K.1 +- `planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md` §F, §K.1 diff --git a/planning/PHASE-0B-CODEX-BRIEF.md b/planning/PHASE-0B-CODEX-BRIEF.md deleted file mode 100644 index e382385..0000000 --- a/planning/PHASE-0B-CODEX-BRIEF.md +++ /dev/null @@ -1,272 +0,0 @@ -# Phase 0B — Codex Initial Validation Brief - -**Status:** COMPLETE / HISTORICAL — do not execute as a production prompt -**Purpose:** Run a focused first round of local validation on the three finalist repositories and return a recommendation based on what was actually learned. - -## 1. Goal - -We are **not** asking you to build the production application yet. - -The goal of this round is to answer one question: - -> Which existing project is the best starting point for the local interactive-story application, and what important technical facts did we learn that should affect the next design step? - -The three finalists are: - -1. AI-DnD - https://github.com/parththakkar106/AI-DnD - -2. Open Dungeon - https://github.com/newideas99/open-dungeon - -3. ai-adventure - https://github.com/CaoRuiming/ai-adventure - -## 2. Important Product Requirements - -Use these as the main evaluation criteria. - -The eventual application should be: - -- browser-first, -- local-only in v1, -- based on local Ollama inference, -- single-user, -- genre-agnostic, -- persistent across restarts, -- able to retain authoritative story state separately from model prose, -- able to Undo/Redo/Retry safely, -- able to preserve named save points/checkpoints, -- able to retain abandoned history without immediately deleting it, -- able to prevent abandoned-history facts/memories from leaking into the active story, -- able to support long-term context/memory, -- able to import local knowledge, -- able to inspect what context was sent to the model, -- architecturally compatible with future local image/video/TTS/STT support. - -Do not try to implement all of these now. - -This round is about determining which candidate already gives us the strongest foundation. - -## 3. Read Only What You Need - -Start with: - -1. `README.md` -2. `SPECIFICATION.md` -3. `reports/PRELIMINARY-RECOMMENDATION.md` -4. `reports/REUSE-MATRIX.md` - -Then use these only when relevant to a specific experiment: - -- `STORY-BRANCH-SEMANTICS.md` -- `CONTEXT-AND-MEMORY.md` -- `SECURITY-THREAT-MODEL.md` -- `TEST-CAMPAIGN-FIXTURE.md` - -Do **not** read every planning document up front unless needed. - -## 4. Baseline Work for Each Candidate - -For each repository: - -1. Clone it cleanly. -2. Record the exact commit SHA. -3. Follow the documented install instructions. -4. Run the existing tests. -5. Build/start the application. -6. Confirm the basic local story flow. -7. Record: - - test results, - - storage/database technology, - - model/provider assumptions, - - local ports, - - major runtime failures, - - obvious cloud/hosted dependencies. - -Do not spend excessive time fixing unrelated upstream problems. - -If a project does not run cleanly, document why and continue. - -## 5. Focused Experiment A — AI-DnD - -We want to know whether AI-DnD can realistically serve as the production base. - -Test: - -- Can it run with Ollama locally? -- Can its story-tree/history system support simple user-facing Undo/Redo/Retry behavior? -- Does state rollback work independently of heavy RPG/stat mechanics? -- Can RPG-specific state be left empty/minimal without breaking the useful history/state architecture? -- Can QuickJS/scripting and hosted/cloud-oriented features be disabled without breaking local story operation? -- Can local memory/embedding behavior work without cloud services? -- Do memories/state respect the active history path? -- Is its context/Insights system useful for showing what was sent to the model? - -Use a small disposable experiment if necessary. - -Do **not** start stripping the whole application down. - -## 6. Focused Experiment B — Open Dungeon - -We want to know how expensive it would be to fix its history model. - -Test: - -- Confirm how Retry/Edit/Erase affect stored history. -- Identify whether old future turns are deleted. -- Trace which parts of the application depend on that linear/destructive behavior. -- Estimate how invasive it would be to change to: - - parent-linked turns, - - active head, - - retained abandoned history, - - named checkpoints, - - lineage-safe summaries/state. - -Do not implement the complete branch system. - -Also record useful existing pieces: -- browser UX, -- local image generation, -- character visual continuity, -- any scene/media architecture worth reusing. - -## 7. Focused Experiment C — ai-adventure - -We want to know whether its strong state/privacy architecture can realistically become a browser-based Ollama application. - -Test: - -- Run the existing tests. -- Confirm Undo/branch/checkpoint/replay behavior. -- Identify the provider abstraction. -- Prove one local Ollama-backed story turn using the smallest practical adapter. -- Determine how tightly the core application logic is coupled to the CLI. -- Assess whether the core could sit behind a browser/API layer without moving authoritative state logic. -- Review its local lore/FTS approach for possible reuse. - -Do not build a browser frontend. - -## 8. Offline / Privacy Check - -For each candidate, once dependencies/models are installed: - -- run it with outbound Internet unavailable or blocked where practical, -- exercise basic story generation, -- note any unexpected network attempts. - -We do not need a full penetration test in this round. - -We do need to know: - -- whether local story use truly works offline, -- whether cloud services are required, -- whether analytics/telemetry/remote assets are present, -- how difficult those paths would be to remove. - -## 9. Use the Standard Fixture Selectively - -Use `TEST-CAMPAIGN-FIXTURE.md` where it helps answer continuity questions. - -You do not need to execute the entire fixture against every candidate. - -The most important checks are: - -- possession/state consistency, -- restore/undo behavior, -- abandoned-path isolation, -- whether an old discarded fact can leak into current memory/context. - -## 10. What Not to Do - -Do not: - -- build the production fork, -- merge repositories, -- redesign the full UI, -- implement full RAG, -- implement complete branching in Open Dungeon, -- remove all RPG code from AI-DnD, -- build a browser frontend for ai-adventure, -- add image/video/TTS/STT features, -- write the final production milestone plan. - -Small disposable code changes are allowed only when needed to answer the evaluation questions. - -## 11. Final Deliverable - -The main output from this round should be a single recommendation document: - -```text -PHASE-0B-RECOMMENDATION.md -``` - -It should summarize what was learned, not just list test logs. - -Include: - -### A. Executive Recommendation - -- Which repository should be the production base? -- Confidence level: high / medium / low. -- Did the initial AI-DnD recommendation hold up? - -### B. What We Learned About Each Candidate - -For each: -- what worked, -- what failed, -- strongest reusable pieces, -- major architectural problems, -- likely amount/type of adaptation required. - -### C. Key Technical Findings - -Especially: -- history/undo model, -- state rollback, -- memory isolation, -- local Ollama support, -- offline/privacy behavior, -- browser suitability, -- imported-knowledge potential, -- future media extension potential. - -### D. Important Surprises - -Anything that contradicts the current planning assumptions. - -### E. Recommendation for Next Step - -Do **not** perform the next step. - -Instead recommend what should happen next, such as: -- fork AI-DnD and begin a controlled strip-down, -- perform one additional experiment first, -- reconsider Open Dungeon, -- use ai-adventure as the base instead, -- revise one of the product assumptions. - -### F. Open Questions - -List anything that could not be resolved in this round. - -## 12. Supporting Evidence - -You may also create concise supporting notes/logs for: - -- baseline results, -- AI-DnD experiment, -- Open Dungeon history analysis, -- ai-adventure Ollama adapter, -- offline/network observations. - -Keep them concise. - -The recommendation document is the primary deliverable. - -## 13. Stop Condition - -When `PHASE-0B-RECOMMENDATION.md` is complete, stop. - -We will take the findings back into the design discussion, re-examine the assumptions, and decide the next step before any production implementation begins. diff --git a/planning/PHASE-0B-CODEX-HANDOFF.md b/planning/PHASE-0B-CODEX-HANDOFF.md deleted file mode 100644 index f4d7818..0000000 --- a/planning/PHASE-0B-CODEX-HANDOFF.md +++ /dev/null @@ -1,277 +0,0 @@ -# Phase 0B — Codex Local Validation Handoff - -**Status:** COMPLETE / HISTORICAL — do not execute as a production prompt -**Purpose:** Validate the Phase 0A recommendation using local builds, tests, offline runtime observation, and tightly scoped experiments. -**Stop rule:** Do not begin production implementation. - -## 1. Read Before Starting - -Read the package in the order listed in `README.md`. - -At minimum, before modifying any finalist, read: - -1. `SPECIFICATION.md` -2. `DATA-MODEL.md` -3. `STORY-BRANCH-SEMANTICS.md` -4. `CONTEXT-AND-MEMORY.md` -5. `IMPORTED-KNOWLEDGE-DESIGN.md` -6. `SECURITY-THREAT-MODEL.md` -7. `MEDIA-EXTENSION-CONTRACT.md` -8. `BROWSER-UX-SPEC.md` -9. `TEST-CAMPAIGN-FIXTURE.md` -10. `V1-ACCEPTANCE-TESTS.md` -11. `reports/PRELIMINARY-RECOMMENDATION.md` -12. `reports/REUSE-MATRIX.md` - -Treat the detailed behavioral documents and acceptance tests as the target behavior. Treat `TECHNICAL-DESIGN.md` as provisional. - -## 2. Finalists to Clone - -Clone only these three primary finalists for Phase 0B: - -1. https://github.com/parththakkar106/AI-DnD -2. https://github.com/newideas99/open-dungeon -3. https://github.com/CaoRuiming/ai-adventure - -At clone time record exact commit SHA, branch/tag, date, license, dependency lockfiles, required runtimes, and documented local model/provider assumptions. - -Keep each upstream clone clean. Use separate experiment branches/worktrees for disposable changes. Do not merge candidate repositories together. - -## 3. Result Codes - -Use consistently: - -```text -PASS -PARTIAL -FAIL -NOT IMPLEMENTED -NOT APPLICABLE -``` - -Do not convert an untested requirement into a PASS. - -## 4. Validation V0 — Environment and Baseline - -For all three: - -- install from documented instructions, -- run existing test suite, -- run build/lint/typecheck where applicable, -- record failures, -- record actual current test count, -- record local data paths, -- record listening ports, -- record child processes/services, -- record model/provider configuration, -- record database/storage technology. - -Deliver one baseline report per project. Do not rely on README claims for test counts or feature behavior. - -## 5. Validation V1 — Offline and Network Behavior - -After dependencies and local models are already installed, block outbound Internet and exercise launch, story creation, 5+ turns, restart/resume, summaries, memory/embeddings if present, Retry, Undo/rewind, checkpoint/branch features if present, `.txt`/`.md` import if present, and Open Dungeon local image generation if configured. - -Capture open sockets, DNS attempts, HTTP(S)/WebSocket destinations, and which feature caused each request. - -Use `SECURITY-THREAT-MODEL.md` and acceptance groups A, G, and H. - -Pass condition for target v1 operation: - -> Story content, imported knowledge, prompt/context data, and media prompts do not leave loopback or explicitly approved local endpoints. - -## 6. Standard Fixture Use - -Use `TEST-CAMPAIGN-FIXTURE.md` as the standard narrative test bed. - -Where a finalist cannot represent the fixture directly, map it as closely as possible, document the mismatch, and do not silently change expected truth/state to suit the candidate. - -Important checks: - -- Mara's knowledge boundaries, -- Silver Key ownership, -- resurrection canon, -- Canon vs Reference vs Inspiration authority, -- Path A secret followed by restore/divergence into Path B, -- abandoned-history memory isolation, -- long-term memory plant, -- checkpoint persistence, -- science-fiction variant. - -## 7. Acceptance-Test Mapping - -Use `V1-ACCEPTANCE-TESTS.md` as the common comparison contract. Produce a gap matrix rather than forcing each candidate to fully pass v1. - -Use these interpretations: - -```text -Already passes -Passes with configuration -Small adaptation -Foundational redesign -Not present -``` - -Prioritize high-risk groups: - -- A01-A05 — local operation/persistence -- C01-C05 — canon/state -- D01-D14 — Undo/Redo/Retry/checkpoints -- E01-E04 — lineage safety -- F01-F08 — memory/context -- G01-G10 — imported knowledge where supported -- H01-H10 — security/privacy -- J01-J03 — genre independence -- K01-K04 — media readiness -- L01-L04 — data integrity - -Long-run M01-M04 need not be fully executed against every candidate if disproportionate; identify production risk and existing test coverage instead. - -## 8. Experiment V2 — AI-DnD Strip-Down Feasibility - -Do not redesign the application. - -Answer: - -1. Can a scenario run with RPG stats absent, empty, or minimal? -2. Do branch/retry/undo/tree tests operate independently of RPG mechanics? -3. Can user-facing tree complexity be hidden behind `STORY-BRANCH-SEMANTICS.md`? -4. Disable QuickJS scripting. What breaks? -5. Disable/remove hosted multi-user/auth/demo/analytics paths. What breaks locally? -6. Configure only local Ollama generation. -7. Configure only local embeddings, preferably Ollama/local. -8. Verify branch switching restores correct generic state. -9. Verify memory retrieval respects active lineage. -10. Test whether abandoned Path A facts leak into Path B. -11. Map Story Cards/world-info to Canon / Reference / Inspiration. -12. Determine whether prompt/context snapshots satisfy Context Inspector requirements. -13. Determine whether visual/scene snapshot data can be added without RPG coupling. -14. Inventory code coupled to RPG worldstate, scripting, hosted auth, analytics, remote providers, and AI Dungeon compatibility. - -Estimate invasiveness by affected files/modules, not hours. Do not merge the experiment. - -## 9. Experiment V3 — Open Dungeon Branch Retrofit Impact - -Do not implement full branching. - -Trace message CRUD, Retry, Erase, Edit, Continue, summary generation, state/character persistence, image association, and visual continuity. Confirm destructive-tail assumptions. - -Design a minimal hypothetical persistence change supporting: - -```text -turn/node ID -parent turn ID -active head -alternate narrator takes -retained disposable history -checkpoint pointer -lineage-safe summaries/memories -``` - -Use the standard fixture to reason through Undo/restore, Path A -> Path B divergence, stale summary/state risks, and image attachment after divergence. - -Also inspect local image/provider patterns for reuse. Measure invasiveness; do not build the branch system. - -## 10. Experiment V4 — ai-adventure Ollama / Service Boundary - -1. Run existing tests unchanged. -2. Identify provider interface. -3. Prove one Ollama-backed turn using the smallest disposable adapter possible. -4. Identify modules that know about the CLI. -5. Determine whether the app/state layer can be wrapped by a browser/API service without moving authoritative logic. -6. Verify undo, branch, checkpoint, restore, replay. -7. Evaluate event/commit discipline for reuse. -8. Evaluate its FTS/lore system against `IMPORTED-KNOWLEDGE-DESIGN.md`. -9. Determine difficulty of adding semantic local retrieval while preserving lexical retrieval. -10. Check privacy boundary with the Ollama adapter. - -Do not build a browser UI. - -## 11. Validation V5 — Test Quality - -For each finalist report actual test count, categories, branch/rollback coverage, state reconstruction, migrations, summary/memory coverage, provider mocks, offline/network tests, browser tests, security tests, flaky/failing tests, and tests requiring Internet. - -Highlight which high-risk acceptance requirements already have regression coverage. - -## 12. Validation V6 — Imported Knowledge Gap Analysis - -Against `IMPORTED-KNOWLEDGE-DESIGN.md`, report local `.txt`/`.md` ingestion, classifications, campaign isolation, provenance, lexical/semantic search, embedding provider, enable/disable, deletion, export/import, hidden canon, prompt-injection framing, and remote URL/image behavior. - -Do not implement a full new RAG subsystem during Phase 0B. - -## 13. Validation V7 — Browser UX Gap Analysis - -Against `BROWSER-UX-SPEC.md`, report story reading/input quality, streaming, Undo/Redo/Retry UI, alternate-take selection, edit behavior, Save Points, state inspection, knowledge management, prompt/context inspection, local-model status, and advanced complexity exposed to the user. - -Explicitly identify AI-DnD components worth retaining and Open Dungeon components worth borrowing/reimplementing. Do not redesign the frontend. - -## 14. Validation V8 — Future Media and Speech Readiness - -Against `MEDIA-EXTENSION-CONTRACT.md`, determine whether the architecture can support future local image generation, video generation, audio/ambience, text-to-speech, and speech-to-text. - -For STT, verify the architecture can support: - -```text -local microphone/audio - -> -local STT provider - -> -editable draft text - -> -normal user submission -``` - -STT output must not bypass the normal story commit path. - -Do not implement STT/TTS/video during Phase 0B. Open Dungeon local image behavior may be exercised because it already exists. - -## 15. Final Acceptance Gap Matrix - -Produce a matrix organized by acceptance-test group covering A, C, D, E, F, G, H, I, J, K, L, plus UX fit. Include production impact for each gap. - -## 16. Final Decision Matrix - -Return: - -| Question | AI-DnD | Open Dungeon | ai-adventure | -|---|---|---|---| -| Baseline builds | | | | -| Existing tests pass | | | | -| Runs offline after setup | | | | -| Ollama works | | | | -| History semantics fit | | | | -| State authority fits | | | | -| Memory/lineage fits | | | | -| Imported knowledge fit | | | | -| Prompt inspection fit | | | | -| Security/local-only hardening | | | | -| Unwanted-code removal scope | | | | -| Browser UX fit | | | | -| Media extension fit | | | | -| Future TTS/STT fit | | | | -| Major blockers | | | | - -## 17. Recommendation Report - -The final recommendation should answer: - -1. Which single repository should be the production base? -2. Why? -3. What are the top architectural risks? -4. What must be removed? -5. What must be generalized? -6. Which concepts/components should be reimplemented from other candidates? -7. Does Phase 0B change the preliminary AI-DnD recommendation? -8. Which open questions remain before `TECHNICAL-DESIGN.md` v1.0? -9. Are any v1 requirements likely to need reconsideration because of real technical constraints? -10. Is unlimited Undo straightforward? If not, what practical limit exists and why? - -Use evidence, not repository popularity or feature count. - -## 18. Stop Condition - -Stop after baseline reports, offline/network evidence, three scoped experiments, test-quality report, acceptance-gap matrix, decision matrix, and final recommendation. - -Do not start the production fork conversion, implement the complete branch system, build the final browser UI, implement full RAG, add video/TTS/STT, rewrite the production technical design, or write production milestones. - -Return reports and experiment diffs/results for review. The fork/architecture decision will be made from those results. diff --git a/planning/PLANNING-UPDATE-SUMMARY.md b/planning/PLANNING-UPDATE-SUMMARY.md deleted file mode 100644 index 87f2a12..0000000 --- a/planning/PLANNING-UPDATE-SUMMARY.md +++ /dev/null @@ -1,49 +0,0 @@ -# Planning Update Summary — Post Phase 0B Review - -**Date:** 2026-09-01 -**Purpose:** Review aid. This file summarizes planning changes made after accepting the ten architecture decisions from the Phase 0B review. - -## New Decisions Recorded - -1. AI-DnD is the production base at pinned Phase 0B commit `d72f7c1b...`. -2. AI-DnD's browser/service/story-tree/memory/context foundation is retained as the ownership center. -3. The non-destructive head-cursor Undo/Redo spike is promoted into the production design, but the disposable spike is not treated as merge-ready production code. -4. Narrative state will use explicit typed events/absolute assignments inspired by ai-adventure, not AI-DnD's relative-delta protocol. -5. ai-adventure is an implementation reference, not the product specification. -6. Open Dungeon is a UX/media reference only. -7. Abandoned history is retained and marked disposable; no automatic cleanup is required in v1. -8. Export/import must preserve active head position as part of the history work. -9. Imported knowledge will be a separate first-class subsystem rather than an extension of Story Cards. -10. Future image/video/audio/TTS/STT extension contracts remain, but no media generation is required for v1. -11. Local-only inference may span user-controlled machines: same-host Ollama is the default, but an explicitly configured trusted-LAN Ollama host is supported in v1 without exposing the storyteller UI/API to the LAN. - -## Documents Materially Revised - -- `README.md` -- `SPECIFICATION.md` -- `TECHNICAL-DESIGN.md` -- `DATA-MODEL.md` -- `STORY-BRANCH-SEMANTICS.md` -- `CONTEXT-AND-MEMORY.md` -- `IMPORTED-KNOWLEDGE-DESIGN.md` -- `SECURITY-THREAT-MODEL.md` -- `V1-ACCEPTANCE-TESTS.md` -- `BUILD-MILESTONES.md` -- `RESEARCH-PLAN.md` -- `CODEX-HANDOFF-NOTE.md` -- `004-local-only-production.md` -- `005-branch-preserving-history.md` -- `008-phase0-before-build-plan.md` - -## New ADRs - -- `009-ai-dnd-production-base.md` -- `010-explicit-typed-narrative-state-events.md` - -## Historical Evidence Intentionally Preserved - -The Phase 0A candidate reports and the coding agent's `PHASE-0B-RECOMMENDATION.md` remain research evidence. They may contain assumptions that are superseded by the current planning documents. They are not silently rewritten to make the historical analysis appear as if it had reached later conclusions originally. - -## Coding Prompt Status - -No next Codex implementation prompt is included in this revision. diff --git a/planning/PROJECT-SOURCES.md b/planning/PROJECT-SOURCES.md new file mode 100644 index 0000000..ca05ba4 --- /dev/null +++ b/planning/PROJECT-SOURCES.md @@ -0,0 +1,121 @@ +# ChatGPT Project Sources — what to upload + +This file exists for one purpose: to tell the repository owner which files +belong in the **InteractiveStory** ChatGPT project's Sources. It is not a +documentation index — `planning/README.md` is that. + +`planning/project-sources.txt` is the same list as bare paths, one per line, for +gathering the files. + +The rule behind the list: **upload what is authoritative now.** A project source +is treated as current fact by whatever reads it, so a superseded document +uploaded alongside a current one does not add context — it manufactures a +conflict. + +Refresh the Sources whenever the planning package is revised (`VERSION.md` +records each revision) and whenever a milestone completes. + +## REQUIRED PROJECT SOURCES + +Twenty-eight files. + +**Repository orientation** + +```text +README.md +DEVELOPMENT.md +PROVENANCE.md +``` + +**The planning index and its version** + +```text +planning/README.md +planning/VERSION.md +``` + +**Product requirements and design** + +```text +planning/SPECIFICATION.md +planning/TECHNICAL-DESIGN.md +planning/DATA-MODEL.md +planning/STORY-BRANCH-SEMANTICS.md +planning/CONTEXT-AND-MEMORY.md +planning/IMPORTED-KNOWLEDGE-DESIGN.md +planning/SECURITY-THREAT-MODEL.md +planning/MEDIA-EXTENSION-CONTRACT.md +planning/BROWSER-UX-SPEC.md +``` + +**Implementation plan and acceptance contract** + +```text +planning/BUILD-MILESTONES.md +planning/V1-ACCEPTANCE-TESTS.md +planning/TEST-CAMPAIGN-FIXTURE.md +``` + +**The active ADRs** — all eleven. They are short, and each one closes a question +that will otherwise be reopened. + +```text +planning/DECISIONS/001-browser-first.md +planning/DECISIONS/002-ollama-only-v1.md +planning/DECISIONS/003-authoritative-local-state.md +planning/DECISIONS/004-local-only-production.md +planning/DECISIONS/005-branch-preserving-history.md +planning/DECISIONS/006-genre-agnostic-core.md +planning/DECISIONS/007-future-media-extension.md +planning/DECISIONS/009-ai-dnd-production-base.md +planning/DECISIONS/010-explicit-typed-narrative-state-events.md +planning/DECISIONS/011-local-inference-endpoint-policy.md +planning/DECISIONS/012-active-head-non-destructive-history.md +``` + +ADR **008** is deliberately absent: it was the process gate requiring Phase 0 to +close before production work began, Phase 0 closed on 2026-09-01, and it is now +in `planning/archive/decisions/`. + +## CURRENT-MILESTONE SOURCE + +One file, and it changes as development progresses: + +```text +planning/reports/M3-IMPLEMENTATION-REPORT.md +``` + +M3 is the most recently completed milestone, and M4 is the next to be briefed. +This report is M3's review *and* its primary evidence record — no separate M3 +baseline report was produced — so it is the only place some of what M3 left +behind is written down, including the browser smoke test M3 still owes. + +**Replace it, do not accumulate.** When M4's report lands, remove this one from +the project Sources and upload M4's instead. The repository does the same thing: +`planning/reports/` holds the current milestone's report and +`planning/archive/milestone-reports/` holds the rest. + +## DO NOT UPLOAD AS PROJECT SOURCES + +Not because these are worthless — because a source is read as current fact, and +these are not current. + +- **Everything under `planning/archive/`.** The Phase 0 research reports, the + Phase 0B recommendation and spikes, the completed M1 and M2 milestone reports, + and archived ADR 008. Every conclusion they reached that still matters has + already been applied to the active documents; what is left is superseded + reasoning that will contradict the current package if uploaded beside it. +- **Older milestone reports**, once their successor exists. Uploading M1, M2 and + M3 together produces three descriptions of the same subsystem at three + different stages. +- **Source code.** `backend/`, `frontend/`, tests, migrations. The planning + package describes the system; the code is read in the repository, where it can + be searched and run. +- **Inherited upstream documentation.** Upstream AI-DnD's `plan/` build log and + `docs/` guides, generated HTML and screenshots were deleted from this + repository on 2026-09-03 for exactly this reason: they describe a hosted, + scripted, multi-user product this fork removed. Do not re-upload them from + upstream or from Git history. +- **Generated HTML, images and screenshots.** They add tokens, not facts. +- **`repo-inventory.txt` or any other local scratch output.** Not tracked, not + authoritative, stale the moment it is written. diff --git a/planning/README.md b/planning/README.md index a10ef76..8e5f2ac 100644 --- a/planning/README.md +++ b/planning/README.md @@ -1,21 +1,151 @@ -# Adventure Storyteller Planning Package +# Adventure Storyteller — Planning Package -**Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted (M3: 2026-09-03)**. -**Production coding:** Underway, milestone by milestone. M1, M2 and M3 are done; M4 is the next milestone to brief. +**This file is the index. Start here.** -This package contains the current product requirements, final Phase 0 architecture decisions, detailed subsystem designs, acceptance tests, research evidence, and the production milestone plan for the local-only interactive-story project. +**Current state:** Phase 0 complete; AI-DnD forked as the production base; +milestones **M1, M2 and M3 implemented and accepted** (M3: 2026-09-03). +**Next:** **M4 — named Save Points.** Its brief has not been written yet, and +writing it is the current action. -## Current Decision +**Package version:** see `VERSION.md`, which records what each revision changed +and why. -Phase 0A static research and Phase 0B local validation are complete. +## Where documentation lives -The production starting point is: +```text +planning/ + active specifications and implementation planning — authoritative + +planning/DECISIONS/ + active architectural decisions (ADRs) — authoritative + +planning/reports/ + the current milestone's implementation report — evidence, not instruction + +planning/archive/ + historical evidence and completed planning material; + not authoritative for current implementation +``` + +You do not need to open `planning/archive/` to do ordinary milestone work. Go +there only when an active document sends you for a specific piece of historical +evidence. If an archived document contradicts an active one, the active one is +right. + +Outside `planning/`: `README.md` describes the application, `DEVELOPMENT.md` is +setup and local operation, and `PROVENANCE.md` records what came from upstream +AI-DnD and what each milestone changed. + +## Which document controls + +When two documents appear to conflict, the one higher in this list wins: + +1. `SPECIFICATION.md` — product requirements and required behavior. +2. The detailed behavior/design documents: + `STORY-BRANCH-SEMANTICS.md`, `CONTEXT-AND-MEMORY.md`, + `IMPORTED-KNOWLEDGE-DESIGN.md`, `SECURITY-THREAT-MODEL.md`, + `MEDIA-EXTENSION-CONTRACT.md`, `BROWSER-UX-SPEC.md`, `DATA-MODEL.md`. +3. `V1-ACCEPTANCE-TESTS.md` — the observable pass/fail contract. +4. `TECHNICAL-DESIGN.md` — the selected implementation architecture. +5. The ADRs in `DECISIONS/`. +6. `BUILD-MILESTONES.md` — implementation sequence. It sequences work; it does + not override product behavior. +7. The current milestone report in `reports/`, then `planning/archive/` — + evidence and historical findings, authoritative over nothing. + +Two standing qualifications: + +- Candidate repositories and research reports are **not** specifications. In + particular, ai-adventure is an implementation reference for selected patterns; + its behavior does not override this package. +- Where an ADR records the architecture chosen to implement a requirement stated + elsewhere, both stand: ADR 005 states the history requirement and ADR 012 + states the architecture that implements it. + +## The active documents + +| Document | What it is for | +| --- | --- | +| `SPECIFICATION.md` | What the product must do. The top of the authority order. | +| `TECHNICAL-DESIGN.md` | The selected architecture, including what M1-M3 built, recorded as fact. | +| `DATA-MODEL.md` | Entities, the stored head, branch disposition, and the export shape. | +| `STORY-BRANCH-SEMANTICS.md` | Undo/Redo/Retry/branch/take behavior, including the M3 ratifications. | +| `CONTEXT-AND-MEMORY.md` | Prompt assembly, summarization, branch-safe memory. | +| `IMPORTED-KNOWLEDGE-DESIGN.md` | Canon / Reference / Inspiration knowledge as a first-class subsystem. | +| `SECURITY-THREAT-MODEL.md` | The trust boundary, and the inference endpoint policy as implemented. | +| `MEDIA-EXTENSION-CONTRACT.md` | The contract future image/video/audio/TTS/STT work must fit. | +| `BROWSER-UX-SPEC.md` | The browser surface, and what is deliberately not in it. | +| `BUILD-MILESTONES.md` | M1-M11, what each delivers, what is done, and the notes each milestone leaves its successors. | +| `V1-ACCEPTANCE-TESTS.md` | The pass/fail contract v1 is measured against. | +| `TEST-CAMPAIGN-FIXTURE.md` | The standard campaign the acceptance tests are run on. | +| `VERSION.md` | Package revision history: what each closeout changed. | +| `PROJECT-SOURCES.md` | Which files to upload as ChatGPT Project Sources. | + +## Reading order for a new coding agent + +1. This file. +2. `SPECIFICATION.md` +3. `TECHNICAL-DESIGN.md` +4. `BUILD-MILESTONES.md` — find the milestone you are being asked to do. +5. `STORY-BRANCH-SEMANTICS.md` +6. `DATA-MODEL.md` +7. `CONTEXT-AND-MEMORY.md` +8. `IMPORTED-KNOWLEDGE-DESIGN.md` +9. `SECURITY-THREAT-MODEL.md` +10. `BROWSER-UX-SPEC.md` +11. `V1-ACCEPTANCE-TESTS.md` +12. `DECISIONS/` — all of them; they are short. +13. `reports/M3-IMPLEMENTATION-REPORT.md`, for what the last milestone actually + left behind. Nothing in `planning/archive/` unless sent there. + +## Architectural decisions + +Active ADRs, all of which still constrain current or future work: + +| ADR | Decision | +| --- | --- | +| `001-browser-first.md` | The UI is a browser application. | +| `002-ollama-only-v1.md` | Ollama-compatible local inference only; trusted-LAN HTTPS with a private CA is verified, never bypassed. | +| `003-authoritative-local-state.md` | The application owns authoritative state; the model does not. | +| `004-local-only-production.md` | Local-only runtime; no cloud, telemetry or runtime remote assets. | +| `005-branch-preserving-history.md` | History is preserved, not overwritten. **The requirement.** | +| `006-genre-agnostic-core.md` | The core state model is genre-agnostic. | +| `007-future-media-extension.md` | Media generation stays optional and decoupled. | +| `009-ai-dnd-production-base.md` | AI-DnD at `d72f7c1b…` is the production base. | +| `010-explicit-typed-narrative-state-events.md` | Explicit typed events / absolute assignments, not relative deltas. | +| `011-local-inference-endpoint-policy.md` | Address allowlist, deny by default, checked twice, TLS mandatory. | +| `012-active-head-non-destructive-history.md` | The stored active head. **The architecture implementing ADR 005.** | + +`008-phase0-before-build-plan.md` was a process gate — do not begin production +work until Phase 0 closes — which Phase 0 satisfied on 2026-09-01. It is in +`archive/decisions/` and constrains nothing now. ADR numbering continues from +012; 008 is not reused. + +## Milestone reports + +`reports/` holds the report for the milestone most recently completed, because +that is the one the next milestone's planning has to consult: + +- `reports/M3-IMPLEMENTATION-REPORT.md` — M3's review **and** its primary + evidence record; no separate M3 baseline report was produced. M4 needs its + §W (closeout) and §M/§W.4 (the open browser smoke test). + +Completed earlier milestones are in `archive/milestone-reports/`. When M4's +report lands, M3's moves there too: a milestone report is useful during the +immediate next milestone and historical afterwards. + +## The decision this package rests on + +Phase 0A static research and Phase 0B local validation closed on 2026-09-01 with +one decision: > **Fork AI-DnD at upstream commit `d72f7c1bda0f34fccd84afb7a25c34eb01c901de`.** -The selection is based on measured Phase 0B behavior, not feature count. AI-DnD already contains the highest-value structural machinery: browser UI, FastAPI service boundary, SQLite persistence, parent-linked story history, alternate takes, branch-aware state snapshots, local Ollama operation, branch-scoped memory, prompt/context inspection, streaming, export/import, and a substantial automated test suite. - -The selected composition of ideas is: +It was chosen on measured behavior, not feature count: AI-DnD already had the +browser UI, the FastAPI service boundary, SQLite persistence, parent-linked +story history, alternate takes, branch-aware state snapshots, local Ollama +operation, branch-scoped memory, prompt inspection, streaming, export/import and +a substantial test suite. The composition of ideas selected around it: ```text AI-DnD production base @@ -26,45 +156,20 @@ AI-DnD production base + Gamentic provider-neutral media concepts ``` -This is **not** a repository merge. AI-DnD is the ownership center. Other projects are implementation references only unless a later milestone explicitly reimplements a compatible idea. +This is **not** a repository merge. AI-DnD is the ownership center; the others +are implementation references only. -## Phase 0B Findings That Changed the Plan +Phase 0B also corrected several Phase 0A assumptions, and those corrections are +now built into the active documents rather than needing to be read from the +research: shipped Undo was destructive with no Redo (ADR 012 replaced it); +relative-delta world state can be semantically wrong while syntactically valid +(ADR 010); `tiktoken` and Google Fonts broke offline operation (fixed in M1); +export had to carry the head position (M3); Story Cards were not a sufficient +imported-knowledge store (`IMPORTED-KNOWLEDGE-DESIGN.md`). -Phase 0B confirmed the fork choice while correcting several Phase 0A assumptions: - -- AI-DnD's shipped Undo was destructive and had no Redo. -- A disposable spike proved non-destructive head-cursor Undo/Redo in three backend files while preserving branch-scoped memory isolation. -- A new continuation written after Undo can fork from the moved-back head while retaining the abandoned future. -- AI-DnD's current relative-delta world-state protocol can produce semantically wrong state under realistic context even when the proposal is syntactically valid. -- Production narrative state will therefore use explicit typed events/absolute assignments inspired by ai-adventure rather than AI-DnD's relative-delta protocol. -- AI-DnD requires offline hardening: `tiktoken` attempts a first-use CDN fetch and the SPA requests Google Fonts at runtime. -- AI-DnD's export format must preserve the active head position; otherwise export/import can silently redo an undone story. -- AI-DnD Story Cards are not a sufficient imported-knowledge store because they are not designed for the required classification, provenance, chunking, and lineage semantics. -- Open Dungeon remains useful for UX/media ideas but is no longer a serious production-fork candidate. -- ai-adventure is not the production base but is the strongest implementation reference for authoritative typed state events, head movement, checkpoints, replay, and narrow local-only behavior. - -See `PHASE-0B-RECOMMENDATION.md` for the coding agent's evidence. That report is retained as research evidence; the planning documents in this package record the decisions made after reviewing it. - -## Document Authority - -Use the documents in this order when requirements appear to conflict: - -1. `SPECIFICATION.md` — product requirements and required behavior. -2. Detailed behavior/design documents: - - `STORY-BRANCH-SEMANTICS.md` - - `CONTEXT-AND-MEMORY.md` - - `IMPORTED-KNOWLEDGE-DESIGN.md` - - `SECURITY-THREAT-MODEL.md` - - `MEDIA-EXTENSION-CONTRACT.md` - - `BROWSER-UX-SPEC.md` - - `DATA-MODEL.md` -3. `V1-ACCEPTANCE-TESTS.md` — observable pass/fail contract. -4. `TECHNICAL-DESIGN.md` — selected implementation architecture. -5. Foundational ADRs (`001-...md` through the current ADR set). -6. `BUILD-MILESTONES.md` — implementation sequence; it does not override product behavior. -7. Phase 0 research reports — evidence and historical findings. - -Candidate repositories and research reports are **not** specifications. In particular, ai-adventure is an implementation reference for selected patterns; its behavior does not override this package. +The evidence is in `archive/phase0/`, and `archive/phase0/PHASE-0B-RECOMMENDATION.md` +is the strongest single document there. It is research evidence, not a +specification, and it was not rewritten to match later conclusions. ## Foundational Decisions @@ -95,29 +200,10 @@ The following are settled for v1: - LAN inference is distinct from LAN exposure of the storyteller UI/API; the latter is not required for v1, - future local image/video/audio/TTS/STT support remains optional and decoupled from the story engine. -## Phase 0 Status -### Complete +## Still open, and deliberately so -- candidate discovery and triage, -- static architecture/privacy/licensing review, -- local clone/build/test validation, -- real Ollama testing, -- offline/network observation, -- AI-DnD strip-down/entanglement checks, -- Open Dungeon history-retrofit analysis, -- ai-adventure Ollama/service-boundary checks, -- AI-DnD non-destructive Undo/Redo spike, -- referee/state-protocol follow-up, -- export/import head-position follow-up, -- Story Card lineage review, -- Postgres removability review, -- production fork decision, -- production architecture decision. - -### Deferred to implementation/release validation - -These do not block the architecture decision: +These do not block the architecture and are not v1 requirements: - comparative recommendation of narrator/state models for real users, - multi-hour/100-turn long-run behavior, @@ -125,78 +211,56 @@ These do not block the architecture decision: - actual future image/video/TTS/STT provider integration, - abandoned-history cleanup UI/policy (not required in v1). -## Recommended Reading Order for the Next Implementation Stage -Do not convert this into a coding prompt until the package review is approved. - -When implementation planning resumes, read: - -1. `SPECIFICATION.md` -2. `TECHNICAL-DESIGN.md` -3. `BUILD-MILESTONES.md` -4. `STORY-BRANCH-SEMANTICS.md` -5. `DATA-MODEL.md` -6. `CONTEXT-AND-MEMORY.md` -7. `IMPORTED-KNOWLEDGE-DESIGN.md` -8. `SECURITY-THREAT-MODEL.md` -9. `BROWSER-UX-SPEC.md` -10. `V1-ACCEPTANCE-TESTS.md` -11. ADRs, especially the production-base and narrative-state-event decisions -12. Phase 0B reports only as supporting evidence - -## Workflow From Here +## Where the work stands ```text Phase 0 research and spikes COMPLETE +Architecture/fork decision COMPLETE +Planning package approved COMPLETE | v -Architecture/fork decision COMPLETE +Milestone M1 COMPLETE (2026-09-02) + fork + offline baseline archive/milestone-reports/M1-*.md | v -Planning package revision COMPLETE - | - v -Approve planning package COMPLETE - | - v -Milestone M1 COMPLETE (2026-09-02) - fork + offline baseline see planning/reports/M1-*.md - | - v -Milestone M2 COMPLETE (2026-09-02) - local-only surface + endpoint see planning/reports/M2-*.md +Milestone M2 COMPLETE (2026-09-02) + local-only surface + endpoint archive/milestone-reports/M2-*.md policy | v -Milestone M3 COMPLETE (2026-09-03) - non-destructive undo/redo, see planning/reports/M3-*.md and ADR 012 - active-head export one open condition: the browser smoke test +Milestone M3 COMPLETE (2026-09-03) + non-destructive undo/redo, reports/M3-IMPLEMENTATION-REPORT.md + active-head export and ADR 012 | v -Milestone M4 NEXT — brief not yet prepared +Milestone M4 NEXT — brief not yet prepared named Save Points | v -Implement and review milestone-by-milestone +M5-M11, one at a time see BUILD-MILESTONES.md ``` ## Stop Rule **One milestone at a time. Do not begin a milestone before its brief exists.** -M1, M2 and M3 are complete and accepted; the evidence is in `reports/M1-*.md`, -`reports/M2-*.md` and `reports/M3-IMPLEMENTATION-REPORT.md` — the last of which -is M3's primary evidence record as well as its review, since no separate M3 -baseline report was produced. **No M4 brief has been prepared.** The current -action is to write one, informed by the post-M3 corrections below, by the note -`BUILD-MILESTONES.md` now attaches to M4, and by **ADR 012**, which records the -head-movement mechanism M4 must reuse rather than reimplement. +**No M4 brief has been prepared.** Writing one is the current action, informed +by the post-M3 corrections below, by the note `BUILD-MILESTONES.md` attaches to +M4, and by **ADR 012**, which records the head-movement mechanism M4 must reuse +rather than reimplement. -One M3 condition remains open and is not a blocker for M4: the required -**browser smoke test has not been performed**, because no session in which M3 -was implemented or reviewed had a browser available. See +One M3 condition remains open and does not block M4: the required **browser +smoke test has not been performed**, because no session in which M3 was +implemented or reviewed had a browser available. See `reports/M3-IMPLEMENTATION-REPORT.md` §M and §W.4. +## What each milestone closeout corrected + +These tables are the audit trail: what implementation evidence forced back into +the planning package, milestone by milestone. `VERSION.md` narrates the same +changes. + ### Post-M3 corrections applied (2026-09-03) M3's review recommended planning changes and, following the M2 pattern, reported diff --git a/planning/TECHNICAL-DESIGN.md b/planning/TECHNICAL-DESIGN.md index ef4c605..98fbc71 100644 --- a/planning/TECHNICAL-DESIGN.md +++ b/planning/TECHNICAL-DESIGN.md @@ -184,7 +184,7 @@ controls on a network they trust — and why the endpoint is always explicitly configured, never discovered. See ADR 002 (*Transport for a Trusted-LAN Endpoint*) and, for the demonstrated -deployment, `planning/reports/M1-IMPLEMENTATION-REPORT.md` §F. +deployment, `planning/archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md` §F. Allowed future local paths may also include explicitly configured local media services. diff --git a/planning/VERSION.md b/planning/VERSION.md index 9742865..46fece8 100644 --- a/planning/VERSION.md +++ b/planning/VERSION.md @@ -1,8 +1,44 @@ # Planning Package Version -**Package:** Adventure Storyteller Planning Package v2.3 -**Revision date:** 2026-09-03 -**Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted**; M4 is next to brief. +- **Package:** Adventure Storyteller Planning Package v2.4 +- **Revision date:** 2026-09-03 +- **Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted**; M4 is next to brief. + +## v2.4 — Documentation Consolidation (2026-09-03) + +No product requirement, architecture decision or milestone status changed in +this revision. It reorganises the documentation so that a new coding agent can +tell authoritative material from evidence at a glance. + +- `planning/archive/` is created and is **non-authoritative by declaration** + (`archive/README.md`). It holds `phase0/` — the research that chose AI-DnD — + `milestone-reports/` — the completed M1 and M2 reports — and `decisions/`, + which now holds ADR **008**, the Phase-0-before-build process gate that Phase 0 + satisfied. ADR numbering continues from 012; 008 is not reused. +- `planning/reports/` now holds **only the current milestone's report**, + `M3-IMPLEMENTATION-REPORT.md`, because M4 planning has to consult it. It moves + to the archive when M4's report replaces it. +- The Phase 0B execution prompts and handoff/status/summary documents + (`CODEX-HANDOFF-NOTE.md`, `PHASE-0B-CODEX-BRIEF.md`, + `PHASE-0B-CODEX-HANDOFF.md`, `PLANNING-UPDATE-SUMMARY.md`) and the Phase 0A + discovery and triage reports were **deleted**: intermediate working documents + whose conclusions all reached the two recommendation reports, and which remain + in Git history. +- Upstream AI-DnD's inherited `plan/` build log and `docs/` project site + (guides, generated HTML, screenshots) were **deleted**. They documented a + hosted, scripted, multi-user product with accounts — every screenshot showed a + Scripts tab and a Sign up button — which M2 removed. Both trees remain in Git + history and in upstream. +- `README.md`, `DEVELOPMENT.md` and `PROVENANCE.md` are corrected where they + pointed at the removed trees or described removed capability as present. + `DEVELOPMENT.md`'s "things M1 did not touch" section had gone stale at M2 and + now says what is actually still inherited. +- `planning/README.md` is rewritten as **the documentation index**: current + milestone, the three-way active/ADR/archive split, the authority order, + reading order, where reports live, and what comes next. The milestone + correction tables are preserved unchanged. +- New: `planning/PROJECT-SOURCES.md` and `planning/project-sources.txt`, the + manifest of files to upload as ChatGPT Project Sources. ## v2.3 — Post-M3 Closeout (2026-09-03) @@ -51,7 +87,7 @@ M2 removed the hosted, cloud, account and scripting surface and added the inference endpoint policy. Its review recommended six planning changes and reported rather than applied them; all six are applied in this revision, listed in `README.md` § *Post-M2 corrections applied*, with the evidence in -`reports/M2-BASELINE-REPORT.md` and `reports/M2-IMPLEMENTATION-REPORT.md`. +`archive/milestone-reports/M2-BASELINE-REPORT.md` and `archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md`. In summary: @@ -84,7 +120,7 @@ the architecture selected in v2 was reversed. M1 implementation evidence contradicted or under-specified parts of v2. The corrections are recorded in the documents themselves and listed in `README.md` § *Post-M1 corrections applied*; the evidence behind them is in -`reports/M1-BASELINE-REPORT.md` and `reports/M1-IMPLEMENTATION-REPORT.md`. +`archive/milestone-reports/M1-BASELINE-REPORT.md` and `archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md`. In summary: diff --git a/planning/archive/README.md b/planning/archive/README.md new file mode 100644 index 0000000..23dc245 --- /dev/null +++ b/planning/archive/README.md @@ -0,0 +1,67 @@ +# Archive — historical material, not authoritative + +Everything under `planning/archive/` is **evidence and history**. None of it +governs current implementation. If an archived document and an active planning +document disagree, the active document is right and the archived one records +what was believed or measured at the time. + +Do not consult this directory during ordinary milestone work unless an active +document sends you here for a specific piece of historical evidence. + +## What is here + +### `phase0/` — why AI-DnD was selected + +Phase 0A static research and Phase 0B local validation, closed 2026-09-01. + +| File | What it is | +| --- | --- | +| `RESEARCH-PLAN.md` | The question Phase 0 existed to answer, and its final dispositions. | +| `PRELIMINARY-RECOMMENDATION.md` | The Phase 0A conclusion, from static review alone. | +| `REUSE-MATRIX.md` | What each candidate offered against the required subsystems. | +| `AI-DND-ANALYSIS.md` | The selected base, analysed before the fork. | +| `AI-ADVENTURE-ANALYSIS.md` | The rejected finalist still cited as the implementation reference for typed state events. | +| `OPEN-DUNGEON-ANALYSIS.md` | The rejected finalist still cited as the UX/future-media reference. | +| `PHASE-0B-RECOMMENDATION.md` | **The strongest single document.** The measured Phase 0B findings and the fork decision. | +| `PHASE-0B-BASELINE.md` | Clean clones, builds and test runs for the three finalists. | +| `PHASE-0B-AI-DND-EXPERIMENT.md` | AI-DnD driven against a real local Ollama. | +| `PHASE-0B-AI-ADVENTURE-OLLAMA.md` | ai-adventure's Ollama and service-boundary behaviour. | +| `PHASE-0B-OPEN-DUNGEON-HISTORY.md` | Whether Open Dungeon could be retrofitted with history. | +| `PHASE-0B-OFFLINE-NETWORK.md` | What each candidate reached for with no route to the Internet. | +| `PHASE-0B-UNDO-SPIKE.md` | The disposable spike that proved non-destructive undo/redo, and became ADR 012's architecture. | +| `PHASE-0B-FOLLOWUP-CHECKS.md` | The world-state protocol, export/head, story-card lineage and Postgres checks. | + +Phase 0A discovery and triage material (the candidate inventory, source index, +reference-project list, licensing and static-privacy reviews, the Phase 0A +status page) and the Phase 0B execution prompts were deleted in the 2026-09-03 +documentation cleanup. They are intermediate working documents whose +conclusions all reached the two recommendation reports above, and they remain in +Git history. + +### `milestone-reports/` — completed milestone evidence + +`M1-BASELINE-REPORT.md`, `M1-IMPLEMENTATION-REPORT.md`, +`M2-BASELINE-REPORT.md`, `M2-IMPLEMENTATION-REPORT.md`. + +Every architectural conclusion these reports reached has already been applied to +the active planning documents and the ADRs — see `planning/VERSION.md`, which +lists the corrections each milestone produced. The reports are kept for their +measurements and their reasoning, not as instructions. + +The **current** milestone's report stays in `planning/reports/` while it is +still useful for reviewing the next milestone, and moves here when it is not. + +### `decisions/` — superseded or completed ADRs + +`008-phase0-before-build-plan.md` — a process gate ("do not start production +work before Phase 0 closes") that Phase 0 satisfied on 2026-09-01. It +constrains nothing now. ADR numbering continues from 012 in +`planning/DECISIONS/`; 008 is not reused. + +## A note on paths inside these files + +Archived documents are kept **verbatim**. File paths written inside them refer +to where those files lived when the document was written — before this archive +existed, and in some cases before files were deleted. That is deliberate: an +evidence record that has been quietly edited is no longer evidence. Resolve any +such path against Git history, not against the current tree. diff --git a/planning/DECISIONS/008-phase0-before-build-plan.md b/planning/archive/decisions/008-phase0-before-build-plan.md similarity index 100% rename from planning/DECISIONS/008-phase0-before-build-plan.md rename to planning/archive/decisions/008-phase0-before-build-plan.md diff --git a/planning/reports/M1-BASELINE-REPORT.md b/planning/archive/milestone-reports/M1-BASELINE-REPORT.md similarity index 100% rename from planning/reports/M1-BASELINE-REPORT.md rename to planning/archive/milestone-reports/M1-BASELINE-REPORT.md diff --git a/planning/reports/M1-IMPLEMENTATION-REPORT.md b/planning/archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md similarity index 100% rename from planning/reports/M1-IMPLEMENTATION-REPORT.md rename to planning/archive/milestone-reports/M1-IMPLEMENTATION-REPORT.md diff --git a/planning/reports/M2-BASELINE-REPORT.md b/planning/archive/milestone-reports/M2-BASELINE-REPORT.md similarity index 100% rename from planning/reports/M2-BASELINE-REPORT.md rename to planning/archive/milestone-reports/M2-BASELINE-REPORT.md diff --git a/planning/reports/M2-IMPLEMENTATION-REPORT.md b/planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md similarity index 100% rename from planning/reports/M2-IMPLEMENTATION-REPORT.md rename to planning/archive/milestone-reports/M2-IMPLEMENTATION-REPORT.md diff --git a/planning/reports/AI-ADVENTURE-ANALYSIS.md b/planning/archive/phase0/AI-ADVENTURE-ANALYSIS.md similarity index 100% rename from planning/reports/AI-ADVENTURE-ANALYSIS.md rename to planning/archive/phase0/AI-ADVENTURE-ANALYSIS.md diff --git a/planning/reports/AI-DND-ANALYSIS.md b/planning/archive/phase0/AI-DND-ANALYSIS.md similarity index 100% rename from planning/reports/AI-DND-ANALYSIS.md rename to planning/archive/phase0/AI-DND-ANALYSIS.md diff --git a/planning/reports/OPEN-DUNGEON-ANALYSIS.md b/planning/archive/phase0/OPEN-DUNGEON-ANALYSIS.md similarity index 100% rename from planning/reports/OPEN-DUNGEON-ANALYSIS.md rename to planning/archive/phase0/OPEN-DUNGEON-ANALYSIS.md diff --git a/planning/reports/PHASE-0B-AI-ADVENTURE-OLLAMA.md b/planning/archive/phase0/PHASE-0B-AI-ADVENTURE-OLLAMA.md similarity index 100% rename from planning/reports/PHASE-0B-AI-ADVENTURE-OLLAMA.md rename to planning/archive/phase0/PHASE-0B-AI-ADVENTURE-OLLAMA.md diff --git a/planning/reports/PHASE-0B-AI-DND-EXPERIMENT.md b/planning/archive/phase0/PHASE-0B-AI-DND-EXPERIMENT.md similarity index 100% rename from planning/reports/PHASE-0B-AI-DND-EXPERIMENT.md rename to planning/archive/phase0/PHASE-0B-AI-DND-EXPERIMENT.md diff --git a/planning/reports/PHASE-0B-BASELINE.md b/planning/archive/phase0/PHASE-0B-BASELINE.md similarity index 100% rename from planning/reports/PHASE-0B-BASELINE.md rename to planning/archive/phase0/PHASE-0B-BASELINE.md diff --git a/planning/reports/PHASE-0B-FOLLOWUP-CHECKS.md b/planning/archive/phase0/PHASE-0B-FOLLOWUP-CHECKS.md similarity index 100% rename from planning/reports/PHASE-0B-FOLLOWUP-CHECKS.md rename to planning/archive/phase0/PHASE-0B-FOLLOWUP-CHECKS.md diff --git a/planning/reports/PHASE-0B-OFFLINE-NETWORK.md b/planning/archive/phase0/PHASE-0B-OFFLINE-NETWORK.md similarity index 100% rename from planning/reports/PHASE-0B-OFFLINE-NETWORK.md rename to planning/archive/phase0/PHASE-0B-OFFLINE-NETWORK.md diff --git a/planning/reports/PHASE-0B-OPEN-DUNGEON-HISTORY.md b/planning/archive/phase0/PHASE-0B-OPEN-DUNGEON-HISTORY.md similarity index 100% rename from planning/reports/PHASE-0B-OPEN-DUNGEON-HISTORY.md rename to planning/archive/phase0/PHASE-0B-OPEN-DUNGEON-HISTORY.md diff --git a/planning/PHASE-0B-RECOMMENDATION.md b/planning/archive/phase0/PHASE-0B-RECOMMENDATION.md similarity index 100% rename from planning/PHASE-0B-RECOMMENDATION.md rename to planning/archive/phase0/PHASE-0B-RECOMMENDATION.md diff --git a/planning/reports/PHASE-0B-UNDO-SPIKE.md b/planning/archive/phase0/PHASE-0B-UNDO-SPIKE.md similarity index 100% rename from planning/reports/PHASE-0B-UNDO-SPIKE.md rename to planning/archive/phase0/PHASE-0B-UNDO-SPIKE.md diff --git a/planning/reports/PRELIMINARY-RECOMMENDATION.md b/planning/archive/phase0/PRELIMINARY-RECOMMENDATION.md similarity index 100% rename from planning/reports/PRELIMINARY-RECOMMENDATION.md rename to planning/archive/phase0/PRELIMINARY-RECOMMENDATION.md diff --git a/planning/RESEARCH-PLAN.md b/planning/archive/phase0/RESEARCH-PLAN.md similarity index 100% rename from planning/RESEARCH-PLAN.md rename to planning/archive/phase0/RESEARCH-PLAN.md diff --git a/planning/reports/REUSE-MATRIX.md b/planning/archive/phase0/REUSE-MATRIX.md similarity index 100% rename from planning/reports/REUSE-MATRIX.md rename to planning/archive/phase0/REUSE-MATRIX.md diff --git a/planning/project-sources.txt b/planning/project-sources.txt new file mode 100644 index 0000000..7bdda56 --- /dev/null +++ b/planning/project-sources.txt @@ -0,0 +1,29 @@ +README.md +DEVELOPMENT.md +PROVENANCE.md +planning/README.md +planning/VERSION.md +planning/SPECIFICATION.md +planning/TECHNICAL-DESIGN.md +planning/DATA-MODEL.md +planning/STORY-BRANCH-SEMANTICS.md +planning/CONTEXT-AND-MEMORY.md +planning/IMPORTED-KNOWLEDGE-DESIGN.md +planning/SECURITY-THREAT-MODEL.md +planning/MEDIA-EXTENSION-CONTRACT.md +planning/BROWSER-UX-SPEC.md +planning/BUILD-MILESTONES.md +planning/V1-ACCEPTANCE-TESTS.md +planning/TEST-CAMPAIGN-FIXTURE.md +planning/DECISIONS/001-browser-first.md +planning/DECISIONS/002-ollama-only-v1.md +planning/DECISIONS/003-authoritative-local-state.md +planning/DECISIONS/004-local-only-production.md +planning/DECISIONS/005-branch-preserving-history.md +planning/DECISIONS/006-genre-agnostic-core.md +planning/DECISIONS/007-future-media-extension.md +planning/DECISIONS/009-ai-dnd-production-base.md +planning/DECISIONS/010-explicit-typed-narrative-state-events.md +planning/DECISIONS/011-local-inference-endpoint-policy.md +planning/DECISIONS/012-active-head-non-destructive-history.md +planning/reports/M3-IMPLEMENTATION-REPORT.md diff --git a/planning/reports/AIMULTIFOOL-ANALYSIS.md b/planning/reports/AIMULTIFOOL-ANALYSIS.md deleted file mode 100644 index 321848c..0000000 --- a/planning/reports/AIMULTIFOOL-ANALYSIS.md +++ /dev/null @@ -1,46 +0,0 @@ -# aiMultiFool — Static Architecture Analysis - -**Repository:** https://github.com/omgboohoo/aimultifool -**Date reviewed:** 2026-09-01 -**Disposition:** Reference only. - -## Useful ideas - -aiMultiFool is a local roleplay/chat sandbox with: -- Ollama support, -- local inference paths, -- Vector Chat / semantic-memory concepts, -- save/load, -- rewind/regenerate, -- context inspection, -- optional encrypted local data. - -Those are useful implementation references for local memory tooling and diagnostics. - -## Why it is not a fork finalist - -### Product mismatch -The interface is terminal/Textual-oriented and character-chat/roleplay focused rather than a browser-first persistent fiction editor. - -### History/context mismatch -Its documented smart-pruning strategy removes older middle messages from active chat state as context pressure grows. That is a reasonable chat optimization but not the target architecture. The target must preserve an immutable authoritative transcript and prune only the prompt representation. - -### State model -The project does not provide the same authoritative event/state/branch model found in AI-DnD or ai-adventure. - -### License -The repository is GPL-3.0. Directly copying substantial GPL code into an MIT/Apache-derived application would change licensing obligations. Unless the final project intentionally adopts GPL-compatible distribution terms, use this project for concepts rather than source copying. - -## Recommended reuse - -Study: -- local embedding workflow, -- vector inspection/debugging, -- encrypted local payload design, -- user-facing memory controls. - -Do not make it a Phase 0B build finalist. - -## Source - -- Repository: https://github.com/omgboohoo/aimultifool diff --git a/planning/reports/CANDIDATE-INVENTORY.md b/planning/reports/CANDIDATE-INVENTORY.md deleted file mode 100644 index 962907f..0000000 --- a/planning/reports/CANDIDATE-INVENTORY.md +++ /dev/null @@ -1,68 +0,0 @@ -# Candidate Inventory and Triage - -**Historical status:** Phase 0A triage. The final production disposition was selected after Phase 0B; see ADR 009. - -**Phase:** 0A — Static research -**Date:** 2026-09-01 - -## Executive result - -Three projects should advance to local validation: - -1. **AI-DnD** — strongest implementation of the hardest required backend capabilities. -2. **Open Dungeon** — strongest direct product/UX fit and strongest near-term media path. -3. **CaoRuiming/ai-adventure (Local Adventure Engine)** — strongest authoritative-state, replay, checkpoint, and privacy architecture. - -Everything else should remain available as a design/source reference but should not consume local build-validation effort unless one of the three finalists fails. - -## Triage table - -| Project | Browser-first | Ollama | Durable branch/rollback | Long memory | Local knowledge | Future media | Static disposition | -|---|---|---|---|---|---|---|---| -| AI-DnD | Yes | Yes | **Strong** | **Strong** | Story cards + memory | Not core | **Finalist #1** | -| Open Dungeon | **Yes** | **Yes** | Weak / destructive linear tail today | Summary-based | Limited | **Strong; local image generation already present** | **Finalist #2** | -| ai-adventure | No; CLI | LM Studio today | **Strong** | Summary + lore FTS | **Strong deterministic local lore** | No | **Finalist #3** | -| Chronicler | Yes | Yes | Not the focus | **Excellent memory model** | Memory-centric | No | Reference | -| Gamentic | **Yes** | llama.cpp/OpenAI-compatible | Game-state oriented | Strong | World bible | **Excellent image/voice provider design** | Reference | -| Interactive Fiction Framework | **Yes** | **Yes** | Not established as required story-tree model | Canon/scene/character memory | Story Bible | Not core | Reference | -| Sonder Engine | **Yes** | **Yes** | Persistent variants/checkpoints, but much more agentic | **Very sophisticated** | Character-scoped retrieval | Not primary | Reference | -| Corvus Story Core | **Yes** | OpenAI-compatible | No equivalent branch tree established | Summaries + state | World/state | ComfyUI + TTS | Reference | -| aiMultiFool | No; terminal | **Yes** | Rewind, not target architecture | Vector chat | RAG-oriented | No | Reference only | -| SillyTavern | **Yes** | Local backends | Chat-oriented | Extensions/lorebooks | **Excellent lorebook UX** | Broad extensions | Reference only | -| RisuAI | **Yes** | Local/remote ecosystem | Chat-oriented | Hypa/SupaMemory | Lorebooks | Broad media | Reference only | -| KoboldAI | **Yes** | Local ecosystem | Traditional save/load | Memory/World Info | World Info | Limited | Reference only | - -## Why the shortlist is only three - -### AI-DnD advances because -It already implements the expensive correctness work: a parent/lineage story tree, alternate takes, non-destructive retry, state snapshots and rollback, branch-aware context, summaries, embedding retrieval, story cards, exact prompt inspection, export/import of the complete tree, and a substantial automated test suite. - -### Open Dungeon advances because -It is almost exactly the desired product shape: browser-first, simple interactive fiction, Ollama, local SQLite, streaming narration, visual character continuity, and local image-generation hooks. Its key weakness is architectural rather than cosmetic: its current message schema is linear and its retry/erase operation deletes the selected message and the rest of the tail. - -### ai-adventure advances because -Its core philosophy most closely matches the required trust model. SQLite and typed events are authoritative; the model proposes changes; validation occurs before atomic commit; undo/checkpoint/restore/branch work by replaying parent-linked history; lore is local; and the privacy documentation explicitly minimizes network and executable-extension surfaces. - -## Projects eliminated from fork contention - -### Chronicler -Excellent source for memory semantics, but the application is centered on long-running roleplay and YantrikDB cognitive memory rather than the simpler interactive-story product. Its memory-tier design should be borrowed conceptually. - -### Gamentic -Technically impressive and very useful for future media design, but it is intentionally a multi-agent RPG with image and voice infrastructure, tuned around a heavier local stack. Forking it would mean removing more game/agent behavior than necessary. - -### Interactive Fiction Framework -Its Story Bible, validation, and application-owned-state design are highly relevant. However, it is oriented toward contributor-authored, schema-driven stories and planner-approved choices rather than the unrestricted natural-language story continuation and branch history required here. - -### Sonder Engine -Strong engineering, but its core differentiator is separate fictional minds with strict perception/knowledge boundaries and a multi-stage agent pipeline. That is substantially more complexity than v1 requires. - -### Corvus Story Core -Useful image/TTS and state-extraction reference, but its persistence is JSON/JSONL-oriented and the static review did not establish the required non-destructive branch/checkpoint model. - -### aiMultiFool -Useful local vector-memory ideas, but it is a terminal character-roleplay application, its context-pruning approach is not the desired immutable-history architecture, and GPL-3.0 complicates direct code reuse into a permissively licensed fork. - -## Sources - -See `SOURCE-INDEX.md` for repository/source links. diff --git a/planning/reports/LICENSING-REUSE.md b/planning/reports/LICENSING-REUSE.md deleted file mode 100644 index ffce6f8..0000000 --- a/planning/reports/LICENSING-REUSE.md +++ /dev/null @@ -1,75 +0,0 @@ -# Licensing and Reuse Review - -**Date:** 2026-09-01 -**Nature:** Engineering planning summary, not legal advice. - -## Production Selection Update - -Phase 0B selected **AI-DnD (MIT)** as the production fork/base. The production strategy therefore remains on a permissive-license path. - -The project may reimplement compatible ideas from other projects, but direct source copying must still be reviewed file-by-file and retain applicable notices. GPL/AGPL projects remain concept/reference sources unless a later explicit licensing decision changes that policy. - -Maintain third-party notices from the first production milestone. - -## Permissive finalists - -### AI-DnD -- License: MIT -- Direct modification/forking is generally compatible with a permissive local application, subject to preserving required notices. - -### Open Dungeon -- License: MIT -- Same practical advantage for direct reuse. - -### ai-adventure -- License: Apache-2.0 -- Permissive, but Apache notice/license obligations must be preserved. - -These three can plausibly participate in a permissively licensed implementation strategy, subject to checking individual vendored/third-party files. - -## Permissive reference projects - -Static repository licensing indicates: -- Chronicler: MIT -- Interactive Fiction Framework: MIT -- Gamentic: MIT -- Sonder Engine: MIT -- Corvus Story Core: MIT - -If source is copied, retain the applicable notices and verify whether particular directories/files carry separate licenses. - -## Copyleft references - -### aiMultiFool -- GPL-3.0 -- Treat as a concept/reference source unless the final project intentionally accepts GPL obligations. - -### LettuceAI -- AGPL-3.0 -- Reference only for this project unless there is a deliberate licensing decision. - -Mature roleplay ecosystems such as SillyTavern/RisuAI/KoboldAI should have their exact current license verified before any code copying. No direct reuse is currently recommended. - -## Media dependencies - -Important distinction: -- application code license, -- media runtime license, -- model-weight license -are separate. - -For example Gamentic documents: -- its own code under MIT, -- ComfyUI runtime under GPL-3.0, -- model weights under their own terms. - -Using a separately running local service through an API is architecturally different from copying its code into the storyteller, but distribution/bundling choices should be reviewed before release. - -## Recommendation - -Keep the production application's own code on a permissive-license path if possible: -- primary fork from MIT or Apache-2.0, -- copy code only from compatible permissive sources, -- treat GPL/AGPL projects as design references unless a conscious license change is made, -- keep optional media providers as external adapters/services where practical, -- maintain a third-party notices file from the first production milestone. diff --git a/planning/reports/PHASE-0A-STATUS.md b/planning/reports/PHASE-0A-STATUS.md deleted file mode 100644 index 8c55f05..0000000 --- a/planning/reports/PHASE-0A-STATUS.md +++ /dev/null @@ -1,41 +0,0 @@ -# Phase 0A Status - -**Historical status:** Phase 0A complete. Phase 0B is also complete; see README, ADR 009, ADR 010, and `TECHNICAL-DESIGN.md` for current architecture. - -**Completed:** 2026-09-01 - -## Completed statically - -- candidate discovery and triage, -- deep source/document architecture review of the three finalists, -- static privacy/network-surface review, -- preliminary licensing/reuse review, -- subsystem reuse matrix, -- preliminary fork recommendation, -- narrowed Codex validation plan. - -## Preliminary decision - -Validate **AI-DnD first as the production fork candidate**. - -Keep: -- **Open Dungeon** as the fallback fork and primary UI/media reference. -- **ai-adventure** as the state/replay/privacy architecture reference and third validation candidate. - -## Still requires local/Codex work - -- pin exact SHAs, -- clone/install/build, -- run actual tests, -- verify Ollama against the user's machine, -- runtime network capture, -- offline operation, -- AI-DnD strip-down experiment, -- Open Dungeon branch-retrofit impact experiment, -- ai-adventure Ollama/service-boundary experiment. - -See `PHASE-0B-CODEX-HANDOFF.md`. - -## Phase gate - -Do not finalize `TECHNICAL-DESIGN.md` v1.0 or production `BUILD-MILESTONES.md` until Phase 0B results are reviewed. diff --git a/planning/reports/PRIVACY-STATIC-ANALYSIS.md b/planning/reports/PRIVACY-STATIC-ANALYSIS.md deleted file mode 100644 index 4d0df2b..0000000 --- a/planning/reports/PRIVACY-STATIC-ANALYSIS.md +++ /dev/null @@ -1,122 +0,0 @@ -# Static Privacy and Network Review - -**Date:** 2026-09-01 -**Scope:** Source/config/documentation review only. Runtime capture is still required in Phase 0B. - -## Target rule - -The final v1 should be able to operate with Internet access physically blocked, with ordinary story data traveling only: - -```text -Browser -> local application -> local Ollama -``` - -Future media should similarly use explicitly configured local providers. - -## AI-DnD - -### Static positives -- documented local single-user mode, -- local SQLite, -- local Ollama support, -- no auth required in local mode, -- hosted analytics are first-party application functionality rather than a required third-party browser tracker. - -### Unwanted surfaces to remove -- OpenRouter/OpenAI/Groq/vLLM provider support, -- hosted account/guest flows, -- demo API keys, -- Render deployment, -- Neon/Postgres cloud deployment path, -- visit analytics, -- QuickJS user scripting, -- Claude CLI shim if not wanted, -- any hosted-mode rate-limit/account code that adds no local value. - -### Risk -The cloud/hosted code is explicit and documented, which is good, but Phase 0B must prove it can be removed cleanly. - -## Open Dungeon - -### Static positives -- Ollama loopback default, -- local SQLite, -- local image backend, -- no telemetry requirement apparent in inspected package/config. - -### Unwanted or optional surfaces -- OpenRouter configuration, -- arbitrary remote OpenAI-compatible endpoint support, -- Tailscale/LAN exposure options, -- any runtime remote assets, -- any model/image automatic download behavior after setup. - -### Risk -The app is smaller, so hardening may be easier, but no runtime capture has been performed. - -## ai-adventure - -### Static positives -This project most closely matches the target from the outset: -- no telemetry, -- no cloud account, -- no MCP, -- no executable plugins, -- no shell tools, -- loopback model endpoint default, -- non-loopback warning, -- imported content treated as bounded data, -- path traversal/symlink defenses documented. - -### Unwanted surface -- configurable non-loopback model endpoint should be prohibited or strongly gated in the target v1. -- LM Studio provider should be replaced/extended with Ollama. - -## Reference projects - -### Gamentic -Local defaults are strong, but the project intentionally supports cloud text/image/audio dialects as alternatives. A target fork would need those disabled. Its Docker/media stack also has setup-time model acquisition concerns separate from story-time privacy. - -### Chronicler -Supports local Ollama but also broader providers and a separate local YantrikDB/MCP memory service. More moving parts than needed. - -### Sonder / Corvus -Both support local backends but also remote provider configurations; Sonder additionally has extension/optional external-service surfaces. - -### aiMultiFool -Primarily local, but direct code reuse is constrained by GPL considerations and it is not a fork finalist. - -## Required Phase 0B runtime tests - -For each finalist: - -1. block outbound Internet access, -2. start the app, -3. create/load a story, -4. generate multiple turns, -5. trigger summarization/memory, -6. trigger embeddings where applicable, -7. save/restore/branch, -8. for Open Dungeon, generate a local image, -9. capture socket/DNS/HTTP activity, -10. fail the test if story content leaves loopback or explicitly approved LAN endpoints. - -Record: -- process, -- destination IP/hostname, -- port, -- trigger, -- payload classification, -- whether required or optional. - -## Recommended production hardening - -- bind app and Ollama to loopback by default, -- allowlist provider URLs rather than accept arbitrary URLs, -- no API-key UI in v1, -- no remote URL ingestion, -- no executable campaign scripts, -- no third-party analytics, -- bundle frontend assets locally, -- content-security policy that rejects remote scripts/styles/images by default, -- CI test or integration harness that runs with outbound networking disabled. diff --git a/planning/reports/REFERENCE-PROJECTS.md b/planning/reports/REFERENCE-PROJECTS.md deleted file mode 100644 index 7fc2c93..0000000 --- a/planning/reports/REFERENCE-PROJECTS.md +++ /dev/null @@ -1,111 +0,0 @@ -# Reference Project Findings - -**Date:** 2026-09-01 - -These projects are not recommended as primary forks after static review, but each contributes a useful architectural pattern. - -## Chronicler - -Repository: https://github.com/yantrikos/chronicler - -### Borrow -Its memory model distinguishes different trust levels rather than treating all remembered text equally. - -Useful conceptual tiers: -- durable canon, -- scene/recent memory, -- heuristic/inferred memory. - -Its anti-confabulation approach is especially relevant: retrieved hints should not automatically become established historical fact. - -### Do not necessarily adopt -The full YantrikDB/MCP cognitive-memory stack is heavier than v1 needs. Start with a simpler local store and preserve the trust-tier semantics. - -## Interactive Fiction Framework - -Repository: https://github.com/georgebutler/interactive-fiction-framework - -### Borrow -- Story Bible as highest-authority narrative context, -- application owns durable state, -- model enriches prose rather than overriding state, -- structured output validation, -- deterministic fallback, -- separation of director/planner/memory/validator. - -### Why not fork -It is designed around contributor-authored story bundles and planner-approved choices, whereas the target is more freeform collaborative fiction with branch-preserving history. - -## Gamentic - -Repository: https://github.com/hec-ovi/gamentic - -### Borrow -This is the strongest reference found for future multimodal architecture. - -It separates each modality behind a provider layer: - -```text -engine - -> text provider - -> image provider - -> audio provider -``` - -The game can continue text-first while images render asynchronously. Character image/voice identity lives in game state rather than in provider-specific code. - -It also demonstrates an unusually strong local-project test strategy with over a thousand automated tests documented across backend/frontend/services. - -### Why not fork -The core product is a multi-agent RPG with significant game mechanics and a heavy local image/voice stack. That is broader than the desired v1 storyteller. - -## Sonder Engine - -Repository: https://github.com/N0819/Sonder_Engine - -### Borrow later -- one persistent commit boundary, -- objective state distinct from character perception/belief/memory, -- retrieval scoped by what a character may legitimately know, -- model stages with different contexts. - -### Why not fork -Its defining feature is separate character minds and a multi-stage agent pipeline. That is valuable for a future sophisticated simulation but unnecessary complexity for v1. - -## Corvus Story Core - -Repository: https://github.com/JustLateNightAI/Corvus-Story-Core - -### Borrow -- hidden GM/state extraction pass, -- scene/NPC visual descriptions, -- ComfyUI scene art, -- optional TTS, -- local-first media integration. - -### Why not fork -Static review did not show the same robust branch/checkpoint/replay model; persistence is oriented around local JSON/JSONL rather than the desired transactional story graph. - -## SillyTavern / RisuAI / KoboldAI - -### Borrow -- lorebook/world-info UX, -- author's-note concepts, -- context placement and triggering, -- character/world metadata workflows. - -### Why not fork -They are mature but broad roleplay/chat ecosystems. Adapting them would mean carrying a large amount of unrelated general-purpose functionality. - -## Design consequence - -The production fork should not try to merge these projects. - -Use a primary codebase, then deliberately implement selected patterns: - -- AI-DnD: story tree, rollback, memory, prompt inspection. -- ai-adventure: authoritative event/replay/privacy discipline. -- Open Dungeon: story-focused UX and visual continuity. -- Chronicler: memory trust tiers. -- Gamentic: provider-neutral/asynchronous media. -- IFF: Story Bible authority and validation. diff --git a/planning/reports/SOURCE-INDEX.md b/planning/reports/SOURCE-INDEX.md deleted file mode 100644 index dee93ee..0000000 --- a/planning/reports/SOURCE-INDEX.md +++ /dev/null @@ -1,83 +0,0 @@ -# Phase 0 Source Index - -**Status:** Phase 0 source inventory; static links plus Phase 0B pinned finalist commits. -**Scope:** Repository/source references used during Phase 0. Runtime findings are recorded in `PHASE-0B-RECOMMENDATION.md` and supporting Phase 0B evidence. - -## Phase 0B Production Selection - -Selected production base: - -- AI-DnD -- pinned Phase 0B commit: `d72f7c1bda0f34fccd84afb7a25c34eb01c901de` -- license: MIT - -Phase 0B also evaluated: - -- Open Dungeon `b0a79f96bf852be7b4e53908dff6a7f7c179da23` -- ai-adventure `873ea9180d5b611576cddb155921fc16a17ae88b` - -The source list below records the Phase 0 research set. - -## Finalists - -### AI-DnD -- Repository: https://github.com/parththakkar106/AI-DnD -- README / architecture summary: https://github.com/parththakkar106/AI-DnD/blob/main/README.md -- Design guide: https://github.com/parththakkar106/AI-DnD/blob/main/docs/GUIDE.md -- License: MIT - -### Open Dungeon -- Repository: https://github.com/newideas99/open-dungeon -- Database layer: https://github.com/newideas99/open-dungeon/blob/main/src/lib/db.ts -- Prompt/context layer: https://github.com/newideas99/open-dungeon/blob/main/src/lib/story-prompt.ts -- Environment configuration: https://github.com/newideas99/open-dungeon/blob/main/.env.example -- Package manifest: https://github.com/newideas99/open-dungeon/blob/main/package.json -- License: MIT - -### Local Adventure Engine / ai-adventure -- Repository: https://github.com/CaoRuiming/ai-adventure -- Architecture: https://github.com/CaoRuiming/ai-adventure/blob/main/docs/architecture.md -- Privacy/security: https://github.com/CaoRuiming/ai-adventure/blob/main/docs/privacy-and-security.md -- License: Apache-2.0 - -## High-value reference projects - -### aiMultiFool -- Repository: https://github.com/omgboohoo/aimultifool -- Role: local roleplay/RAG/encryption ideas -- License: GPL-3.0 - -### Chronicler -- Repository: https://github.com/yantrikos/chronicler -- Role: memory tiers, canon/heuristic/reflex separation, anti-confabulation patterns -- License: MIT (application); YantrikDB is separately Apache-2.0 - -### Interactive Fiction Framework -- Repository: https://github.com/georgebutler/interactive-fiction-framework -- Role: Story Bible, model-as-prose-writer/application-as-state-owner, validation/fallback patterns -- License: MIT - -### Gamentic -- Repository: https://github.com/hec-ovi/gamentic -- Role: local text/image/voice provider abstraction, asynchronous media generation, large automated test suite -- License: MIT - -### Sonder Engine -- Repository: https://github.com/N0819/Sonder_Engine -- Role: objective truth vs perception/memory/belief, commit boundary, sophisticated character knowledge -- License: MIT - -### Corvus Story Core -- Repository: https://github.com/JustLateNightAI/Corvus-Story-Core -- Role: structured state + local ComfyUI/TTS integration -- License: MIT - -## Mature ecosystem references - -- SillyTavern: https://github.com/SillyTavern/SillyTavern -- RisuAI: https://github.com/kwaroran/RisuAI -- KoboldAI Client: https://github.com/KoboldAI/KoboldAI-Client - -## Important research caveat - -Repository documentation can be stale relative to current source. Phase 0B should pin exact commit SHAs at clone time, run the projects, run their tests, and verify all network behavior locally.