CLAUDE.md · diff
git:20261020.ac7123f to git:20261020.b5561d4
198 added, 522 removed. Audit A to A.
# CLAUDE.md
- Engineering law for Front Porch AI. Agents read this before writing code.
- [AGENTS.md](AGENTS.md) is a short pointer at the same law — do not let the
- two files drift.
-
- Humans: [CONTRIBUTING.md](CONTRIBUTING.md). Driving agents without reading
- Dart: [docs/maintainer-agent-playbook.md](docs/maintainer-agent-playbook.md).
- User docs live under `docs/` and are **not** agent law.
-
- Prefer short durable rules over session narration. If a number or file list
- will rot, point at the live source instead of pinning it here.
-
- ---
-
- ## Definition of done (read this first)
-
- A PR is done when a machine has checked it. People only judge looks.
-
- - **Hand-check items are tests.** Each line under `## Hand-check` in the PR
- description names the test that does it, or starts `eye-only:` (looks,
- real OS dialogs, hardware). Smoke does the `eye-only:` lines and looks at
- the changed screens; it never re-runs what a test already does.
- - **A desktop journey gets its web twin** when the feature is on both.
- - **Create / save buttons:** the test calls `doubleTapNoFrame`
- (`test/helpers/double_tap.dart`) and asserts exactly one write.
- - **Clocks:** code comparing against "now" takes it from the caller or an
- injectable clock. A test never pins a date that the code under test
- compares with the real clock (#559 went red on its own date).
- - **New screen, panel or sidebar:** check it at 400px and 1440px wide and
- in a short window. Any overflow fails. Top-level pages and the chat are
- swept by `integration_test/layout_sweep_test.dart`; give a new page a stop
- there.
- - **Rendered text** never shows `{{char}}`, `{{user}}` or a raw enum name.
- - **Never commit screenshots of real chats or user data.** Evidence images
- go in the PR thread, not the repo.
+ Engineering rules for Front Porch AI. Everything here is either enforced by
+ CI, pinned by a test, or a product contract that has already been broken
+ once by an agent that did not know it. Point at live sources rather than
+ pinning numbers that rot.
- ---
+ Humans: [CONTRIBUTING.md](CONTRIBUTING.md). Maintainer prompts:
+ [docs/maintainer-agent-playbook.md](docs/maintainer-agent-playbook.md).
+ [AGENTS.md](AGENTS.md) is a pointer at this file; keep them in agreement.
## Product
- Front Porch AI is a Flutter desktop app (Windows / Linux / macOS) for
- character chat with local LLMs (KoboldCpp) and optional remote APIs. It
- includes a Realism Engine (emotion / trust / relationship / needs), RAG
- memory via in-process ONNX embeddings, TTS/STT, Porch Stories, **The Stoop**
- (opt-in 18+ community hub), Waifu Coder (managed OpenCode), and a companion
- web/mobile UI in `web_ui/`. Automatic local backups cover the library.
-
- - **License:** AGPL-3.0-or-later (v0.9.0+). Earlier releases were GPLv3.
- - **State:** Provider for the `main.dart` graph. Riverpod only for new
- self-contained state that does not replace `ChatService`, `StorageService`,
- or that graph. There is no project to migrate the whole app to Riverpod.
- - **Database:** SQLite via Drift. Schema version lives on `AppDatabase` in
- `lib/database/database.dart` — read it, do not memorise it here.
-
- Nightly / Rawhide builds isolate data in `FrontPorchAI-Beta/` (historical
- name) with `beta_` SharedPreferences keys. Do not point them at a stable
- library.
+ Flutter desktop app (Windows, Linux, macOS) for character chat with local
+ LLMs (KoboldCpp) and remote APIs, with a companion React PWA in `web_ui/`
+ served by the app's built-in server. Realism Engine (emotion, trust,
+ relationship, needs), RAG memory via in-process ONNX, TTS/STT, Porch
+ Stories, The Stoop (opt-in 18+ hub), Waifu Coder (managed OpenCode), Living
+ Worlds (places with lore and climate). AGPL-3.0-or-later.
- ---
+ - State: Provider for the `main.dart` graph. Riverpod only for new
+ self-contained state. No project to migrate the app.
+ - Database: SQLite via Drift. `schemaVersion` lives on `AppDatabase` in
+ `lib/database/database.dart`.
+ - Nightly / Rawhide builds isolate data in `FrontPorchAI-Beta/` with
+ `beta_` preference keys. Never point them at a stable library.
## Commands
```bash
flutter pub get
- dart run build_runner build --delete-conflicting-outputs
- # After a Drift schema change (regenerates database.g.dart)
- # or after adding a @riverpod provider.
-
- flutter run
- flutter analyze
- dart format path/to/the_file_you_edited.dart
- # Only files you already edited. NEVER `dart format .`
-
- # Before a push: the checks the branch needs, chosen from what it changes
- # (analyze on changed Dart, the hygiene ratchets, the branch's own tests,
- # web lint/vitest/bundle when web_ui changed, --browser for the Playwright
- # suite). An integration of two branches runs every suite either one had.
- tools/preflight.sh
-
- # What the CI `test` job runs
- flutter test --concurrency=4 --exclude-tags golden
+ dart run build_runner build --delete-conflicting-outputs # after a Drift or @riverpod change
+ flutter analyze lib/path/you/touched.dart
+ dart format lib/path/you/touched.dart # edited files only, never a directory
+ tools/preflight.sh # the checks the branch needs, chosen from what it changes
+ flutter test --concurrency=4 --exclude-tags golden # CI `test`
flutter test test/path/to/file.dart
- flutter test --plain-name "test name"
-
- # Linux-gated pixel goldens (macOS `flutter test` never runs them)
- ./scripts/ci-local.sh # goldens in the fpai-golden image
- ./scripts/ci-local.sh test | all | update-goldens
-
- # E2E — ONE file per invocation. A directory launch starts a second app
- # while the first still holds the device.
- flutter test integration_test/app_smoke_test.dart -d linux
- # macOS: unset MallocStackLogging MallocStackLoggingNoCompact
- # MallocStackLoggingLite before E2E (even `=0` prints to stderr and
- # corrupts Flutter's isolate JSON).
-
- cd web_ui && npm ci
- cd web_ui && npm run lint && npm test # CI `web-tests` (tsc + react-hooks eslint + vitest)
- # Web UI in real browsers against the booted app — CI `web-e2e`
- # (once: cd web_ui && npm ci && npx playwright install chromium webkit)
- flutter test integration_test/web_ui/browser_test.dart -d macos
- # FPAI_E2E_HOLD=1 on that command keeps the sandboxed app serving; then
- # `cd web_ui && npm run e2e` with the env in web_ui/e2e/.auth/server.json
- cd web_ui && npm ci && npm run build # writes ../assets/web_app. The bundle
- # is built, not tracked: once after a
- # clone and after ANY web_ui change,
- # or the desktop app serves nothing
- # (CI, the nightly and the release
- # build it themselves)
+ ./scripts/ci-local.sh test | all | update-goldens # Linux-gated pixel goldens
- flutter build linux
- flutter build windows
- ./scripts/build-macos.sh
+ flutter test integration_test/app_smoke_test.dart -d linux # E2E: ONE file per run
+ cd web_ui && npm ci && npm run lint && npm test # CI `web-tests`
+ cd web_ui && npm run build # writes ../assets/web_app; required after any web_ui change
```
- CI Flutter is **3.47.7**. Dart SDK constraint is `^3.10.8`.
-
- ---
+ CI Flutter version: `.github/workflows/ci.yml` `flutter-version`. Dart SDK
+ constraint: `pubspec.yaml`. macOS E2E: unset the `MallocStackLogging*`
+ variables first or Flutter's isolate JSON is corrupted.
- ## Layout (find code here; do not trust counts)
+ ## Layout
```
- lib/
- main.dart # Startup shell; bodies in main.*.dart parts
- database/ # Drift shell + parts. NO database/migrations/
- models/
- providers/app_state.dart
- services/
- chat_service.dart # Hub SHELL: fields, fake-pinned forwarders
- chat/ # Leaves + chat_service_*.dart parts
- backporch/ # The Stoop Dart client
- web/ # Built-in server, facades, /api/stoop relay
- story/ image/ grpc/
- services.dart # Curated barrel — does NOT re-export chat/
- ui/
- pages/ settings/ dialogs/ widgets/
- chat_components/ layout/ theme/app_colors.dart
- utils/
- web_ui/ # React PWA; desktop parity is mandatory
- integration_test/ # One suite per *_test.dart file
+ lib/main.dart startup shell (legacy main.*.dart parts)
+ lib/database/ Drift shell + parts; no migrations/ directory
+ lib/models/ lib/providers/ lib/utils/
+ lib/services/chat_service.dart hub shell: fields and one-line forwarders
+ lib/services/chat/ ChatService parts + leaf classes; grep here for behaviour
+ lib/services/backporch/ The Stoop client
+ lib/services/web/ built-in server, facades, /api/stoop relay
+ lib/services/world_from_wiki/ story/ image/ grpc/
+ lib/ui/pages/ settings/ dialogs/ widgets/ chat_components/ layout/ theme/
+ web_ui/ React PWA; desktop parity is mandatory
+ integration_test/ one suite per *_test.dart file
+ test/hygiene/ the ratchets listed under CI
+ docs/design/ design docs; user docs under docs/ are not rules
```
- **ChatService is a `part` library.** Behaviour lives under
- `lib/services/chat/`. Grep that directory when hunting a method. Its part
- list is frozen (see Size law): new ChatService code goes into an existing
- part or a leaf class under `lib/services/chat/`, never a new part.
-
- Fake-pinned members stay on the **class** as one-line forwarders. Golden
- fakes override the class member; moving the body onto an extension-only
- name breaks their dispatch.
+ Barrels: if a directory has `<dir>/<dir>.dart`, import it. Never export a
+ `chat_service_*.dart` part from `services/chat/chat.dart`. Do not run mass
+ import rewrites.
- Hunt current barrels with:
+ `ChatService` is a `part` library. Members that golden fakes override stay
+ on the class as forwarders; new behaviour goes in an existing part or a
+ leaf class, never a new part.
- ```bash
- find lib -name '*.dart' | awk -F/ '$NF==$(NF-1)".dart"'
- ```
+ ## What CI enforces
- High-frequency ones: `models/models.dart`, `utils/utils.dart`,
- `services/services.dart`, `services/chat/chat.dart` (leaves only — never
- export `chat_service_*.dart` parts), `ui/widgets/widgets.dart`,
- `ui/chat_components/chat_components.dart`, `ui/dialogs/dialogs.dart`,
- `ui/pages/pages.dart`. If a barrel covers a file, import the barrel. If you
- import 2+ siblings from an un-barrelled directory, add a barrel in the same
- change. `show` / `hide` is not an exemption unless the barrel reintroduces a
- real collision (`database.dart hide World` is that case).
+ | Check | Fails when | Escape hatch |
+ |-------|-----------|--------------|
+ | `analyze` | analyzer issues in changed `.dart` files | none |
+ | `test` | unit/widget tests | none |
+ | `e2e-smoke` | any `integration_test/*_test.dart` suite | none; Linux on PRs, all OSes on Rawhide/main pushes |
+ | `web-tests` | `npm run lint` or `npm test` in `web_ui/` | none |
+ | `web-e2e` | Playwright sweep and journeys against the real app | none |
+ | `golden` | pixel drift (Linux image) | `update-goldens` with the label below |
+ | `theme-lint` | new raw `Colors.*` under `lib/` | `// theme-keep: <reason>` for a maintainer-approved semantic hue |
+ | `io-lint` | new sync I/O under `lib/ui/` | `// io-ok: <reason>` |
+ | `god_file_ratchet_test` | handwritten `lib/` Dart at or over 500 lines; `test/baselines/god_files.json` is empty and stays empty | split the file |
+ | `part_file_ratchet_test` | a library gains a `part` | extract a class instead |
+ | `generated_dart_size_test` | `*.g.dart` over its recorded ceiling; Drift managers on | none; `build.yaml` keeps `generate_manager: false` |
+ | `raw_colors_ratchet_test`, `raw_dialogs_ratchet_test` | a file gains a raw colour or a dialog opened outside `showWarmDialog`; a new file must have none | none |
+ | `needs_gate_ratchet_test`, `clock_*_ratchet_test`, `picker_calls_guarded_test`, `settings_parity_manifest_test` | the gates in **Chat contracts** are bypassed; a setting lacks its web twin | none |
+ | PR rules | a UI PR's `## Hand-check` lines do not each name a test or start `eye-only:`; images added outside goldens, `assets/`, `docs/screenshots/` | `approved-images` label |
+ | Test integrity | an existing test, golden, baseline, dependency floor, `.github/`, `scripts/ci-local.sh` or `analysis_options.yaml` is modified or deleted | maintainer applies `approved-test-change` |
- Do **not** run a mass import rewrite. Leave every file you touch on barrels
- and tall style.
+ Adding a new test file, or appending whole `test(` / `group(` calls and
+ imports to an existing one, never blocks. Touching a helper, fake or
+ `setUp` that old tests use does. The dependency-floor test fails when
+ `pubspec.lock` moves a package backwards; do not regenerate the floor file.
- ---
+ Draft PRs run only the cheap jobs. Open PRs as drafts; the merge queue
+ takes them out. `full-lint` is informational. Direct PRs to `main` are
+ not accepted; work lands on `Rawhide`. `schedule:` workflows run from the
+ default branch, and `test-integrity.yml` runs the base branch's copy.
## Size law
- | Rule | Enforcement |
- |------|-------------|
- | **No handwritten `lib/` Dart may reach 500 lines** | CI: `test/hygiene/god_file_ratchet_test.dart` + empty `test/baselines/god_files.json`. `kGodFileBar` is 500. Adding a baseline entry needs `approved-test-change` and should not happen — split the file instead. |
- | New / extracted Dart stays **under 500 lines** | Same gate. Extract; do not grow. |
- | Generated `*.g.dart` stays under recorded ceilings; Drift table managers stay **off** | CI: `test/hygiene/generated_dart_size_test.dart`. `build.yaml` must keep `generate_manager: false`. `lib/database/database.g.dart` is excluded from the handwritten ratchet and owned here. |
-
- Do not invent a second 500-line gate. Generated protobuf / `.g.dart` /
- `grpc/generated` are excluded from `god_file_ratchet_test.dart` (generated
- size is the other test).
-
- **No mechanical part files.** Never split a file into `part` files to get
- under 500 lines. Extract a real class, widget, or plain-Dart helper with
- explicit inputs instead. No new `part` directive anywhere. The Drift library
- (`lib/database/database.dart`) and the ChatService shell
- (`lib/services/chat_service.dart` + `lib/services/chat/chat_service_*.dart`)
- keep their existing parts; every other part file (`main.*.dart` included) is
- legacy and gets unwound. Every library's part count is frozen and only goes
- down.
- `part 'x.g.dart';` codegen is not a split. CI:
- `test/hygiene/part_file_ratchet_test.dart` + `test/baselines/part_files.json`
- — lower an entry in the same change that removes a part.
-
- ---
+ Handwritten `lib/` Dart stays under 500 lines, including new files. Get
+ under by extracting a real class, widget or plain-Dart helper with explicit
+ inputs, never by adding a `part`. The Drift library and the ChatService
+ shell keep their existing parts; every other part file is legacy and the
+ count only goes down.
## Style
- - DRY, readable Dart. One class per file except small related types.
- - **Limited comments.** Explain non-obvious contracts, not the next line.
- - `dart format` per edited file. Never `dart format .`, never a directory,
- never an existing test you were not already changing.
- - After a wrap, fix lints the wrap created (`if (x) return;` split trips
- `curly_braces_in_flow_control_structures`).
- - Touched Dart: **zero** analyzer warnings. `flutter analyze` on those paths.
- - Do not silently swallow errors. Log or surface them.
- - Do not hardcode Unix paths. Use `Directory.systemTemp`,
- `StorageService.rootPath`, or `package:path`.
- - Sync I/O (`existsSync`, `readAs*Sync`, `Process.runSync`, …) is banned in
- widget `build` / per-frame paths. CI `io-lint` fails **new** sync I/O under
- `lib/ui/**` unless the line has `// io-ok: <reason>`.
- - UI uses `AppColors` only, through the `*Of(context)` helpers that read
- the app theme (`AppPalette`). New chrome accents:
- `AppColors.formAccentOf(context)` or `AppColors.porchAmberOf(context)`;
- `AppColors.onAccentOf(context)` on a solid accent fill. A bare palette
- constant (`formMasterAccent`, `onChaosAccent`, …) in UI code stays Porch
- under every theme. Status hues (danger, warning, success, info) come from
- `StatusColors`, dims and faint lines from `InkColors`, the chips under a
- reply from `RealismColors`, chat-only hues from `ChatColors`, sidebar
- destinations from `NavColors`, model quant and fit hues from
- `ModelColors`, a page's feature hues (Director amber, Realism teal)
- from `AccentColors`, all in `lib/ui/theme/`. CI `theme-lint` fails new
- `Colors.blueAccent` under `lib/` unless `// theme-keep: <reason>` and the
- colour is a genuine semantic status hue the maintainer approved **in this
- conversation**. Two ratchets in `test/hygiene/` (`raw_colors_ratchet_test`,
- `raw_dialogs_ratchet_test`, baselines in `test/baselines/`) fail any file
- that gains a raw colour or a dialog opened outside `showWarmDialog`; a new
- file must have none.
- - Creation wizards follow `create_character_page.dart`: AppBar step dots,
- linear `_currentStep`, bottom nav. No side-menu wizards.
- - `GlobalKey`s are owner-scoped. Never `GlobalObjectKey(model)` — two live
- routes showing the same object crash. The owner holds an identity map.
-
- ---
-
- ## Chat contracts (do not “simplify”)
-
- These are live product behaviour. Tests pin them. Do not collapse the cases.
-
- **Mouth speech** — `lib/utils/think_tags.dart` `resolveMouthSpeech`:
-
- - Closed think-only (`<think>…</think>` and nothing visible) **lifts** so
- the bubble speaks.
- - Unclosed cut-off (stream still inside `<think>`) stays tagged. Salvage the
- closer; do not promote. Thought chip stays; display stays empty.
-
- **Continue** is its own product, not regen-lite:
-
- - Does **not** tick the story clock.
- - Post-gen scores the **new** text only (`newPart`), not the whole reply
- (that would double-apply).
- - `_runPocketsPass(asContinuation: true)` **keeps** the message's original
- `pockets_before` (unions new transfer recipients, appends receipts) so
- regen / tail-delete rewind to the turn base.
- - Finalize keeps pre-continue + new body. Partial / history builders use
- think-stripped, history-safe text.
-
- **Pockets hide, they do not erase.** `pocketsFor` returns `null` when the
- feature is off (no eval, no injection, no sidebar panel). Stored records and
- the session save/load wire stay as they were. Switching the feature back on
- finds the kit still there.
-
- **`objectivesActive` is a live AND** — per-chat `_objectivesEnabled` AND
- `realismSettings.objectivesEnabled`. Not a stored AND at seed time. Flipping
- the global off must take effect on the next turn.
-
- **Needs run behind one gate.** `NeedsPass._needsActive`, read as
- `NeedsPass.active` (`lib/services/chat/needs_pass.dart`) = Realism engine AND
- the chat's Needs switch AND the global `needsSimDefault` read live. Every
- run (stamp, prompt lines, judge, clock wear, chip, Reprocess) reads it;
- the stored `_needsSimEnabled` is only for seeding, saving and rewinding
- state. `test/hygiene/needs_gate_ratchet_test.dart` enforces it.
-
- **Clock.** Passage of time needs a **model call**, not the Realism Engine.
- `_clockRunning` is Porch Life `passageOfTimeDefault` **live** — the same
- row Settings → Porch Life shows. Card veto and leftover per-chat
- `passageOfTimeEnabled` are not a gate. Regen/swipe rewind from
- `story_clock_before`. Continue does not tick. One-shot must not apply
- minutes (would double).
-
- **Evals score the user's message**, never the character's own reply. They
- fire before generation. A regen with the same inputs must reproduce the same
- deltas; disagreement is a rewind bug. Non-scalar eval inputs must join
- `captureCadenceAndFeelings` / `restoreFromMessageState`.
-
- **Eval transport.** Structured evals try native tool calls first
- (`fireStructuredEval`), then the regex/JSON text floor. Do **not** put GBNF
- on the JSON floor — it produced empty Kobold replies. The three
- prefix-sharing judges must send the **same** `kJudgeEvalTools` list (named
- `tool_choice` selects the function). Per-eval one-tool lists defeat the
- Kobold jinja KV-cache. Director / character-creator / story prose stay
- text-only. Detail: `docs/design/tools-transport.md`.
-
- **One-shot** is a tri-state (`auto` / `on` / `off`). When active it must
- match the multi-call path for observable Realism/Needs. Local backends stay
- multi-call in `auto`.
-
- **Posture, climax, pockets** are post-generation. When two or more of those
- passes are live, one fused reply-facts call is transport only — each feature
- keeps its own gate.
-
- **Chaos** is not a Realism dependency. It has a per-chat switch and a global
- default (`chaosModeDefault`, OR-override, default false). Every conversation
- **start** path must seed that global or the switch is silently missing on
- that path. Grep `seedFromGroupOrExt` / `chaosModeDefault` rather than
- trusting a site count.
-
- **Growth Rings** are per-chat, per-character character evolution.
+ - `dart format` only the files you edited. Fix lints a wrap creates.
+ - Zero analyzer warnings on touched Dart.
+ - Do not swallow errors silently. Log or surface them.
+ - No hardcoded Unix paths: `Directory.systemTemp`, `StorageService.rootPath`,
+ or `package:path`.
+ - No sync I/O in widget `build` or per-frame paths.
+ - Colours come from `lib/ui/theme/`: `AppColors.*Of(context)` for chrome,
+ `StatusColors`, `InkColors`, `RealismColors`, `ChatColors`, `NavColors`,
+ `ModelColors`, `AccentColors` for their domains. A bare palette constant in
+ UI code stays Porch under every theme.
+ - Dialogs open through `showWarmDialog`.
+ - Creation wizards follow `create_character_page.dart`: step dots, linear
+ `_currentStep`, bottom nav.
+ - `GlobalKey`s are owner-scoped. Never `GlobalObjectKey(model)`.
+ - Rendered text never shows `{{char}}`, `{{user}}` or a raw enum name.
+ - New screens, panels and sidebars work at 400 px and 1440 px wide and in a
+ short window. Top-level pages get a stop in
+ `integration_test/layout_sweep_test.dart`.
+ - Comments explain a non-obvious contract, not the next line.
- **Journal** is per-chat, per-character. Cards never cross chats. Item cards
- are written from applied pocket events when **both** pockets and journal
- are on. Review-first parks a batch; Apply uses the same applier as auto.
+ ## Chat contracts
- **Group restore** keys by **message speaker id**, not whoever is active
- after post-gen.
+ Live product behaviour, each pinned by a test. Do not collapse the cases.
- ---
+ | Contract | Rule | Where |
+ |----------|------|-------|
+ | Mouth speech | Closed think-only body lifts and is spoken; an unclosed cut-off stays tagged, display empty | `lib/utils/think_tags.dart` `resolveMouthSpeech` |
+ | Continue | Not regen. Does not tick the clock, scores only `newPart`, keeps the turn's `pockets_before`, does not consume one-shots, finalizes pre-continue + new body | `chat_service_generation_postgen_engine.dart` |
+ | Pockets | Hide, never erase: `pocketsFor` is null when off; stored records and the save wire are untouched | `pockets_rewind_test` |
+ | Objectives | `objectivesActive` is a live AND of the per-chat switch and `realismSettings.objectivesEnabled`, read every turn | `chat_service_accessors.dart` |
+ | Needs | One gate, `NeedsPass.active` = engine AND chat switch AND global default, read live by every pass; the stored flag only seeds and rewinds | `needs_gate_ratchet_test` |
+ | Clock | Passage of time needs a model call. `_clockRunning` is Porch Life `passageOfTimeDefault` live. Regen rewinds from `story_clock_before`. One-shot must not apply minutes twice | `clock_*_ratchet_test`, `scene_time_evaluator.dart` |
+ | Evals | Score the user's message before generation, never the reply. Regen with the same inputs reproduces the same deltas; non-scalar inputs join `captureCadenceAndFeelings` / `restoreFromMessageState` | `regen_feelings_cadence_test` |
+ | Eval transport | Native tool call first, then the regex/JSON text floor. No GBNF on the floor. The prefix-sharing judges send the same `kJudgeEvalTools` list | `docs/design/tools-transport.md` |
+ | One-shot | Tri-state auto/on/off; when active it matches the multi-call path; local backends stay multi-call in auto | `one_shot_parity_test` |
+ | Post-gen passes | Posture, climax, pockets are post-generation; a fused reply-facts call is transport only, each keeps its own gate | `chat_service_generation_postgen_engine.dart` |
+ | Chaos | Per-chat switch plus `chaosModeDefault` OR-override; every conversation start path seeds it (`grep seedFromGroupOrExt`) | `chaos_mode_service.dart` |
+ | Journal / Growth | Per chat, per character; cards never cross chats; a Journal rewrite rule applies to Growth too | `swipe_buried_journal_test` |
+ | Group restore | Keys by message speaker id, never whoever is active after post-gen; `_saveScalarsIntoGroupRealism` persists group state | `chat_service_group_entry.dart` |
+ | Living Worlds | Setting slot owns weather and prose; Lore slots add books only; lore enumerates once per world id | `chat_places_primary_lore_test` |
## Path-complete chat work
- Mandatory for generation, Continue, regen/swipe/delete/edit, Realism, Needs,
- Journal, Growth, Pockets, RAG, or group orchestration.
-
- Fill the matrices in
- [docs/design/path-complete-chat-work.md](docs/design/path-complete-chat-work.md)
- in the completion summary (or mark N/A with one line why).
-
- Sibling-path law: fixing history think-strip without Continue, Journal
- rewrite without Growth, or 1:1 pockets without group speaker restore is
- incomplete. Grep the twin.
-
- ---
-
- ## Parity laws
-
- **Web ↔ desktop.** Everything the user can see, tap, or configure on
- desktop ships in `web_ui/` in the same work — theming, settings/toggles,
- dialogs, sidebars, buttons, indicators. Adaptation to phone vs wide is
- expected (separate shells if UX needs it). Omission is not. Silent “web
- later” is a violation unless the maintainer deferred **that item** in the
- **current** conversation.
-
- A Stoop endpoint or field needs three edits: Dart client
- (`lib/services/backporch/`), relay (`lib/services/web/` routes + facade),
- and `web_ui/src/stoop/`. The PWA talks to the Dart `/api/stoop/*` relay, not
- the backend directly.
-
- **Realism / Needs 1:1 ↔ group.** Observable results for a character must
- match. Storage may be scalars vs `_groupRealism`. Do not add a second
- simulation. After post-gen, `_saveScalarsIntoGroupRealism` is what makes
- group state persist.
+ Any change to generation, Continue, regen/swipe/delete/edit, Realism,
+ Needs, Journal, Growth, Pockets, RAG, Living Worlds or group orchestration
+ covers every sibling path or says which it skipped and why. Load the
+ `path-complete-chat` skill; the matrix is
+ [docs/design/path-complete-chat-work.md](docs/design/path-complete-chat-work.md).
+ The usual misses: Continue without regen, Journal without Growth, 1:1
+ without group speaker restore, desktop without web.
- Presentation may duplicate (desktop vs phone CSS). Engine logic may not.
+ ## Parity
- ---
+ - **Desktop ↔ web.** Anything the user can see, tap or configure on
+ desktop ships in `web_ui/` in the same change. Phone vs wide adaptation is
+ fine; omission is not, unless the maintainer deferred that item in the
+ current conversation. `settings_parity_manifest_test` checks settings.
+ - **1:1 ↔ group.** A character's observable Realism and Needs results match
+ in both modes. Storage may differ; the simulation may not.
+ - **Stoop.** An endpoint or field is three edits: Dart client
+ (`lib/services/backporch/`), relay (`lib/services/web/` routes + facade),
+ `web_ui/src/stoop/`. The PWA talks to the relay, never the backend.
## Database
- Drift library: `lib/database/database.dart` (annotation, ctor,
- `schemaVersion`, migration stub) plus parts for tables, the onUpgrade
- ladder, repair, and queries. Table **declaration order is load-bearing** for
- codegen. After a schema or layout change, regenerate
- `dart run build_runner build` and keep `database.g.dart` honest — a
- file-move that changes declaration order is a real diff, not noise.
-
- Migrations are additive. A mistake in the ladder corrupts user libraries.
- Breaking changes (removed/renamed columns) need maintainer confirmation
- before you start.
-
- **Identity:** `objectives`, `message_embeddings`, and `data_bank_entries`
- key `character_id` by **`stableGroupId`** (portable image-filename
- basename), not `characters.id`. `avatar_images` uses the UUID. Joining the
- former against the UUID matches nothing and marks real rows as orphans.
- Resolve via `stableGroupIdFrom()` in `lib/utils/character_id.dart`.
-
- ---
+ - Migrations are additive and go in the onUpgrade ladder parts. Table
+ declaration order is load-bearing for codegen; regenerate after any
+ change and commit the honest `database.g.dart` diff.
+ - Removing or renaming a column needs maintainer confirmation first.
+ - `objectives`, `message_embeddings` and `data_bank_entries` key
+ `character_id` by `stableGroupId` (image-filename basename), not
+ `characters.id`; `avatar_images` uses the UUID. Resolve through
+ `stableGroupIdFrom()` in `lib/utils/character_id.dart`.
## The Stoop
- Opt-in, account-gated, 18+ hub. Dart client in `lib/services/backporch/`.
- Backend source and hosting are **not in this repo**. Do not add hosts, IPs,
- buckets, or credentials.
-
- The live fleet is a mix of app versions. Responses are additive-only. Never
- tighten a request the old app still sends. Breaking changes are new
- endpoints; keep the old one until the fleet ages out. Parse defensively.
-
- ---
+ Backend and hosting are not in this repo. Do not add hosts, IPs, buckets or
+ credentials. The fleet runs mixed app versions: responses are additive only,
+ never tighten a request the old app sends, breaking changes are new
+ endpoints, parse defensively.
## Engines
- No Python, no Rust, no helper processes. See
- `docs/design/sidecar-retirement.md`.
-
- Waifu Coder may download a pinned GitHub OpenCode zip into the app-support
- closet and run `opencode serve` on 127.0.0.1. That is process management,
- like Kobold. Do not reintroduce an in-process coding loop. Do not use the
- user's Homebrew OpenCode or `~/.config/opencode` as the product copy.
-
- ---
-
- ## Branches and CI
-
- | Change | Branch |
- |--------|--------|
- | Features, fixes, experiments | `Rawhide` |
- | Tagged stable releases | `main` |
-
- Work lands on `Rawhide`. Direct PRs to `main` are almost never accepted.
-
- GitHub `schedule:` triggers run from the **default** branch. A change to
- `nightly.yml` must land on `main` or nightlies keep the old file.
- `test-integrity.yml` is `pull_request_target` — it runs the **base**
- branch's copy.
-
- **CI jobs that actually fail a PR** (`.github/workflows/ci.yml`):
-
- | Job | Fails when |
- |-----|------------|
- | `analyze` | Analyzer issues in **changed** `.dart` files |
- | `test` | Unit/widget tests (`--concurrency=4 --exclude-tags golden`) |
- | `e2e-smoke` | A suite under `integration_test/*_test.dart` fails (one process per file, 5 shards; Linux on PRs, macOS + Windows on Rawhide/main pushes, all three on the schedule) |
- | `web-tests` | `npm run lint` / `npm test` in `web_ui/` |
- | `web-e2e` | Playwright sweep + journeys against the real app (`integration_test/web_ui/browser_test.dart`); also runs on Rawhide/main pushes |
- | `theme-lint` | New raw `Colors.blueAccent` under `lib/` |
- | `io-lint` | New sync I/O under `lib/ui/` |
- | `golden` | Pixel golden drift (Linux image) |
- | `PR rules` (`pr-rules.yml`) | A UI PR's `## Hand-check` lines don't each name a test or start `eye-only:`; a PR adds pictures outside goldens, `assets/`, `docs/screenshots/` (label `approved-images`) |
-
- A **draft** PR runs only the cheap jobs; `e2e-smoke`, `web-e2e`, `golden`
- and `stoop-live` start when it leaves draft, and never run on a docs-only
- PR. Open PRs as drafts; the merge queue takes them out of draft in order.
-
- `full-lint` is scheduled / branch-push and is **not** a hard PR gate (it
- ends `|| true`). Do not tell contributors the whole tree is analyzed on
- every PR.
-
- **Test integrity** (`.github/workflows/test-integrity.yml`): modifying or
- deleting an existing test, golden, baseline, `test/deps/dependency_floors.json`,
- anything under `.github/`, `scripts/ci-local.sh`, or `analysis_options.yaml`
- fails until a maintainer applies **`approved-test-change`**. Adding a **new**
- test file never blocks, and neither does an edit to an existing `_test.dart`
- that deletes nothing and only adds whole `test(` / `testWidgets(` / `group(`
- calls (and imports). Touching a helper, fake or `setUp` the old tests use
- still needs the label, and so does any edit to a golden test file or one that
- mentions `autoUpdateGoldenFiles`, `goldenFileComparator` or `HttpOverrides`. `integration_test/support/` (harness, no asserts) is
- reported, not blocked.
-
- The dependency-floor test fails if `pubspec.lock` moves a package
- **backwards**. Do not “fix” that by regenerating
- `test/deps/dependency_floors.json`.
-
- Do not edit `pubspec.yaml` version — CI/CD normalizes releases.
-
- ---
-
- ## Testing
-
- - A new guard must be **proven red** (break the fix, see fail) then green.
- If deleting the product call site still leaves the test green, it is
- decoration — say so or fix the test. Do not ship that test.
- - A new guard goes in the **existing** test file for its feature (append a
- `test(` or `group(`). Start a new file only for a feature that has none.
- Every test file is its own compile and launch in CI, and one-bug files
- doubled the suite's file count in three weeks.
- - Prefer one broad interaction / E2E journey over another pure unit of a
- helper. Goldens answer “does it look right”; a tap answers “can a user do
- this.”
- - Web UI: a new screen is swept automatically once it has a route in
- `web_ui/e2e/sweep.spec.ts` `ROUTES`; a new user flow gets a journey in
- `web_ui/e2e/journeys.spec.ts`. The maintainer does not hand-test the PWA.
- - Inventory of E2E suites: [docs/design/e2e-coverage-inventory.md](docs/design/e2e-coverage-inventory.md).
- Before persist-asserting chat state: `await d.waitSendable()`
- (`isSettlingTurn` is part of the turn).
- - Changing an existing test needs a written rationale: what behaviour
- changed, why the old assertion is now false. If the test was right, fix
- the change.
-
- ### No stub tests
-
- A pin that plants the answer is not a pin. **Forbidden:**
-
- - flutter_test / `HttpOverrides` stub clients that return canned 400/200
- without a real listening server
- - Mockito / mocktail HTTP or API fakes that hand the suite its JSON
- - Hand-rolled “fake success” maps that never exercise the real client stack
- - Stub LLMs that return the expected string so the test cannot fail
- - `FakeStoopServer`, `integration_test/support/fake_stoop.dart`, or an
- inline `HttpServer.bind` that invents JSON behind
- `BackporchApi.overrideBaseUrl`
-
- Those stay in old E2E journeys that already use them. **New pins of Stoop
- / Backporch behaviour must not add more.**
-
- Required for Stoop / Backporch / other network features:
-
- - Talk HTTP to a **real** Stoop/backporch server — the live hub
- (`https://api.frontporchai.app`) or the team’s real droplet/API.
- - Not a toy. Not `overrideBaseUrl` at loopback JSON.
-
- If CI has no credentials or network, tag the test `live` / `stoop_live`
- and **skip only when the env is absent**. Do not replace it with a stub.
-
- Runtime env (`Platform.environment`, not dart-define):
-
- - `STOOP_LIVE_URL` — optional. Defaults to `https://api.frontporchai.app`.
- - `STOOP_TEST_EMAIL` — required to run.
- - `STOOP_TEST_PASSWORD` — required to run.
- - `STOOP_TEST_TOTP` — optional, if that account has 2FA.
-
- ```
- STOOP_TEST_EMAIL=… STOOP_TEST_PASSWORD=… flutter test --tags stoop_live
- ```
-
- CI job `stoop-live` in `.github/workflows/ci.yml` injects those env vars
- from repo secrets on same-repo PRs and Rawhide/main pushes. Fork PRs skip
- the job. Do not print the secrets. Local/CI without the vars skips. That
- is a skip, not a pass-by-stub.
+ No Python, no Rust, no helper processes. TTS, STT, embeddings and
+ expression run in-process. Waifu Coder may download a pinned
+ OpenCode zip and run `opencode serve` on loopback; that is process
+ management like Kobold. No in-process coding loop, and never the user's own
+ OpenCode install or config.
- Model download pins: real `file://` or real HTTP of a tiny fixture the
- test controls (a real listening server serving a real file), or tag
- `live`. No 400-stub theater.
+ ## Tests
- ---
+ - A new guard goes in its feature's existing test file. New files only for
+ a feature that has none; each file is its own compile in CI.
+ - A new test must fail with the product change reverted. A test that stays
+ green without the fix pins nothing; do not ship it.
+ - Prefer one interaction or E2E journey over another unit of a helper.
+ - Clocks: code that compares against "now" takes it from the caller or an
+ injectable clock. Tests never pin a date the code compares with the real
+ clock.
+ - Create and save buttons: the test calls `doubleTapNoFrame`
+ (`test/helpers/double_tap.dart`) and asserts exactly one write.
+ - Web: a new screen gets a route in `web_ui/e2e/sweep.spec.ts`; a new flow
+ gets a journey in `web_ui/e2e/journeys.spec.ts`. Before asserting
+ persisted chat state in E2E: `await d.waitSendable()`.
+ - Changing an existing test needs a written reason: what behaviour changed
+ and why the old assertion is now false.
+ - **No stub network tests.** Stoop, Backporch and model-download pins talk
+ to a real server or skip when the env is absent. No `FakeStoopServer`,
+ loopback JSON behind `overrideBaseUrl`, canned `HttpOverrides`, or LLM
+ fakes that return the expected string. Live pins are tagged `stoop_live`
+ and read `STOOP_TEST_EMAIL` / `STOOP_TEST_PASSWORD` (optional
+ `STOOP_LIVE_URL`, `STOOP_TEST_TOTP`) from `Platform.environment`; CI
+ `stoop-live` injects them on same-repo PRs. Never print them.
+ - Inventory of E2E suites: `docs/design/e2e-coverage-inventory.md`.
## Public text
- No personal names in user-facing copy, `docs/Rawhide.md`, `docs/main.md`,
- release notes, or other text that ships to users.
-
- User-facing “What’s New”:
-
- - Nightly / Rawhide → `docs/Rawhide.md`
- - Stable dialog → `docs/main.md`
- - Long-form history → `docs/release-notes.md`
-
- Never paste commit messages or agent session logs into those files.
-
- Do **not** append to `.claude/changelog.md`. That file is a pointer, not a
- progress diary. `git log` is the history.
-
- ---
-
- ## Files that need a reason before you edit them
-
- - `lib/database/database.dart` and its migration parts — plan first.
- - `lib/main.dart` — init order is delicate.
- - `pubspec.yaml` — not for version bumps; new deps need a license check.
- - `analysis_options.yaml` — lint set.
- - `scripts/` — release/build. `scripts/ci-local.sh` is also test-integrity
- protected because it must match the CI golden/test jobs.
-
- ---
-
- ## When the maintainer cannot read Dart
-
- Green analyze + green tests are not a second look. Grep twins (1:1 ↔ group,
- Continue ↔ regen, Journal ↔ Growth, desktop ↔ web/relay). Prove one new
- guard red, then green — or say you could not.
-
- For chat/realism/memory work, fill the path-complete checklist in
- [docs/design/path-complete-chat-work.md](docs/design/path-complete-chat-work.md)
- (or mark N/A with one line why).
-
- A remote sandbox cannot self-certify a UI change. Say so. End with a 2–5
- step poke script.
+ No personal names in anything that ships to users. What's New: nightly in
+ `docs/Rawhide.md`, stable in `docs/main.md`, long form in
+ `docs/release-notes.md`. Never paste commit messages or session logs there.
+ `.claude/changelog.md` is a pointer, not a diary. Never commit screenshots
+ of real chats or user data; evidence goes in the PR thread.
- If you cannot finish the request without leaving the tree broken, stop and
- say so. No stubs, no “skeleton for later.”
+ ## Files that need a reason
- ---
+ `lib/database/database.dart` and its migration parts, `lib/main.dart`
+ (init order), `pubspec.yaml` (no version bumps; new deps need a licence
+ check), `analysis_options.yaml`, `scripts/`, `nightly.yml` (must land on
+ `main` to take effect).
## Git
- - Conventional commit first line, then **why** it mattered.
+ - Conventional commit first line, then why it mattered.
- Never amend or rewrite another author's commits.
- - Never `git checkout -- <file>`, `git restore <file>`, or
- `git checkout HEAD -- <file>` without explicit approval in this
- conversation. Those destroy uncommitted work.
+ - Never `git checkout --`, `git restore`, or `git checkout HEAD --` on a
+ file without approval in the current conversation.
- ## Community
+ ## Reporting
- Discord: https://discord.gg/e4tET6rpdv
+ The maintainer does not read Dart and does not hand-test the PWA. A
+ completion summary states: what was verified by which test, what was not
+ verified and why, the path-complete matrix when chat state is in scope, and
+ a 2 to 5 step poke script for anything a sandbox cannot certify (looks, OS
+ dialogs, a running model). A remote sandbox cannot self-certify a UI change;
+ say so. If the request cannot be finished without leaving the tree broken,
+ stop and say so; no stubs or skeletons.