diff --git a/.claude/rules/fork-for-sidequests.md b/.claude/rules/fork-for-sidequests.md new file mode 100644 index 000000000..3dd7179d8 --- /dev/null +++ b/.claude/rules/fork-for-sidequests.md @@ -0,0 +1,50 @@ +# Fork for side quests + +A side quest is work that is worth doing but is not the task in hand: filing a +ticket, writing up a finding, chasing a tangent, a docs pass. Doing it inline +costs the main thread its context — which is why forward intent tends to go +unrecorded at all (T-1245). + +**Default: fork it.** Spawn an agent from the frozen context, keep working. + +## Fork when + +- Writing or appending a ticket, especially a long one. +- Recording a finding that is real but off the current path. +- An investigation whose ANSWER matters but whose SEARCH does not — "does X + exist", "who else calls this", "is this documented anywhere". +- Anything you catch yourself deferring with "I'll note that later". Later is + where notes die. + +## Do NOT fork when + +- It is on the critical path — if the current task blocks on the answer, do it + inline and keep the reasoning visible. +- It needs a judgement only the lead has context for (a design call, a trade-off + the user has been steering). +- It is one command. A fork costs more than the work. + +## The report contract — required, and brief + +Every fork returns exactly this, in this order. Long reports defeat the purpose; +the lead is mid-task. + +1. **STATUS** — one line: done / blocked / partial, and the outcome. +2. **FILES TOUCHED** — every path created or modified, or `none`. This is not + optional. The lead has to know what is in the working tree before staging, + and an unreported edit turns into a mystery diff at commit time. +3. **ISSUES** — anything found that is wrong, missing or surprising. One line + each. `none` is a valid and welcome answer. +4. **FRICTION** — what was hard to find, misleading, or only found by luck. This + is the docs/skills feedback loop; a fork that hit a wall silently wastes the + evidence. + +## Git discipline + +Forks do not commit. `.claude/hooks/git-centralize-guard.sh` already blocks +`.git`-mutating commands for teammates, so this is enforced rather than trusted — +but the FILES TOUCHED line is what makes it workable, because the lead is the one +who stages and has to know what changed and why. + +If a fork's work should not land in this branch at all, say so in STATUS rather +than leaving the lead to discover it in `git status`. diff --git a/.claude/skills/wiki/SKILL.md b/.claude/skills/wiki/SKILL.md index fa3c42fd1..4d159d141 100644 --- a/.claude/skills/wiki/SKILL.md +++ b/.claude/skills/wiki/SKILL.md @@ -32,8 +32,24 @@ atlas sync alongside. **Never hand-edit:** - anything inside a `` block (System Profile, Topology, Celestial Bodies, Stations & Facilities) -- `bodies/*/index.md` frontmatter — it IS the body definition and it is - machine-owned +- `bodies/*/index.md` frontmatter — machine-owned + +**Body frontmatter is a MIDDLE layer, not the origin.** GOVERNANCE.md calls it +"the body definition", which is true of its role downstream and misleading about +where it comes from. Actually: + +1. The bodies catalog in `server/data/systems.db` is the origin, authored through + the atlas CLI (`tooling/atlas add-body` / `author-system`) — atlas-CLI-owned + and surviving `make regen-db` (`Skill(atlas)`). +2. `scaffold_bodies.py` + `body_definition_parser.py` WRITE the page frontmatter + from that, deriving fields as they go — e.g. `tectonics` is a lookup off + `planet_class` (`body_definition_parser.py:563`), with an override hook, not + an independently authored value. +3. `import_economics` then READS some of those fields back. + +So it is output of one stage and input to the next, which is exactly why hand +edits are both reverted AND wrong. To change a body's identity, go to the atlas +CLI, not the page. **Do author**, in place, and it survives regeneration: - the named prose sections of a system page: Supply Dependency, Faction Notes, @@ -58,7 +74,14 @@ empty scaffolding: | `technology/` | 7 | | | `contraband/`, `concepts/` | 4 each | | | `triangles/` | 2 | relationship structures | -| `institutions/`, `species/`, `cultural-groups/`, `lore/` | **0** | templates exist, no content yet | +| `institutions/`, `species/`, `cultural-groups/`, `lore/` | **0** | templates exist — but see Q-118 | + +**Those four empty categories are an OPEN QUESTION, not an invitation.** +`governance/questions/scope.md` Q-118 (2026-06-12, unresolved) asks whether they +should be populated at all or retired: Phase 1 closed as done without them, +because the cultural layer shipped structurally instead (D-232 trait catalog + +D-237 system pins as TOML). Surface Q-118 before authoring into them — the +template's existence is not scope approval. To add one: copy the matching `_templates/` file, fill ALL frontmatter, write a one-line `description`, set `status: proposed`, follow the template's structure. @@ -115,6 +138,23 @@ source set, so a typo fix in body text trips the pre-push stamp check. Run before loading full files". Read frontmatter descriptions and load selectively — this tree is 11,864 files and will eat a context window whole. +**Check the current state before editing anything.** A task can describe a change +that is already true. Ferrath was asked to be made "arid with low tectonics" and +already was, in all four places it is recorded — acting on the framing would have +hand-edited a machine-owned, already-correct file. Read the target first; the +rules below only protect you if you look before you touch. + +**Some bodies are visual-regression goldens.** Ferrath (`GJ820Bc`) appears in +`tests/visual.json` and `tests/atlas_shots.json` (Global, District, and the +descent-ladder set) precisely because of its current terrain. Regenerating its +terrain is legitimate under every rule here and will silently break goldens — +re-capture them if you do. Nothing in `wiki/` says so; check `tests/` for the +body id before regenerating any body. + +**`status` is editorial, not a gate.** `import_economics` has no status filter, +so a `proposed` corp imports into the live economy exactly like a `canonical` +one. Do not assume `proposed` means inert. + **Absent variance is often deliberate.** Fields can be forward-reservations or staged gates, not gaps. `chemosynthetic: false` on every body reserves the namespace for dextro-DNA-style biochemistry once geology and nature spawn to the diff --git a/.pql/changelog/ticket_history/2026-08.sql b/.pql/changelog/ticket_history/2026-08.sql index 2d8ede410..48e5367e2 100644 --- a/.pql/changelog/ticket_history/2026-08.sql +++ b/.pql/changelog/ticket_history/2026-08.sql @@ -1096,3 +1096,32 @@ Design implication, carried into step 3 (INVESTIGATE FORM): weight the options b cost-to-append, not just by cost-to-read. An artefact that is pleasant to read and expensive to update will rot, and this project already has heavily-documented trees that stayed accurate precisely because updating them was one line.', NULL, '2026-08-19 22:39:34', '2026-08-19 22:39:34.248', '2026-08-19 22:39:34.248', NULL, 'e74c0f651c9053b62e256b0201f77d9a', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1R8WZ987YCP1ARANSA70R0W', 'description', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md.', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md. + +--- +MATERIAL CORRECTION (2026-08-20, same day): tectonics is DERIVED, not authored. + +`tooling/planet-gen/body_definition_parser.py:563` reads +`tectonics = tectonic_map.get(planet_class, "low")`, with an override hook on the +next line. So the "67.8% tectonics: low" measured above is not an authoring +decision at all — it is a projection of the planet_class distribution (51.1% +frozen, and whatever else maps to low) through a fixed lookup, plus a default of +"low" for any class not in the map. + +That changes the question this ticket asks. It is NOT "did the authoring pass +default tectonics and move on". It is: + + 1. Is the PLANET_CLASS distribution intended? That is the real authored axis, + and it is where variance would have to come from. + 2. Is the class -> tectonics map right? A lookup that sends most classes to + "low", plus a "low" default for unmapped classes, concentrates the output + further than the input. Check the map''s spread before blaming the data. + 3. Is the override hook used at all? If no body sets it, the map is the whole + story. + +Found by a cold-context agent reading body_definition_parser.py while testing +Skill(wiki); verified directly afterwards. Worth noting how it was missed: the +original measurement scanned frontmatter VALUES and never asked where they came +from, so a derived field was read as an authored one. Same class of error as the +scalar-only parser caveat already recorded — a scan tells you what a field says, +never who decided it.', NULL, '2026-08-19 22:45:45', '2026-08-19 22:45:45.557', '2026-08-19 22:45:45.557', NULL, '2b98fb90a8e4d5ea3447cfe489c89848', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql index 8e0a6d177..97b50247a 100644 --- a/.pql/changelog/tickets/2026-08.sql +++ b/.pql/changelog/tickets/2026-08.sql @@ -1082,3 +1082,32 @@ Design implication, carried into step 3 (INVESTIGATE FORM): weight the options b cost-to-append, not just by cost-to-read. An artefact that is pleasant to read and expensive to update will rot, and this project already has heavily-documented trees that stayed accurate precisely because updating them was one line.', 'backlog', 'high', NULL, 'meta', NULL, '2026-08-19 22:38:22.412', '2026-08-19 22:39:34.248', NULL, '69d785aeb9a77c4c9296c6b404237dfb', 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 ('06G1R8WZ987YCP1ARANSA70R0W', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Seed variance: 68% of bodies are tectonics:low — is the categorical monoculture intended?', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md. + +--- +MATERIAL CORRECTION (2026-08-20, same day): tectonics is DERIVED, not authored. + +`tooling/planet-gen/body_definition_parser.py:563` reads +`tectonics = tectonic_map.get(planet_class, "low")`, with an override hook on the +next line. So the "67.8% tectonics: low" measured above is not an authoring +decision at all — it is a projection of the planet_class distribution (51.1% +frozen, and whatever else maps to low) through a fixed lookup, plus a default of +"low" for any class not in the map. + +That changes the question this ticket asks. It is NOT "did the authoring pass +default tectonics and move on". It is: + + 1. Is the PLANET_CLASS distribution intended? That is the real authored axis, + and it is where variance would have to come from. + 2. Is the class -> tectonics map right? A lookup that sends most classes to + "low", plus a "low" default for unmapped classes, concentrates the output + further than the input. Check the map''s spread before blaming the data. + 3. Is the override hook used at all? If no body sets it, the map is the whole + story. + +Found by a cold-context agent reading body_definition_parser.py while testing +Skill(wiki); verified directly afterwards. Worth noting how it was missed: the +original measurement scanned frontmatter VALUES and never asked where they came +from, so a derived field was read as an authored one. Same class of error as the +scalar-only parser caveat already recorded — a scan tells you what a field says, +never who decided it.', 'backlog', 'medium', NULL, 'server', NULL, '2026-08-19 22:28:11.722', '2026-08-19 22:45:45.557', NULL, '7a1737a72bba4913d5e8f5fa78e44312', 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;