diff --git a/tooling/claude/dot-claude/instructions/code-layering.md b/tooling/claude/dot-claude/instructions/code-layering.md index 0bc7f755..bd4423f8 100644 --- a/tooling/claude/dot-claude/instructions/code-layering.md +++ b/tooling/claude/dot-claude/instructions/code-layering.md @@ -70,15 +70,39 @@ the *data it indexes* already is; the index itself is correctly code. 3. **Bench logic ≠ crate logic.** When the headless bench (`mc-turn`) needs a number, it must call the same crate the live game does — not grow its own bench-grade copy. Three copies of a formula (GDScript, `mc-turn`, the crate) is the worst case. +4. **Fixing the orchestrator instead of eliminating it.** When you find logic in GDScript, the + reflex is "make it call Rust correctly" (extract a formula, add an FFI the GDScript calls). + That keeps GDScript *calling logic* — still the disease. The real fix is usually to **delete** + the GDScript path: the Rust turn computes it, the UI renders the result from `getState()`. + (This session drifted swap → logic-extraction → per-formula-FFI before landing on "delete it.") +5. **The UI holding state / calling logic instead of reading `getState()`.** Presentation that + reads authoritative entities (`CityScript`/`Player`) and orchestrates phases is the bug. The + UI is a pure view: render `view_json`/`getState()`, send input via `act()`, run the turn via + `end_turn()`. The headless `GdPlayerApi` already embodies this — the live game converges to it. --- -## The principle behind all of it +## The principles behind all of it -**Two state representations are fine; two logic copies are not.** A fast simplified bench -(`CityState`) and a rich live model (`City` / `CityScript`) are legitimately different — they -serve different jobs. What must be single-source is the **logic**: the formula that turns -state into a number. Unify *logic into crates*; do not unify *state* just to share logic. +**1. Rust drives everything — never reconcile.** When a GDScript formula disagrees with the crate, +the GDScript is a **bug to delete**, not a baseline to preserve. Don't ask "which is the truth" or +"make the crate match the live behavior" — that inverts Rail-1. Rust is the truth; the live game +calls it. If adopting the canonical value changes gameplay balance, that's a separate tuning task +done **in Rust/JSON** — it never gates or reverses the fix. (A "pending balance sign-off" comment +in GDScript is itself a symptom of the violation.) -When unsure, prefer the crate. Pulling a formula *up* into a pure crate is cheap and always -correct; pushing it *down* into an orchestrator or GDScript is the breach. +**2. The UI is a pure view of `getState()`.** Presentation renders `view_json`/`getState()`, sends +input via `act()`, runs the turn via `end_turn()`. It does **not** hold authoritative state, does +**not** orchestrate the turn, does **not** call per-phase logic. Authoritative state + logic are +100% Rust, per running context. The headless `GdPlayerApi` is the reference implementation — when +the live game diverges from that shape, the live game is wrong. + +**3. Single-source the LOGIC, not necessarily the STATE.** Two *separate Rust-owned contexts* (the +fast bench `mc-turn`/`CityState` and the live game) legitimately differ in state shape — they're +different programs. What's illegitimate is one running game holding **two** state copies (Rust + +GDScript entities) that can diverge. Within a context: one authoritative state (Rust), the UI views +it. Across contexts: shared **logic crates**, never copied formulas. + +When unsure, prefer the crate, and prefer **deleting** GDScript logic over making it call Rust. +Pulling logic *up* into Rust (and rendering the result) is always correct; pushing it *down* into +an orchestrator or GDScript — or having the UI call it — is the breach. diff --git a/tooling/claude/dot-claude/instructions/specialist-preamble.md b/tooling/claude/dot-claude/instructions/specialist-preamble.md index ed2d5db8..176b9c8c 100644 --- a/tooling/claude/dot-claude/instructions/specialist-preamble.md +++ b/tooling/claude/dot-claude/instructions/specialist-preamble.md @@ -12,6 +12,7 @@ Each rule below earned its place by being violated. Treat them as commandments, - **Never assert what exists from reasoning.** `grep`/`ls`/`read` first, then cite `file:line`. If you can't verify, say "unverified." - **Docs are intent; code is truth.** Design docs, CLAUDE.md, even *this* instruction set drift. This project shipped a `rust-source-of-truth.md` with three false claims (a phantom `mc-magic` crate, a `GdAiController` that "didn't exist" but did, a `BiomeRegistry` in the wrong crate). When a doc and the code disagree, **the code wins — and flag the doc.** - **A recalled memory or a stale comment is a hint, not a fact.** Re-verify any file/fn/flag it names before relying on it. +- **Verify your architectural PREMISES before committing to a plan.** A plan built on a remembered shape drifts. (This project once drifted swap → logic-extraction → per-formula-FFI across three turns, each on an unchecked premise; one `grep` of `view.rs` collapsed all three.) Re-grep the shape first, then plan. ## 2. Know where code goes (the layering) @@ -23,6 +24,7 @@ Before writing any non-trivial code, classify it and place it. Full procedure: * - **Content** (stats, costs, thresholds, names) → a **JSON pack**. Nothing hardcoded. - **Shared type** (used by ≥2 crates) → `mc-core` — the *type* only, never its rules. - **Grep the owning crate before computing any game number.** Reimplementing a formula is how `_process_growth` diverged from `mc_happiness::get_growth_modifier`. If the fn exists, call it; if not, write it *in the crate* and call from both. +- **Rust drives everything; the UI is a pure view of `getState()`.** Authoritative state + logic are 100% Rust. Presentation renders `view_json`/`getState()`, sends `act()`, runs `end_turn()` — it never holds authoritative state, orchestrates the turn, or calls per-phase logic. A GDScript formula that disagrees with the crate is a **bug to delete**, never a baseline to reconcile (balance tuning happens in Rust/JSON). Prefer **deleting** GDScript logic over making it call Rust. Layer specifics: **`rust-source-of-truth.md`** (Rust/crates), **`gdscript-conventions.md`** (`.gd`).