From 54d7f8f63059fbde963fccd5043093d7d67b1a5a Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 05:38:43 -0400 Subject: [PATCH] =?UTF-8?q?docs(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=A7=AD=20tooling=20=E2=80=94=20encode=20getState/never-re?= =?UTF-8?q?concile=20+=20verify-premises=20lessons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final agent-tooling pass folding in this session's hardest-won lessons (previously only in my memory): code-layering.md: - anti-pattern #4 "fixing the orchestrator instead of eliminating it" (the swap→extract→FFI drift; the fix is usually DELETE the GDScript path, let Rust compute + UI render getState()). - anti-pattern #5 "UI holding state / calling logic instead of reading getState()". - principles rewritten: (1) Rust drives everything — divergence is a bug to DELETE, never reconcile; (2) the UI is a pure view of getState() (render/act/end_turn, GdPlayerApi is the reference); (3) single-source LOGIC not necessarily STATE — but never two state copies in ONE running game. specialist-preamble.md (always-loaded core): - verify rule gains "verify architectural PREMISES before committing to a plan" (the 3-turn drift, collapsed by one grep of view.rs). - layering rule gains the Rust-drives / UI-is-a-view-of-getState one-liner. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dot-claude/instructions/code-layering.md | 38 +++++++++++++++---- .../instructions/specialist-preamble.md | 2 + 2 files changed, 33 insertions(+), 7 deletions(-) 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`).