docs(@projects/@magic-civilization): 🧭 tooling — encode getState/never-reconcile + verify-premises lessons
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) <noreply@anthropic.com>
This commit is contained in:
parent
b93328cd11
commit
54d7f8f630
2 changed files with 33 additions and 7 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue