diff --git a/.claude/rules/project-structure.md b/.claude/rules/project-structure.md
index fd9cdc9ba..3b0aca491 100644
--- a/.claude/rules/project-structure.md
+++ b/.claude/rules/project-structure.md
@@ -1,5 +1,10 @@
# Project Structure (detailed)
+For how `wiki/`, the generators, `systems.db` and the runtime relate — which
+way the arrows point for any given file — the canonical map is the diagram
+`docs/diagrams/data-flow/wiki-generator-flow.d2` (**D-262**), not the tree
+below. The tree says where things live; the diagram says who writes them.
+
```
wiki/ # THE SEED (11,864 files, the largest tree here). Authored
# world content the generator varies from — NOT a
diff --git a/.claude/skills/wiki/SKILL.md b/.claude/skills/wiki/SKILL.md
index f550d97b9..541d5ec6b 100644
--- a/.claude/skills/wiki/SKILL.md
+++ b/.claude/skills/wiki/SKILL.md
@@ -23,6 +23,12 @@ should read its own docs for detail:
Two ownership models, running OPPOSITE ways. Get this wrong and your work
vanishes on the next sync, without an error.
+**The canonical answer is a diagram, not this text (D-262):**
+`docs/diagrams/data-flow/wiki-generator-flow.d2` — view it with
+`clide draw --file docs/diagrams/data-flow/wiki-generator-flow.d2`. Every edge
+in it was read in the tool's own source. What follows here is a summary; where
+the two disagree, the diagram is right.
+
### `star-systems/` — DB owns structure, wiki owns prose (OUTPUT)
3,262 pages, 93% of the wiki. Generated FROM `server/data/systems.db` and
diff --git a/.config/hooks/pre-push b/.config/hooks/pre-push
index a43d5d777..f30a19dca 100755
--- a/.config/hooks/pre-push
+++ b/.config/hooks/pre-push
@@ -316,6 +316,20 @@ if [ -f "$REPO_ROOT/tooling/check-canvas-version" ]; then
fi
fi
+# --- Data-flow diagram paths (D-262) ---
+# A diagram that names files goes stale SILENTLY — nothing fails when a path
+# moves, so the map keeps asserting a layout that is no longer true. This checks
+# only that the paths still resolve; whether an EDGE still means what it says is
+# a human check against the tool's source. Cheap (no subprocess beyond python).
+if [ -f "$REPO_ROOT/tooling/check-dataflow-graph.py" ]; then
+ echo "pre-push: checking data-flow diagram paths..."
+ if python3 "$REPO_ROOT/tooling/check-dataflow-graph.py"; then
+ :
+ else
+ fail_check "a path named in a data-flow diagram no longer resolves"
+ fi
+fi
+
# --- Clerk review (D-221) ---
# DISABLED 2026-05-23 (#965) pending rework. Two problems made it net-negative:
# 1. Non-exhaustive — a single run reports ~the first contradiction it finds
diff --git a/.pql/changelog/ticket_idmap/2026-08.sql b/.pql/changelog/ticket_idmap/2026-08.sql
index 176ef19f4..c8ac32380 100644
--- a/.pql/changelog/ticket_idmap/2026-08.sql
+++ b/.pql/changelog/ticket_idmap/2026-08.sql
@@ -5,3 +5,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G0P0F9C3RKKK23ZHHTANCPZW', 'T-1243', '2026-08-16 14:37:49.794', '2026-08-16 14:37:49.794', NULL, '0dbba61f1c75851f6e0cca78402da745', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1R8WZ987YCP1ARANSA70R0W', 'T-1244', '2026-08-19 22:28:11.726', '2026-08-19 22:28:11.726', NULL, 'fd1f9d38036692125ba7c7de0198bfc8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1RB7GSJA4379EAQDZPQQDN8', 'T-1245', '2026-08-19 22:38:22.413', '2026-08-19 22:38:22.413', NULL, '34a0757e14a868c4d9227c372c013e90', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
+INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1RWR7M1H1PC3TY2RT7AC174', 'T-1246', '2026-08-19 23:54:55.776', '2026-08-19 23:54:55.776', NULL, 'f3663ab4236d777635d78a5cf33e65e1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql
index 97c799c49..9ea63295c 100644
--- a/.pql/changelog/tickets/2026-08.sql
+++ b/.pql/changelog/tickets/2026-08.sql
@@ -1176,3 +1176,12 @@ the same root cause — measuring VALUES without reading the CODE that produced
them. First correction: tectonics is derived. Second: the whole continuous/
categorical split is an artefact of the resolution tiers. Both were available in
a docstring the original pass grepped but never read.', 'backlog', 'medium', NULL, 'server', NULL, '2026-08-19 22:28:11.722', '2026-08-19 22:52:46.636', NULL, '65db5461d46f8f84aed68526113025a6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
+INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1RWR7M1H1PC3TY2RT7AC174', 'story', '06FBPPMZNNEV052DBYYY3A897C', 'Investigate retooling Python tooling into a Rust CLI (Q-124)', 'Answer Q-124. Investigation and costing FIRST — do not open with a rewrite.
+
+Price the cheap alternative before recommending Rust: a single Python dispatcher entrypoint (one command, many subcommands) would resolve the permission-prompt friction and most of the startup cost without touching the numerics. If that is enough, say so and close Q-124 with it.
+
+Scope order if Rust is chosen: port the check/gate family first (check-client-version, check-canvas-version, check-systems-db-stamp, check-dataflow-graph) — small, pure, no numerics, highest prompt-friction, runs on every push. Leave planet-gen numerics (numpy/scipy/PIL) in Python behind a stable interface.
+
+Hard constraint: import_economics is stamped by source SHA via tooling/generator_sources.py. Any port must keep the stamp/pre-push contract intact through the transition, not disabled during it.
+
+Outcome: a recommendation with costs, then a D-record and Jeroen''s ruling. Epics only after that.', 'backlog', 'low', NULL, 'tooling', 'Q-124', '2026-08-19 23:54:55.776', '2026-08-19 23:54:55.776', NULL, 'cfb08832c72e06f85b075a46cd3223f2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
diff --git a/Makefile b/Makefile
index cba180f4f..e01c94115 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
perf-baseline debug-schedule \
test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \
screenshot visual-movie test-visual visual-update \
- manifest diagrams check-diagrams
+ manifest diagrams check-diagrams check-dataflow-graph
# --- Configuration ---
@@ -561,6 +561,11 @@ check-diagrams:
test $$fail -eq 0 || { echo "Run 'make diagrams'."; exit 1; }; \
echo "All diagrams rendered."
+# D-262: every repo path named in a hand-authored data-flow diagram must still
+# resolve. Catches the stale half of a diagram; edge MEANING is a human check.
+check-dataflow-graph:
+ @python3 tooling/check-dataflow-graph.py
+
# --- Clean ---
clean:
diff --git a/docs/diagrams/data-flow/wiki-generator-flow.d2 b/docs/diagrams/data-flow/wiki-generator-flow.d2
index d5b7b16bf..90c64c4b0 100644
--- a/docs/diagrams/data-flow/wiki-generator-flow.d2
+++ b/docs/diagrams/data-flow/wiki-generator-flow.d2
@@ -59,8 +59,8 @@ db: "systems.db" {class: store}
smap: "star-map.json" {class: store}
# ---- generated into wiki/ (grey): do not hand-edit ----
-sys: "GJ-*/index.md\nREAD-ONLY blocks" {class: generated}
-body: "bodies/{id}/index.md\nfrontmatter = body definition" {class: generated}
+sys: "wiki/star-systems/GJ-*/index.md\nREAD-ONLY blocks" {class: generated}
+body: "wiki/star-systems/GJ-*/bodies/{id}/index.md\nfrontmatter = body definition" {class: generated}
assets: "heightmap · reliefmap · globe\nterrain.npz · markers.json" {class: generated}
brands: "generated_brands.toml" {class: generated}
diff --git a/docs/diagrams/data-flow/wiki-generator-flow.svg b/docs/diagrams/data-flow/wiki-generator-flow.svg
index 221c942b0..26bedb3b3 100644
--- a/docs/diagrams/data-flow/wiki-generator-flow.svg
+++ b/docs/diagrams/data-flow/wiki-generator-flow.svg
@@ -1,16 +1,16 @@
-
diff --git a/governance/README.md b/governance/README.md
index 063a7ae0e..dbe7022cb 100644
--- a/governance/README.md
+++ b/governance/README.md
@@ -327,6 +327,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it.
- [D-259: Enclosed settlements — open-air requires a breathable atmosphere](decisions/architecture.md#d-259-enclosed-settlements--open-air-requires-a-breathable-atmosphere) — _architecture_
- [D-260: Generator scope — Sol is authored, not generated](decisions/architecture.md#d-260-generator-scope--sol-is-authored-not-generated) — _architecture_
- [D-261: River rendering — a stroke, not a scaled feature](decisions/architecture.md#d-261-river-rendering--a-stroke-not-a-scaled-feature) — _architecture_
+- [D-262: The wiki↔generator data flow has one canonical map, and it is a diagram](decisions/architecture.md#d-262-the-wikigenerator-data-flow-has-one-canonical-map-and-it-is-a-diagram) — _architecture_
## Open questions
@@ -411,6 +412,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it.
- [Q-121: Where does cultural dress canon live — TOML registers now, prose pages in Phase 6?](questions/content.md#q-121-where-does-cultural-dress-canon-live--toml-registers-now-prose-pages-in-phase-6) — _content_
- [Q-122: Third-party asset sourcing policy — when to use CC0/purchased bases vs Trellis generation, per asset class?](questions/content.md#q-122-third-party-asset-sourcing-policy--when-to-use-cc0purchased-bases-vs-trellis-generation-per-asset-class) — _content_
- [Q-123: Believability gate threshold calibration](questions/architecture.md#q-123-believability-gate-threshold-calibration) — _architecture_
+- [Q-124: Should the Python tooling be retooled into a single Rust CLI?](questions/architecture.md#q-124-should-the-python-tooling-be-retooled-into-a-single-rust-cli) — _architecture_
## Resolved questions
diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md
index 828a0f10b..ead62a1f4 100644
--- a/governance/decisions/architecture.md
+++ b/governance/decisions/architecture.md
@@ -2514,6 +2514,23 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Cross-reference:** [D-255](#d-255) (stepped ladder — premise (1) server-determines-content / premise (2) client-is-a-map-art-function, the split this record applies; and the 2026-07-26 extent inversion that made screen-pixel reasoning coherent per rung), [D-258](#d-258) (rung 0.5 — its amendment records the 375-course / 458-pixel measurement this record is built on), [D-227](#d-227) (derive-don't-store — culling is presentation, not derivation, so it is outside that rule).
- **Dissent:** None.
+### D-262: The wiki↔generator data flow has one canonical map, and it is a diagram
+
+- **Date:** 2026-08-20
+- **Decision:** `docs/diagrams/data-flow/wiki-generator-flow.d2` (rendered to `.svg` beside it) is the **source of truth for how `wiki/`, the generators, `systems.db` and the runtime relate**. It answers one question — *for any file under `wiki/`, which way do the arrows point?* — and that question has no other home. Prose descriptions of the flow in `CLAUDE.md`, `Skill(wiki)`, `.claude/rules/project-structure.md` and `wiki/GOVERNANCE.md` remain, but each now points here, and where any of them disagrees with the diagram, **the diagram is right and the prose is a stale copy**.
+- **Why a diagram and not prose.** The relationship is a directed graph with two edges running *opposite* to the obvious direction and one running *backwards into its own producer*. Prose renders that badly: every document that has tried states a single ownership direction and is therefore wrong about part of the tree. The graph is the natural shape of the data, so it is the honest representation.
+- **What the diagram records that nothing else does:**
+ - **Two paths run BOTH ways.** `wiki_sync.py` renders DB→wiki (`generate_wiki()`, routine) *and* parses wiki→DB (`import_from_wiki()`, bootstrap/migration only). Neither direction alone is "the" direction.
+ - **The trap.** `scaffold_bodies.py` writes a body page **once and never overwrites it**; `import_economics` then reads that frontmatter directly as input. So a hand-edited body page is **not reverted — it is obeyed**, and silently changes world generation. This is strictly worse than being overwritten, and it is the real reason `wiki/GOVERNANCE.md` forbids the edit. Every document that described body pages as "machine-owned, reverted on sync" was wrong in the direction that matters.
+ - **Two routes into the game.** `systems.db` ships with the build; the per-body terrain PNGs are read at **runtime** via `bodies.terrain_reference`, never baked in.
+- **Provenance is part of the artefact.** Every edge was read in the tool's own source, not inferred from filenames or grep. The header comment says so. A diagram whose provenance is unstated is a guess with good typography, and this one exists because an earlier statistical scan of the same tree produced confident, wrong conclusions.
+- **Enforcement — and its explicit limit.** `tooling/check-dataflow-graph.py` (`make check-dataflow-graph`, run by the pre-push hook) asserts that **every repo path named in the diagram still resolves**. That is the cheap half. It **cannot** verify that an edge still *means* what it says: if `wiki_sync.py` stopped writing body pages tomorrow, every path would still exist and the check would still pass. **Edge semantics are a human check against the tool's source**, and this limit is written into the script's docstring so nobody mistakes a green gate for a verified map. The failure mode being prevented is silence — a path moves, nothing fails, and the map keeps asserting a layout that is no longer true.
+- **Scope: hand-authored diagrams only.** `docs/diagrams/design/star-map-*.d2` are **generator output** (`tooling/generate-star-map.py`), so their correctness is the generator's problem under the same source-canonical rule as [D-223](#d-223)/`asset-pipeline.md`, not this checker's. They are excluded by name.
+- **Consequence for format.** Diagrams are now `.d2` + `.svg`, not `.png` — d2 emits SVG natively while its PNG path needs a ~150 MB headless-Chromium download that this system does not have. `make diagrams` renders, `make check-diagrams` gates. Note the trap this exposed: a d2 `|md` block compiles to an SVG ``, which ImageMagick and `flutter_svg` both silently drop — text that exists in the file and is invisible in every viewer except a browser. Legends must be plain labels. Recorded in `.claude/rules/diagrams.md`.
+- **Raised by:** Jeroen, 2026-08-20: *"add this to the docs and relevant d records that this is where the source of truth for this component lies (this may be dangerous since it may be weirdly shaped data so we need some sort of graph solution you can use)"* — the "weirdly shaped data" instinct was correct, and is why this is a graph rather than another table.
+- **Cross-reference:** [D-223](#d-223) (source-canonical asset pipeline — the rule the generated star-maps fall under instead), `Skill(wiki)`, `.claude/rules/diagrams.md`, `wiki/GOVERNANCE.md`.
+- **Dissent:** None.
+
---
-*115 decisions (D-001 through D-261, excluding gaps). Last updated: 2026-07-28 (D-261 — river rendering: a fixed 5 px screen-space stroke through cell centres, never over water, culled below 15 px of on-screen length so the km threshold derives per rung from the stroke itself; client-side because the cull is measured in pixels the server cannot know).*
+*116 decisions (D-001 through D-262, excluding gaps). Last updated: 2026-08-20 (D-262 — the wiki↔generator data flow has one canonical map, `docs/diagrams/data-flow/wiki-generator-flow.d2`; a path checker runs on push, but edge MEANING stays a human check against the tool's source).*
diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md
index f67df06fd..eebe2fe5c 100644
--- a/governance/questions/architecture.md
+++ b/governance/questions/architecture.md
@@ -491,6 +491,24 @@ Technical foundation questions: engine, protocols, data structures, performance,
7. **Pass budget + ratchet** — the % of land probes that must pass to ship (budgeted), and the trigger to ratchet advisory → strict (hard push-gate block).
- **Cross-reference:** [D-245](../decisions/architecture.md#d-245-nature-layer-believability-acceptance-gate) (the gate this calibrates), [D-239](../decisions/architecture.md#d-239) §8/§10 (laws + budget), [D-227](../decisions/architecture.md#d-227), [D-243](../decisions/architecture.md#d-243); tickets T-1083 (enforcer — produces the baselines), T-1079 (epic), T-1080/T-1081/T-1082/T-1084.
+### Q-124: Should the Python tooling be retooled into a single Rust CLI?
+
+- **Date:** 2026-08-20
+- **Status:** Open
+- **Question:** `tooling/` is **123 Python files** plus 36 extensionless executables. Should it become one Rust CLI of the same calibre as `pql` — a single binary, subcommand-structured, no interpreter and no virtualenv — or should it stay Python and have its friction fixed in place?
+- **The friction is concrete, recurring, and mostly not about Python the language:**
+ 1. **Permission prompts.** Every invocation is `python3 tooling/