docs(@projects/@magic-civilization): 🛡️ Rail-2 — document the two-path content divergence + track an enforcement gate

rust-source-of-truth.md: add the "two-path divergence" rule to the canonical
content store section. Content reaches the sim two ways — in-game (GDScript
DataLoader reads JSON at runtime, projection.rs:41) and headless (Rust falls back
to a compile-time include_str!/hardcode copy, dispatch.rs:410). A balance constant
hardcoded in a crate is both a Rail-2 violation and a silent second copy that
drifts from the JSON — and headless is where the AI trains. Rule: grep
public/resources + public/games/**/data for a JSON home before adding a numeric
balance const; if it exists, LOAD it (OnceLock+include_str!, never std::fs in
shared sim code). References the p3-28 ContentRegistry endgame.

p3-28: add the matching "Rail-2 verify gate (enforcement)" acceptance bullet —
tools/check-no-rust-hardcoded-content.py + a verify step to catch the next
hardcode, best landed alongside the ContentRegistry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-27 09:47:43 -04:00
parent 24c0e0c24c
commit 655d25e2c1
2 changed files with 10 additions and 1 deletions

View file

@ -32,6 +32,13 @@ revealed three SOLID/DRY/DIP debts. "Foundation first" tackled the layering + ph
`res://` bytes across FFI, the web guide fetches packs → bytes across bindgen — with
`include_str!` surviving **only** as the headless/test fallback. So the target is a registry
fed by injected bytes, not a path-reading function.
- [ ] **Rail-2 verify gate (enforcement) — catch the next hardcode before it ships.** The gate
has a Rail-1 check (`verify.sh` Step 18 → `tools/check-no-gdscript-sim-logic.py`) but **no Rail-2
equivalent** for hardcoded game content in Rust. Add `tools/check-no-rust-hardcoded-content.py` +
a verify step that flags balance-looking `const`/`static` tables in `mc-*` crates which duplicate
a JSON file. ⚠ heuristic — needs a low-false-positive design (likely a small allowlist/registry of
`(json_file, owning_module)` pairs rather than a blind numeric-array grep). Best landed alongside
the `ContentRegistry` so the check becomes "does the registry own this?", not a regex.
- [ ] **Dedup the ad-hoc `include_str!` content sites (Opportunity A, same arc)** — verified
2026-06-27: **8 JSON-config `include_str!` sites** across simulator crates, each rolling its
own `OnceLock` + a fragile relative path (6 at `../../../../../`). `treaty_rules.json` is

View file

@ -14,7 +14,9 @@ There is **no permanent GDScript carve-out** for AI (Rail-1). New AI work lands
## Canonical content store
**JSON game packs remain the canonical content store.** Stats, costs, effects, thresholds — all in `public/games/age-of-dwarves/data/*.json`. Neither Rust nor GDScript hardcodes game content.
**JSON game packs remain the canonical content store.** Stats, costs, effects, thresholds — all in `public/games/age-of-dwarves/data/*.json` (+ `public/resources/*`). Neither Rust nor GDScript hardcodes game content.
> **The two-path divergence (why this rule gets violated silently).** Content reaches the sim two ways: **in-game**, GDScript `DataLoader` reads the JSON at runtime (`mc-player-api/src/projection.rs:41`); **headless** (tests, CI, AI self-play, WASM guide), Rust falls back to a compile-time `include_str!`/hardcode copy (`dispatch.rs:410`). A balance constant hardcoded in a Rust crate is both a Rail-2 violation **and** a second copy that drifts from the JSON with no error — and the headless path is where the AI trains. **Before adding a numeric balance constant in a crate, grep `public/resources/**` + `public/games/**/data/**` for an existing JSON home; if it exists, LOAD it (`OnceLock`+`include_str!` — WASM/gdext-safe, never `std::fs` in shared sim code), don't hardcode.** Instance fixed 2026-06-27: `mc-combat` promotion XP thresholds (`promotions.rs` const vs `promotions.json`). The structural endgame (one host-fed `ContentRegistry` both paths read) is tracked on `p3-28`.
- UI labels resolve through `ThemeVocabulary.lookup(engine_key)` — never hardcode theme strings
- Sprites resolve through `ThemeAssets.resolve(path)` — never hardcode asset paths