From 31b553a3b39263bb8d6b7f7e9468d81cf6634abe Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 3 Jul 2026 10:42:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(simulation):=20intra-class=20micro-habitat?= =?UTF-8?q?=20mosaic=20v1=20=E2=80=94=20vegetation/water=20texture=20(T-10?= =?UTF-8?q?84,=20D-246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-chunk mosaic that modulates the D-228 axes into a spatially-coherent, relief/moisture-conditioned mosaic of micro-habitats within a terrain class — the fix for the aliveness probe's uniform "Wetland 100%". - seed.rs: SeedDomain::VoxelMosaic = 12 (append-only; pinned-discriminant guard) - voxel.rs: five new D-228 axis values (Vegetation::{Meadow,Deadfall,Lichen}, TerrainMaterial::{Hardpan,Scree}); the per-family-gated mosaic pass - detail_scatter.rs: voxel_mosaic() selection field (~8-64 m band) - atlas/mosaic.rs: surface-class palette + integer-basis-point weighted selection with moisture-affinity conditioning; temperate/native baseline palettes - data/mosaic_constants.toml: source-canonical palette mirror - believability.rs: micro_habitat_distinct metric + D-245 intra-class criterion v1 scope (D-246-faithful): modulates vegetation + water only, in the vegetated classes (Wetland/Forest/Grassland), so the D-239 §8 lithology law is untouched; water is gated on the T-1040 has_active_channel hydrology rule. Material variants (hardpan/scree) and the extreme classes are a §8-aware follow-up; the biome-aware palette variants (T-1085 vocab) extend the baseline before generation. Goldens regenerated (believability + derivation). Full server suite green. Co-Authored-By: Claude Opus 4.8 --- .pql/changelog/ticket_history/2026-07.sql | 164 +++++++++++ .pql/changelog/tickets/2026-07.sql | 103 +++++++ server/data/mosaic_constants.toml | 254 +++++++++++++++++ server/src/atlas/believability.rs | 39 ++- server/src/atlas/detail_scatter.rs | 78 ++++++ server/src/atlas/mod.rs | 1 + server/src/atlas/mosaic.rs | 295 ++++++++++++++++++++ server/src/atlas/voxel.rs | 83 +++++- server/src/seed.rs | 6 + server/tests/golden/believability.json | 6 +- server/tests/golden/derivation_harness.json | 2 +- 11 files changed, 1022 insertions(+), 9 deletions(-) create mode 100644 server/data/mosaic_constants.toml create mode 100644 server/src/atlas/mosaic.rs diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql index 5c07b73b3..22fb4dbca 100644 --- a/.pql/changelog/ticket_history/2026-07.sql +++ b/.pql/changelog/ticket_history/2026-07.sql @@ -39,3 +39,167 @@ DECISIONS (user-resolved): CODE TOUCHPOINTS (Si-verified 2026-07-02): detail_scatter.rs:87-96 (voxel_relief pattern to extend), seed.rs:84-118 (SeedDomain; pinned-discriminant test ~:267), voxel.rs:1907-1943 (scatter_vegetation — the per-voxel salt-and-pepper this replaces), voxel.rs:445-495 (post-dispatch modulation seam), believability.rs (ContrastMetrics; evaluate_criteria ~:319). Overlaps to reconcile in the axis table: tube_depression (voxel.rs:710-717, Lava vocab) and braided-delta gravel-bar scrub (voxel.rs:1069-1092, Gravel vocab). D-record written: D-246 (governance/decisions/architecture.md) pins all five resolved decisions + the full 32-row vocab→D-228 axis table + palette-key rule. D-228 amended to add the five new axis values (Vegetation::{Meadow,Deadfall,Lichen}, TerrainMaterial::{Hardpan,Scree}). D-246 is now the governing record for implementation; decision_ref D-227 kept as the derive-don''t-store parent.', NULL, '2026-07-02 18:26:37', '2026-07-02 18:26:37.203', '2026-07-02 18:26:37.203', NULL, 'c38329ce7eea51cd8aa53b0c5769da76', 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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'description', 'Operationalize the D-247 native-biosphere premise. Full brief appended.', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab.', NULL, '2026-07-02 20:33:09', '2026-07-02 20:33:09.514', '2026-07-02 20:33:09.514', NULL, '02a8635cdb5f42ec0a512d12804b3f35', 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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'description', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab.', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab. + +Chirality decisions resolved (2026-07-02, with Jeroen): +- Locked exemplars: Arbour=NativeCompatible, Solandar=NativeMirror, Keid=NativeCompatible, Oshima=TerraformedSterile. +- Miri assignments accepted: GJ-524=Mirror, Freyburg(GJ-684A)=Compatible, Hạ Long(GJ-124)=Compatible (native-Compatible harvest, per her rationale). +- USER OVERRIDES: Puerto Último(GJ-197)=NativeCompatible (breadbasket identity over Miri''s balance-motivated Mirror). Edict/Sirius(GJ-244A)=Airless — keep the world harsh/frozen (deliberately-inconvenient below-grade transit capital is load-bearing canon); no climate change; below-grade sealed habitat = no open-air biosphere. +- Two-gate default rule ACCEPTED for canon-silent worlds: Gate 0 (habitability from atmosphere/hydrosphere frontmatter → Airless / TerraformedSterile / Alive), Gate 1 (Compatible vs Mirror from economic_base_primary; agriculture→Compatible near-lock, penal→Mirror, research→50/50 lever). +- NAMING: Keid''s habitable body renamed Cadwal→(pending final pick; rec Linnaeus — pairs with Solander, its student naturalist; or Beagle). Capital stays ''Vance'' (already the author tribute). Miri''s IP flag (Cadwal = Vance''s exact book title) is the driver. +- Still to APPLY: the 7 additive wiki continuity edits (Miri specified exact text), the biosphere_class frontmatter+schema+importer plumbing (workstream 3), and the Cadwal→ rename across wiki + the GJ166A atlas proposal.', NULL, '2026-07-02 21:23:34', '2026-07-02 21:23:34.735', '2026-07-02 21:23:34.735', NULL, '80cb1aabee1e9defb9795e677e7ee58d', 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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'description', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab. + +Chirality decisions resolved (2026-07-02, with Jeroen): +- Locked exemplars: Arbour=NativeCompatible, Solandar=NativeMirror, Keid=NativeCompatible, Oshima=TerraformedSterile. +- Miri assignments accepted: GJ-524=Mirror, Freyburg(GJ-684A)=Compatible, Hạ Long(GJ-124)=Compatible (native-Compatible harvest, per her rationale). +- USER OVERRIDES: Puerto Último(GJ-197)=NativeCompatible (breadbasket identity over Miri''s balance-motivated Mirror). Edict/Sirius(GJ-244A)=Airless — keep the world harsh/frozen (deliberately-inconvenient below-grade transit capital is load-bearing canon); no climate change; below-grade sealed habitat = no open-air biosphere. +- Two-gate default rule ACCEPTED for canon-silent worlds: Gate 0 (habitability from atmosphere/hydrosphere frontmatter → Airless / TerraformedSterile / Alive), Gate 1 (Compatible vs Mirror from economic_base_primary; agriculture→Compatible near-lock, penal→Mirror, research→50/50 lever). +- NAMING: Keid''s habitable body renamed Cadwal→(pending final pick; rec Linnaeus — pairs with Solander, its student naturalist; or Beagle). Capital stays ''Vance'' (already the author tribute). Miri''s IP flag (Cadwal = Vance''s exact book title) is the driver. +- Still to APPLY: the 7 additive wiki continuity edits (Miri specified exact text), the biosphere_class frontmatter+schema+importer plumbing (workstream 3), and the Cadwal→ rename across wiki + the GJ166A atlas proposal.', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab. + +Chirality decisions resolved (2026-07-02, with Jeroen): +- Locked exemplars: Arbour=NativeCompatible, Solandar=NativeMirror, Keid=NativeCompatible, Oshima=TerraformedSterile. +- Miri assignments accepted: GJ-524=Mirror, Freyburg(GJ-684A)=Compatible, Hạ Long(GJ-124)=Compatible (native-Compatible harvest, per her rationale). +- USER OVERRIDES: Puerto Último(GJ-197)=NativeCompatible (breadbasket identity over Miri''s balance-motivated Mirror). Edict/Sirius(GJ-244A)=Airless — keep the world harsh/frozen (deliberately-inconvenient below-grade transit capital is load-bearing canon); no climate change; below-grade sealed habitat = no open-air biosphere. +- Two-gate default rule ACCEPTED for canon-silent worlds: Gate 0 (habitability from atmosphere/hydrosphere frontmatter → Airless / TerraformedSterile / Alive), Gate 1 (Compatible vs Mirror from economic_base_primary; agriculture→Compatible near-lock, penal→Mirror, research→50/50 lever). +- NAMING: Keid''s habitable body renamed Cadwal→(pending final pick; rec Linnaeus — pairs with Solander, its student naturalist; or Beagle). Capital stays ''Vance'' (already the author tribute). Miri''s IP flag (Cadwal = Vance''s exact book title) is the driver. +- Still to APPLY: the 7 additive wiki continuity edits (Miri specified exact text), the biosphere_class frontmatter+schema+importer plumbing (workstream 3), and the Cadwal→ rename across wiki + the GJ166A atlas proposal. + +NAMING CONFIRMED: Keid''s habitable body Cadwal → Linnaeus (the great classifier; ties to Solander/Solandar taxonomy + the ''five kingdoms''). Capital stays Vance.', NULL, '2026-07-02 21:24:12', '2026-07-02 21:24:12.734', '2026-07-02 21:24:12.734', NULL, 'b13c9f1ce1d47437fdb599e6efce7f0e', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index 3e620da0c..d4f3dec05 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -39,3 +39,106 @@ DECISIONS (user-resolved): CODE TOUCHPOINTS (Si-verified 2026-07-02): detail_scatter.rs:87-96 (voxel_relief pattern to extend), seed.rs:84-118 (SeedDomain; pinned-discriminant test ~:267), voxel.rs:1907-1943 (scatter_vegetation — the per-voxel salt-and-pepper this replaces), voxel.rs:445-495 (post-dispatch modulation seam), believability.rs (ContrastMetrics; evaluate_criteria ~:319). Overlaps to reconcile in the axis table: tube_depression (voxel.rs:710-717, Lava vocab) and braided-delta gravel-bar scrub (voxel.rs:1069-1092, Gravel vocab). D-record written: D-246 (governance/decisions/architecture.md) pins all five resolved decisions + the full 32-row vocab→D-228 axis table + palette-key rule. D-228 amended to add the five new axis values (Vegetation::{Meadow,Deadfall,Lichen}, TerrainMaterial::{Hardpan,Scree}). D-246 is now the governing record for implementation; decision_ref D-227 kept as the derive-don''t-store parent.', 'in_progress', 'high', NULL, 'server', 'D-227', '2026-06-28 05:55:34.396', '2026-07-02 18:26:37.202', NULL, '7edbe9dc6461eea90bf94ebcfe0b0687', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Biosphere register (D-247) — chirality bits, wiki continuity, biosphere_class authoring + alien-ecology vocab', 'Operationalize the D-247 native-biosphere premise. Full brief appended.', 'backlog', 'medium', 'miri', NULL, 'D-247', '2026-07-02 20:30:57.222', '2026-07-02 20:30:57.222', NULL, 'e7f137504714f54f2c0cb1a1481609d3', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Biosphere register (D-247) — chirality bits, wiki continuity, biosphere_class authoring + alien-ecology vocab', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab.', 'backlog', 'medium', 'miri', NULL, 'D-247', '2026-07-02 20:30:57.222', '2026-07-02 20:33:09.514', NULL, '648fccf9411f626aaf04a2c5f0a0c9fa', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Biosphere register (D-247) — chirality bits, wiki continuity, biosphere_class authoring + alien-ecology vocab', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab. + +Chirality decisions resolved (2026-07-02, with Jeroen): +- Locked exemplars: Arbour=NativeCompatible, Solandar=NativeMirror, Keid=NativeCompatible, Oshima=TerraformedSterile. +- Miri assignments accepted: GJ-524=Mirror, Freyburg(GJ-684A)=Compatible, Hạ Long(GJ-124)=Compatible (native-Compatible harvest, per her rationale). +- USER OVERRIDES: Puerto Último(GJ-197)=NativeCompatible (breadbasket identity over Miri''s balance-motivated Mirror). Edict/Sirius(GJ-244A)=Airless — keep the world harsh/frozen (deliberately-inconvenient below-grade transit capital is load-bearing canon); no climate change; below-grade sealed habitat = no open-air biosphere. +- Two-gate default rule ACCEPTED for canon-silent worlds: Gate 0 (habitability from atmosphere/hydrosphere frontmatter → Airless / TerraformedSterile / Alive), Gate 1 (Compatible vs Mirror from economic_base_primary; agriculture→Compatible near-lock, penal→Mirror, research→50/50 lever). +- NAMING: Keid''s habitable body renamed Cadwal→(pending final pick; rec Linnaeus — pairs with Solander, its student naturalist; or Beagle). Capital stays ''Vance'' (already the author tribute). Miri''s IP flag (Cadwal = Vance''s exact book title) is the driver. +- Still to APPLY: the 7 additive wiki continuity edits (Miri specified exact text), the biosphere_class frontmatter+schema+importer plumbing (workstream 3), and the Cadwal→ rename across wiki + the GJ166A atlas proposal.', 'backlog', 'medium', 'miri', NULL, 'D-247', '2026-07-02 20:30:57.222', '2026-07-02 21:23:34.730', NULL, 'd575ad0ccc78d862800236759d1af46a', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +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 ('06FJ91R8RV38BJXGB2V55FP3B8', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Biosphere register (D-247) — chirality bits, wiki continuity, biosphere_class authoring + alien-ecology vocab', 'Operationalize the D-247 native-biosphere premise. Full brief appended. + +--- +BRIEF (2026-07-02) — governing record: D-247 (governance/decisions/content.md); consumer: D-246 / T-1084 mosaic register. + +WHAT THIS IS +D-247 established that habitable Reach worlds were alive before humans (the settlement timeline forbids terraforming from dead rock), and that an AUTHORED per-world chirality bit gates whether native life is edible. This ticket turns that canon into data + reconciled wiki + the vocabulary the mosaic needs. Owner: Miri (worldbuilding/continuity). Stakeholder: Burnelli (food-import economics). Co-maintainer for the vocab: Araminta (visual grammar, per the T-995 ObjectTag precedent). + +THE MODEL (locked in D-247 — do not re-litigate, apply it): +- `biosphere_class ∈ {NativeCompatible, NativeMirror, TerraformedSterile, Airless}`. It is an AUTHORED INPUT, never seed-derived — it follows a world''s economic/cultural identity. +- Compatible ("levo") = edible; coupling *permits* an authored hazard but does not force it (most are benign gardens). +- Mirror ("dextro") = inedible but *guaranteed* inert (can''t infect/poison — safe to study). +- "Safe" ≠ "edible" — decoupled. Bio-safe is guaranteed on Mirror; food-safe is Compatible-only. +- Achiral seam (always works either way): breathe air, drink water, mine ore, burn/build with native biomass. Only eat/metabolize/medicate/infect/chirally-poison are gated. +- ~50/50 Compatible:Mirror is a DESIGN TARGET across canon-silent worlds, not a per-world draw. + +WORKSTREAMS +1. CHIRALITY ASSIGNMENT (Miri). Give every inhabited body a biosphere_class by authored identity: principled default from `economic_base_primary` + `planet_class` (food/drink exporters → Compatible; dead+maintained-atmosphere → TerraformedSterile), explicit overrides where canon speaks. Locked exemplars: Arbour (GJ-338B)=Compatible; Solandar (GJ-827)=Mirror; Keid (GJ-166A)=Compatible (preserved *from exploitation* of edible alien life — the stronger conservation motive; Solandar is the Mirror-inert preserve); Oshima (GJ-79)=TerraformedSterile. Hit the ~50/50 target across the silent remainder. +2. WIKI CONTINUITY (Miri). Reconcile the entries that already gesture at native biochemistry so each is consistent with its assigned bit + the mechanism: GJ-827, GJ-524, GJ-166A, GJ-684A, GJ-197, GJ-79, GJ-124 (resolve whether Hạ Long''s "protein" is native-Compatible harvest or Earth aquaculture). Additive/explanatory, not contradictory — preserve deliberately-vague lines as intentional. +3. biosphere_class AUTHORING PATH (asset-pipeline discipline — schema + source + values together, per .claude/rules/asset-pipeline.md). Add `biosphere_class` to wiki body frontmatter (where `substrate:` already lives), a nullable `bodies.biosphere_class` column in systems-schema.sql + MIGRATION_SQL, and importer read. NOT needed for D-246; lands with this pass. +4. ALIEN-ECOLOGY VOCABULARY (Miri + Araminta) — feeds T-1084 directly. The mosaic palette is keyed on surface-class × climate-biome × biosphere-register (D-246 Palette-key). Provide the micro-habitat NAMES per biome × register: native-mirror niches get *alien* clade names (Solandar''s "five kingdoms" flavor); introduced-Earth niches get Earth names (wheatfield, orchard, pasture). Fill the fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) per climate-biome (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …). DETERMINISM: the vocab must be fixed before T-1084 generates its first world (a later addition re-rolls affected voxels). Table size is not a perf/storage concern — author generously but to taste (~100k is headroom, not a target). + +BURNELLI (stakeholder, follow-on): a food-supply demand primitive keyed on biosphere_class (Mirror + frontier → import dependency; Compatible → local-food capable). Phase-2 economy consumer; does not block this ticket. + +CONSTRAINTS +- Authored input, not derivation (Arbour''s estate food must never be coin-flipped inedible). +- Semantic axes stay clade-agnostic (Forest = canopy whether oak or mega-fungus); the register skins niches, it does not change gameplay semantics. +- The mosaic geometry is chirality-blind — this work does NOT touch voxel geometry, only edibility/economy/naming/vocab. + +Chirality decisions resolved (2026-07-02, with Jeroen): +- Locked exemplars: Arbour=NativeCompatible, Solandar=NativeMirror, Keid=NativeCompatible, Oshima=TerraformedSterile. +- Miri assignments accepted: GJ-524=Mirror, Freyburg(GJ-684A)=Compatible, Hạ Long(GJ-124)=Compatible (native-Compatible harvest, per her rationale). +- USER OVERRIDES: Puerto Último(GJ-197)=NativeCompatible (breadbasket identity over Miri''s balance-motivated Mirror). Edict/Sirius(GJ-244A)=Airless — keep the world harsh/frozen (deliberately-inconvenient below-grade transit capital is load-bearing canon); no climate change; below-grade sealed habitat = no open-air biosphere. +- Two-gate default rule ACCEPTED for canon-silent worlds: Gate 0 (habitability from atmosphere/hydrosphere frontmatter → Airless / TerraformedSterile / Alive), Gate 1 (Compatible vs Mirror from economic_base_primary; agriculture→Compatible near-lock, penal→Mirror, research→50/50 lever). +- NAMING: Keid''s habitable body renamed Cadwal→(pending final pick; rec Linnaeus — pairs with Solander, its student naturalist; or Beagle). Capital stays ''Vance'' (already the author tribute). Miri''s IP flag (Cadwal = Vance''s exact book title) is the driver. +- Still to APPLY: the 7 additive wiki continuity edits (Miri specified exact text), the biosphere_class frontmatter+schema+importer plumbing (workstream 3), and the Cadwal→ rename across wiki + the GJ166A atlas proposal. + +NAMING CONFIRMED: Keid''s habitable body Cadwal → Linnaeus (the great classifier; ties to Solander/Solandar taxonomy + the ''five kingdoms''). Capital stays Vance.', 'backlog', 'medium', 'miri', NULL, 'D-247', '2026-07-02 20:30:57.222', '2026-07-02 21:24:12.734', NULL, '4465986381752d527f9ac2cd9e58311f', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/server/data/mosaic_constants.toml b/server/data/mosaic_constants.toml new file mode 100644 index 000000000..0bd689dc5 --- /dev/null +++ b/server/data/mosaic_constants.toml @@ -0,0 +1,254 @@ +# Intra-class micro-habitat mosaic palettes — D-246, T-1084. +# +# SOURCE-CANONICAL MIRROR. The authoritative runtime source today is the embedded +# `atlas::mosaic` const tables; this file mirrors them and is the file to edit when +# tuning weights (the same embedded-default + TOML-mirror pattern as +# `climate_constants.toml`; runtime loading lands with production tuning). Keep the +# two in sync by hand until then. +# +# Each entry is a micro-habitat = a distinct D-228 axis-triple the mosaic can paint, +# plus its relative `weight` and moisture `affinity` (any | wet | dry). Display names +# are derived labels (D-228) — scree/talus and outcrop/ledge collapse to one triple. +# +# This is the TEMPERATE / NATIVE baseline register. Biome variants (savanna, tundra, +# boreal) and the introduced-Earth register extend the table via T-1085, before +# generation (a later addition re-rolls affected voxels — D-246 determinism rule). +# +# v1 (T-1084) applies only the Wetland / Forest / Grassland palettes, and only their +# vegetation + water — terrain material is left to the family generator so the D-239 +# §8 lithology law is untouched. The Sand / Rock / Gravel / Lava palettes below are +# authored for the §8-aware material follow-up. + +[[wetland]] +name = "clearing" +terrain = "Wetland" +vegetation = "Barren" +water = "Dry" +weight = 15 +affinity = "dry" + +[[wetland]] +name = "copse" +terrain = "Wetland" +vegetation = "Forest" +water = "Dry" +weight = 10 +affinity = "dry" + +[[wetland]] +name = "creek" +terrain = "Wetland" +vegetation = "Barren" +water = "Shallow" +weight = 10 +affinity = "wet" + +[[wetland]] +name = "meadow" +terrain = "Wetland" +vegetation = "Meadow" +water = "Dry" +weight = 15 +affinity = "dry" + +[[wetland]] +name = "bog" +terrain = "Wetland" +vegetation = "Scrub" +water = "Shallow" +weight = 12 +affinity = "wet" + +[[wetland]] +name = "marsh" +terrain = "Wetland" +vegetation = "Grass" +water = "Shallow" +weight = 12 +affinity = "wet" + +[[forest]] +name = "dense stand" +terrain = "Soil" +vegetation = "Forest" +water = "Dry" +weight = 40 +affinity = "any" + +[[forest]] +name = "glade" +terrain = "Soil" +vegetation = "Grass" +water = "Dry" +weight = 15 +affinity = "dry" + +[[forest]] +name = "deadfall" +terrain = "Soil" +vegetation = "Deadfall" +water = "Dry" +weight = 10 +affinity = "any" + +[[forest]] +name = "brook" +terrain = "Soil" +vegetation = "Thicket" +water = "Shallow" +weight = 10 +affinity = "wet" + +[[forest]] +name = "fern undergrowth" +terrain = "Soil" +vegetation = "Thicket" +water = "Dry" +weight = 15 +affinity = "any" + +[[grassland]] +name = "tussock" +terrain = "Soil" +vegetation = "Grass" +water = "Dry" +weight = 40 +affinity = "any" + +[[grassland]] +name = "scrub island" +terrain = "Soil" +vegetation = "Scrub" +water = "Dry" +weight = 18 +affinity = "dry" + +[[grassland]] +name = "wildflower" +terrain = "Soil" +vegetation = "Meadow" +water = "Dry" +weight = 17 +affinity = "dry" + +[[grassland]] +name = "dry wash" +terrain = "Gravel" +vegetation = "Barren" +water = "Dry" +weight = 10 +affinity = "wet" + +# ── Follow-up (§8-aware material pass): Sand / Rock / Gravel / Lava ────────────── + +[[sand]] +name = "dune crest" +terrain = "Sand" +vegetation = "Barren" +water = "Dry" +weight = 25 +affinity = "dry" + +[[sand]] +name = "slack" +terrain = "Sand" +vegetation = "Grass" +water = "Dry" +weight = 15 +affinity = "wet" + +[[sand]] +name = "hardpan" +terrain = "Hardpan" +vegetation = "Barren" +water = "Dry" +weight = 20 +affinity = "dry" + +[[sand]] +name = "oasis" +terrain = "Sand" +vegetation = "Forest" +water = "Shallow" +weight = 5 +affinity = "wet" + +[[sand]] +name = "scrub clump" +terrain = "Sand" +vegetation = "Scrub" +water = "Dry" +weight = 15 +affinity = "any" + +[[rock]] +name = "outcrop" +terrain = "Rock" +vegetation = "Barren" +water = "Dry" +weight = 45 +affinity = "any" + +[[rock]] +name = "scree" +terrain = "Scree" +vegetation = "Barren" +water = "Dry" +weight = 30 +affinity = "any" + +[[rock]] +name = "lichen" +terrain = "Rock" +vegetation = "Lichen" +water = "Dry" +weight = 25 +affinity = "any" + +[[gravel]] +name = "bar" +terrain = "Gravel" +vegetation = "Barren" +water = "Dry" +weight = 45 +affinity = "any" + +[[gravel]] +name = "active channel" +terrain = "Gravel" +vegetation = "Barren" +water = "Shallow" +weight = 25 +affinity = "wet" + +[[gravel]] +name = "vegetated island" +terrain = "Gravel" +vegetation = "Scrub" +water = "Dry" +weight = 30 +affinity = "dry" + +[[lava]] +name = "fresh sheet" +terrain = "Lava" +vegetation = "Barren" +water = "Dry" +weight = 50 +affinity = "any" + +[[lava]] +name = "weathered crust" +terrain = "Lava" +vegetation = "Lichen" +water = "Dry" +weight = 30 +affinity = "any" + +[[lava]] +name = "pioneer scrub" +terrain = "Lava" +vegetation = "Scrub" +water = "Dry" +weight = 20 +affinity = "dry" diff --git a/server/src/atlas/believability.rs b/server/src/atlas/believability.rs index 5f4543801..0eab85d95 100644 --- a/server/src/atlas/believability.rs +++ b/server/src/atlas/believability.rs @@ -112,6 +112,12 @@ pub struct ContrastMetrics { /// voxel tier carries mid-scale relief. Measured over the 0.25–2 km band a character /// traverses — not the single 64 m sample chunk, which is too narrow to register it. pub voxel_relief_m: i32, + /// Minimum distinct micro-habitat axis-triples (terrain × vegetation × water) found + /// in any single sampled VEGETATED-land district (T-1084, D-246). The D-245 intra-class + /// variety screen: a vegetated patch must show ≥K distinct micro-features, never 100% + /// one — near 1 on the pre-mosaic uniform world ("Wetland 100%"), ≥3 once the mosaic + /// textures the class. Barren / water patches are excluded (legitimately uniform). + pub micro_habitat_distinct: usize, } /// Coherence checks — does the world render as a *caused* place? Pass/fail counts @@ -181,8 +187,9 @@ pub fn analyze( .values() .map(|d| format!("{:?}", d.vegetation_class)), ), - terrain_materials: 0, // filled from the voxel sample below - voxel_relief_m: 0, // filled from the relief transect below + terrain_materials: 0, // filled from the voxel sample below + voxel_relief_m: 0, // filled from the relief transect below + micro_habitat_distinct: 0, // filled from the per-district triple sets below }; // ── Voxel-derived metrics over a deterministic seeded spread of districts ── @@ -196,6 +203,9 @@ pub fn analyze( // Mid-scale relief accumulators (T-1081): sum of per-district transect ranges over // land districts, and the count contributing, → mean within-district relief. let (mut relief_range_sum, mut relief_district_count) = (0i64, 0i64); + // Intra-class variety (T-1084): the minimum distinct micro-habitat triples over the + // sampled vegetated-land districts — the worst patch, per the D-245 per-patch floor. + let mut min_habitat_distinct = usize::MAX; let keys: Vec = districts.keys().copied().collect(); for idx in sample_indices(world_seed, keys.len(), VOXEL_SAMPLE_DISTRICTS) { @@ -210,6 +220,7 @@ pub fn analyze( // Elevation accumulators for the drainage-monotonicity proxy. let (mut wet_elev_sum, mut wet_n) = (0i64, 0i64); let (mut dry_elev_sum, mut dry_n) = (0i64, 0i64); + let mut habitat_triples: BTreeSet = BTreeSet::new(); for sx in (0..CHUNK_M as usize).step_by(VOXEL_SAMPLE_STRIDE) { for sy in (0..CHUNK_M as usize).step_by(VOXEL_SAMPLE_STRIDE) { @@ -217,6 +228,10 @@ pub fn analyze( let ty = chunk.1 * CHUNK_M + sy as i32; let col = derive_voxel_column(world_seed, body_id, prof, &ctx, tx, ty); terrain_set.insert(format!("{:?}", col.terrain)); + habitat_triples.insert(format!( + "{:?}/{:?}/{:?}", + col.terrain, col.vegetation, col.water + )); if col.vegetation != Vegetation::Barren { any_veg = true; } @@ -231,6 +246,12 @@ pub fn analyze( } } + // Intra-class micro-habitat variety (T-1084): distinct habitat triples in this + // patch; the report tracks the worst (minimum) vegetated patch. + if any_veg { + min_habitat_distinct = min_habitat_distinct.min(habitat_triples.len()); + } + // Water coherence (T-1082): water-present districts must render wet voxels. if prof.ocean_fraction_q >= WATER_PRESENCE_Q || ctx.has_active_channel { coh.water_districts += 1; @@ -294,6 +315,11 @@ pub fn analyze( } else { 0 }; + let micro_habitat_distinct = if min_habitat_distinct == usize::MAX { + 0 + } else { + min_habitat_distinct + }; BelievabilityReport { body_id: body_id.to_string(), @@ -303,6 +329,7 @@ pub fn analyze( contrast: ContrastMetrics { terrain_materials: terrain_set.len(), voxel_relief_m, + micro_habitat_distinct, ..contrast_scalar }, coherence: coh, @@ -346,6 +373,14 @@ pub fn evaluate_criteria(r: &BelievabilityReport) -> Vec { pass: c.terrain_materials >= 2, detail: format!("{} distinct materials", c.terrain_materials), }, + Criterion { + name: "intra-class variety", + pass: c.micro_habitat_distinct >= 3, + detail: format!( + "min {} distinct micro-habitats per vegetated patch", + c.micro_habitat_distinct + ), + }, Criterion { name: "water renders wet", pass: pct(h.water_districts_wet, h.water_districts) >= 50, diff --git a/server/src/atlas/detail_scatter.rs b/server/src/atlas/detail_scatter.rs index a23271491..207ccffb0 100644 --- a/server/src/atlas/detail_scatter.rs +++ b/server/src/atlas/detail_scatter.rs @@ -32,6 +32,11 @@ const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0, 4_096.0]; /// the district size so the relief never competes with `elev_q`'s district-scale role. const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0]; +/// Sub-chunk mosaic octave wavelengths in metres — the ≈8–64 m band, finer than the +/// [`VOXEL_OCTAVE_WAVELENGTHS_M`] sub-district band, so a micro-habitat patch reads as +/// a distinct blob *inside* a single 64 m chunk (T-1084, D-246). Coarsest first. +const MOSAIC_OCTAVE_WAVELENGTHS_M: [f64; 4] = [64.0, 32.0, 16.0, 8.0]; + /// Deterministic lattice value in `[-1, 1)` for an integer noise cell. #[inline] fn lattice(seed: u64, ix: i64, iy: i64) -> f64 { @@ -95,6 +100,35 @@ pub fn voxel_relief(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64) ) } +/// The sub-chunk micro-habitat **selection field** in `[0, 1]` — a smooth, +/// body-global, position-keyed multi-octave value-noise scalar the mosaic pass +/// thresholds against a per-class palette's cumulative weights (T-1084, D-246). +/// +/// Unlike [`voxel_relief`] this is not an elevation perturbation: it carries no +/// envelope and no ridge folding — it is a plain fBm remapped to `[0, 1]` whose only +/// job is to pick *which* micro-habitat a voxel lands in, as spatially-coherent blobs +/// (low-frequency → patches, never per-tile salt-and-pepper). The seed must be +/// **body-global** (constant across the body); the position `(wx, wy)` carries the +/// variation, exactly as for [`voxel_relief`]. +pub fn voxel_mosaic(seed: u64, wx: f64, wy: f64) -> f64 { + let mut sum = 0.0; + let mut amp = 1.0; + let mut norm = 0.0; + for (i, &wl) in MOSAIC_OCTAVE_WAVELENGTHS_M.iter().enumerate() { + let n = value_noise( + seed.wrapping_add((i as u64).wrapping_mul(0x1000)), + wx, + wy, + wl, + ); + sum += n * amp; + norm += amp; + amp *= 0.5; + } + let fbm = sum / norm; // ≈ [-1, 1] + ((fbm + 1.0) * 0.5).clamp(0.0, 1.0) +} + /// Shared adaptive-fBm core for [`terrain_detail`] and [`voxel_relief`] — the only /// difference between the two tiers is the octave wavelength band. Returns the /// enveloped, ruggedness-modulated perturbation (roughly `[-envelope, +envelope]`). @@ -235,4 +269,48 @@ mod tests { max - min ); } + + // ── voxel_mosaic (T-1084): sub-chunk selection field ───────────────────── + + #[test] + fn voxel_mosaic_deterministic_and_in_range() { + let a = voxel_mosaic(42, 12_345.0, -6_789.0); + let b = voxel_mosaic(42, 12_345.0, -6_789.0); + assert_eq!(a, b); + for i in 0..400 { + let v = voxel_mosaic(99, i as f64 * 137.0, i as f64 * -91.0); + assert!((0.0..=1.0).contains(&v), "v={v} out of [0,1] at {i}"); + } + } + + #[test] + fn voxel_mosaic_varies_at_sub_chunk_scale() { + // The mosaic must produce DISTINCT values within a single 64 m chunk — the + // blob scale that reads as patches, not the district-scale voxel_relief band. + // Steps stay well under 64 m and non-aligned to the octave wavelengths. + let seed = 7; + let vals: Vec = (0..16) + .map(|i| voxel_mosaic(seed, i as f64 * 3.7, i as f64 * 2.9)) + .collect(); + let min = vals.iter().cloned().fold(f64::INFINITY, f64::min); + let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + assert!( + max - min > 0.1, + "mosaic field is near-flat inside a chunk: range {}", + max - min + ); + } + + #[test] + fn voxel_mosaic_is_smooth_not_salt_and_pepper() { + // C¹ value noise → a small position step is a small field change, so patches + // are coherent blobs, never the per-tile scatter the mosaic replaces. + let base = voxel_mosaic(11, 5_000.0, 5_000.0); + let near = voxel_mosaic(11, 5_000.5, 5_000.0); + assert!( + (base - near).abs() < 0.05, + "0.5 m step jumped by {}", + (base - near).abs() + ); + } } diff --git a/server/src/atlas/mod.rs b/server/src/atlas/mod.rs index b8359f660..8bec7dcf2 100644 --- a/server/src/atlas/mod.rs +++ b/server/src/atlas/mod.rs @@ -21,6 +21,7 @@ pub mod gen_queue; pub mod heightmap; pub mod layer1; pub mod layer_proxy; +pub mod mosaic; pub mod plugin; pub mod region_profile; pub mod road_graph; diff --git a/server/src/atlas/mosaic.rs b/server/src/atlas/mosaic.rs new file mode 100644 index 000000000..da8a90fbc --- /dev/null +++ b/server/src/atlas/mosaic.rs @@ -0,0 +1,295 @@ +//! Intra-class micro-habitat mosaic (D-246, T-1084) — the sub-chunk palette that +//! modulates the D-228 axes into a spatially-coherent, relief/moisture-conditioned +//! mosaic of micro-habitats within a single terrain class. +//! +//! Mechanism (D-246): [`detail_scatter::voxel_mosaic`](crate::atlas::detail_scatter::voxel_mosaic) +//! supplies a smooth, body-global selection field in `[0, 1]`; this module maps a +//! voxel's base surface-class to a weighted palette of micro-habitats and picks one +//! by thresholding that field against cumulative weights — conditioned on a local +//! wetness signal (relief micro-thalweg + district moisture) so wet habitats +//! (creek / bog / marsh) land in the lows and dry ones (meadow / glade / clearing) on +//! the rises. Selection is **integer basis-points** (D-010): the f64 field and +//! wetness are the sanctioned positional inputs, truncated before the integer decision. +//! +//! Micro-habitats are distinct D-228 **axis-triples**, not names — several display +//! names (scree/talus, outcrop/ledge) collapse to one triple, the name being a +//! downstream derived label (D-228). +//! +//! Data residency (D-246): the palette is source-canonical in +//! `server/data/mosaic_constants.toml`; the embedded consts here mirror it and are +//! the authoritative runtime source today — the same embedded-default + TOML-mirror +//! pattern as `climate_constants.toml` (runtime loading lands with production tuning). +//! This is the **temperate / native baseline** register; biome variants +//! (savanna / tundra / boreal) and the introduced-Earth register extend the table via +//! T-1085 before generation. + +use crate::atlas::voxel::{TerrainMaterial, Vegetation, Water}; + +/// How the local wetness signal reweights a micro-habitat. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MoistureAffinity { + /// Unconditioned — flat weight across wet and dry. + Any, + /// Favoured in the wet lows (creek / bog / marsh / oasis / active channel). + Wet, + /// Favoured on the dry rises (meadow / glade / clearing / dune crest / hardpan). + Dry, +} + +/// One micro-habitat: a distinct D-228 axis-triple the mosaic can paint, plus its +/// relative weight and moisture affinity. `name` is documentation / a derived-label +/// seed only — it is never stored on the voxel. +#[derive(Debug, Clone, Copy)] +pub struct MicroHabitat { + pub name: &'static str, + pub terrain: TerrainMaterial, + pub vegetation: Vegetation, + pub water: Water, + pub weight: i32, + pub affinity: MoistureAffinity, +} + +/// The surface-class palette key (D-246) — derived from the base voxel's terrain + +/// dominant vegetation, never stored. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SurfaceClass { + Wetland, + Forest, + Grassland, + Sand, + Rock, + Gravel, + Lava, +} + +impl SurfaceClass { + /// Palette key from the family generator's base output (D-246 Palette key): + /// terrain material selects directly, except `Soil` splits Forest vs Grassland + /// by the district's dominant vegetation. `Hardpan`/`Scree` never arrive as a + /// *base* material (they are mosaic outputs) but map defensively. + pub fn from_base(terrain: TerrainMaterial, dominant_vegetation: Vegetation) -> Self { + match terrain { + TerrainMaterial::Wetland => SurfaceClass::Wetland, + TerrainMaterial::Sand | TerrainMaterial::Hardpan => SurfaceClass::Sand, + TerrainMaterial::Rock | TerrainMaterial::Scree => SurfaceClass::Rock, + TerrainMaterial::Gravel => SurfaceClass::Gravel, + TerrainMaterial::Lava => SurfaceClass::Lava, + TerrainMaterial::Soil => { + if matches!(dominant_vegetation, Vegetation::Forest) { + SurfaceClass::Forest + } else { + SurfaceClass::Grassland + } + } + } + } +} + +// Shorthand for the const tables below. +use MoistureAffinity::{Any, Dry, Wet}; +use TerrainMaterial as T; +use Vegetation as V; +use Water as W; + +const fn h( + name: &'static str, + terrain: TerrainMaterial, + vegetation: Vegetation, + water: Water, + weight: i32, + affinity: MoistureAffinity, +) -> MicroHabitat { + MicroHabitat { + name, + terrain, + vegetation, + water, + weight, + affinity, + } +} + +// ── Temperate / native baseline palettes (mirror of mosaic_constants.toml) ────── + +const WETLAND: &[MicroHabitat] = &[ + h("clearing", T::Wetland, V::Barren, W::Dry, 15, Dry), + h("copse", T::Wetland, V::Forest, W::Dry, 10, Dry), + h("creek", T::Wetland, V::Barren, W::Shallow, 10, Wet), + h("meadow", T::Wetland, V::Meadow, W::Dry, 15, Dry), + h("bog", T::Wetland, V::Scrub, W::Shallow, 12, Wet), + h("marsh", T::Wetland, V::Grass, W::Shallow, 12, Wet), +]; + +const FOREST: &[MicroHabitat] = &[ + h("dense stand", T::Soil, V::Forest, W::Dry, 40, Any), + h("glade", T::Soil, V::Grass, W::Dry, 15, Dry), + h("deadfall", T::Soil, V::Deadfall, W::Dry, 10, Any), + h("brook", T::Soil, V::Thicket, W::Shallow, 10, Wet), + h("fern undergrowth", T::Soil, V::Thicket, W::Dry, 15, Any), +]; + +const GRASSLAND: &[MicroHabitat] = &[ + h("tussock", T::Soil, V::Grass, W::Dry, 40, Any), + h("scrub island", T::Soil, V::Scrub, W::Dry, 18, Dry), + h("wildflower", T::Soil, V::Meadow, W::Dry, 17, Dry), + h("dry wash", T::Gravel, V::Barren, W::Dry, 10, Wet), +]; + +const SAND: &[MicroHabitat] = &[ + h("dune crest", T::Sand, V::Barren, W::Dry, 25, Dry), + h("slack", T::Sand, V::Grass, W::Dry, 15, Wet), + h("hardpan", T::Hardpan, V::Barren, W::Dry, 20, Dry), + h("oasis", T::Sand, V::Forest, W::Shallow, 5, Wet), + h("scrub clump", T::Sand, V::Scrub, W::Dry, 15, Any), +]; + +const ROCK: &[MicroHabitat] = &[ + h("outcrop", T::Rock, V::Barren, W::Dry, 45, Any), + h("scree", T::Scree, V::Barren, W::Dry, 30, Any), + h("lichen", T::Rock, V::Lichen, W::Dry, 25, Any), +]; + +const GRAVEL: &[MicroHabitat] = &[ + h("bar", T::Gravel, V::Barren, W::Dry, 45, Any), + h("active channel", T::Gravel, V::Barren, W::Shallow, 25, Wet), + h("vegetated island", T::Gravel, V::Scrub, W::Dry, 30, Dry), +]; + +const LAVA: &[MicroHabitat] = &[ + h("fresh sheet", T::Lava, V::Barren, W::Dry, 50, Any), + h("weathered crust", T::Lava, V::Lichen, W::Dry, 30, Any), + h("pioneer scrub", T::Lava, V::Scrub, W::Dry, 20, Dry), +]; + +/// The embedded palette for a surface-class (temperate / native baseline). +pub fn palette(class: SurfaceClass) -> &'static [MicroHabitat] { + match class { + SurfaceClass::Wetland => WETLAND, + SurfaceClass::Forest => FOREST, + SurfaceClass::Grassland => GRASSLAND, + SurfaceClass::Sand => SAND, + SurfaceClass::Rock => ROCK, + SurfaceClass::Gravel => GRAVEL, + SurfaceClass::Lava => LAVA, + } +} + +/// Moisture-conditioned effective weight of one entry, in the same integer units as +/// `weight`. `wet_bp` is the local wetness in basis-points (0..10000). Wet/Dry +/// affinities scale within `[0.2×, 1.0×]` (a 0.2 floor so a habitat can still appear +/// off-affinity); `Any` is flat. +fn effective_weight(hab: &MicroHabitat, wet_bp: i32) -> i64 { + let factor_bp: i32 = match hab.affinity { + MoistureAffinity::Any => 10_000, + MoistureAffinity::Wet => 2_000 + 8 * wet_bp / 10, // 2000..10000 + MoistureAffinity::Dry => 2_000 + 8 * (10_000 - wet_bp) / 10, + }; + (hab.weight as i64 * factor_bp as i64) / 10_000 +} + +/// Pick a micro-habitat from `palette` using the mosaic selection field and a local +/// wetness signal. Integer basis-points (D-010): `field` and `wetness` are the +/// sanctioned f64 positional inputs, truncated to bp before the integer decision. +/// +/// - `field`: the [`voxel_mosaic`](crate::atlas::detail_scatter::voxel_mosaic) scalar in `[0, 1]`. +/// - `wetness`: local wet/dry signal in `[0, 1]` (relief micro-thalweg + moisture). +pub fn select(palette: &[MicroHabitat], field: f64, wetness: f64) -> &MicroHabitat { + debug_assert!(!palette.is_empty(), "mosaic palette must be non-empty"); + let wet_bp = (wetness.clamp(0.0, 1.0) * 10_000.0) as i32; // truncate (D-010) + let total: i64 = palette + .iter() + .map(|hab| effective_weight(hab, wet_bp)) + .sum::() + .max(1); + let field_bp = (field.clamp(0.0, 1.0) * 10_000.0) as i64; // truncate (D-010) + let mut threshold = field_bp * total / 10_000; + for hab in palette { + let w = effective_weight(hab, wet_bp); + if threshold < w { + return hab; + } + threshold -= w; + } + // field == 1.0 edge → last bucket. + palette.last().expect("palette non-empty") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_base_maps_soil_by_vegetation() { + assert_eq!( + SurfaceClass::from_base(T::Soil, V::Forest), + SurfaceClass::Forest + ); + assert_eq!( + SurfaceClass::from_base(T::Soil, V::Grass), + SurfaceClass::Grassland + ); + assert_eq!( + SurfaceClass::from_base(T::Wetland, V::Barren), + SurfaceClass::Wetland + ); + assert_eq!( + SurfaceClass::from_base(T::Lava, V::Barren), + SurfaceClass::Lava + ); + } + + #[test] + fn every_palette_nonempty_and_positively_weighted() { + for class in [ + SurfaceClass::Wetland, + SurfaceClass::Forest, + SurfaceClass::Grassland, + SurfaceClass::Sand, + SurfaceClass::Rock, + SurfaceClass::Gravel, + SurfaceClass::Lava, + ] { + let p = palette(class); + assert!(!p.is_empty(), "{class:?} palette empty"); + assert!( + p.iter().all(|hab| hab.weight > 0), + "{class:?} has a non-positive weight" + ); + } + } + + #[test] + fn select_is_deterministic_and_spans_the_palette() { + let p = palette(SurfaceClass::Wetland); + assert_eq!(select(p, 0.37, 0.5).name, select(p, 0.37, 0.5).name); + let names: std::collections::BTreeSet<_> = (0..100) + .map(|i| select(p, i as f64 / 100.0, 0.5).name) + .collect(); + assert!( + names.len() >= 3, + "selection collapsed to {} habitats", + names.len() + ); + } + + #[test] + fn wetness_favours_wet_habitats_over_dry_signal() { + let p = palette(SurfaceClass::Wetland); + let wet_count = |wetness: f64| { + (0..100) + .filter(|i| { + matches!( + select(p, *i as f64 / 100.0, wetness).affinity, + MoistureAffinity::Wet + ) + }) + .count() + }; + assert!( + wet_count(0.95) > wet_count(0.05), + "wet conditioning did not favour wet habitats: {} vs {}", + wet_count(0.95), + wet_count(0.05) + ); + } +} diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index c71f12067..46311db1d 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -87,6 +87,12 @@ pub enum TerrainMaterial { Wetland = 4, /// Lava — sheets/shield slopes + tubes, immature drainage (D-239 §8). Lava = 5, + /// Hardpan — compacted flat crust (D-246 mosaic). Hard, fast footing; does not + /// slump to an angle of repose like sand. + Hardpan = 6, + /// Scree — loose angular rock debris (D-246 mosaic). Unstable footing; the + /// derived name is "talus" at a cliff foot, "scree" on an open slope. + Scree = 7, } /// Built surface over natural ground (D-228). @@ -128,6 +134,15 @@ pub enum Vegetation { Crop = 5, /// Cleared — formerly vegetated, stripped (player/sim action). Cleared = 6, + /// Meadow — forb-rich flowering herbaceous (D-246 mosaic). Higher cover than + /// Grass; the wetland *meadow* and grassland *wildflower*. + Meadow = 7, + /// Deadfall — dead woody debris (D-246 mosaic). Obstruction + fire fuel, no + /// canopy concealment. + Deadfall = 8, + /// Lichen — crustose/pioneer biological crust on rock/lava (D-246 mosaic). + /// Distinct from Barren; the rock *lichen* and lava *weathered crust*. + Lichen = 9, } impl Vegetation { @@ -464,6 +479,8 @@ pub fn derive_voxel_column( // WaterBody stays at sea level. The flat families are the ones that read flat and // need invented relief; their only feature is a 3–15 m channel, narrow enough that // the smooth relief shifts it bodily with its banks. + // Captured for the mosaic wetness signal below (stays 0 where no relief is added). + let mut relief_signal = 0.0f64; let takes_mid_scale_relief = matches!( family, MorphologyFamily::AlluvialPlain @@ -492,6 +509,58 @@ pub fn derive_voxel_column( ); let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010) column.elevation_m = (column.elevation_m + relief_m).max(0); + relief_signal = relief; // ~[-0.5, +0.5]; negative = mid-scale valley (wetter) + } + + // ── 3c. Intra-class micro-habitat mosaic (T-1084, D-246) ────────────── + // Within the vegetated / wet land classes (Wetland, Forest, Grassland), modulate + // VEGETATION + WATER into a spatially-coherent mosaic of micro-habitats: a + // body-global selection field (SeedDomain::VoxelMosaic, ≈8–64 m band) picks a + // habitat from the surface-class palette, conditioned on a local wetness signal + // (mid-scale relief thalweg + district moisture) so wet habitats settle in the + // lows. This is the motivating fix for the probe's uniform "Wetland 100%". + // + // v1 scope: terrain MATERIAL is left exactly as the family generator set it, so + // the D-239 §8 lithology law holds untouched. The material-variant micro-habitats + // (hardpan / scree / dry-wash gravel) and the extreme classes (Rock / Sand / + // Gravel / Lava) need shape-aware §8 reconciliation and are a follow-up. A + // climatically-barren district stays barren (D-245 §5 climate-appropriateness). + // Within these classes this supersedes the per-voxel `scatter_vegetation` scatter. + if !matches!(family, MorphologyFamily::WaterBody) { + use crate::atlas::mosaic::{self, SurfaceClass}; + let dominant_veg = Vegetation::from_vegetation_class(district_eff.vegetation_class); + let class = SurfaceClass::from_base(column.terrain, dominant_veg); + let apply = match class { + SurfaceClass::Wetland | SurfaceClass::Forest => true, + // Barren grassland (cold / arid) stays barren — no imposed grass. + SurfaceClass::Grassland => !matches!(dominant_veg, Vegetation::Barren), + // Rock / Sand / Gravel / Lava are material-driven — a §8-aware follow-up. + _ => false, + }; + if apply { + let mosaic_seed = SeedChain::for_body(world_seed, body_id) + .derive(SeedDomain::VoxelMosaic, 0) + .seed(); + let field = crate::atlas::detail_scatter::voxel_mosaic( + mosaic_seed, + voxel_x as f64, + voxel_y as f64, + ); + // Wetness in [0,1]: district moisture, raised in the mid-scale lows and + // lowered on the rises (relief_signal negative in valleys). + let moisture = (district_eff.moisture_q as f64 / 100.0).clamp(0.0, 1.0); + let wetness = (moisture - relief_signal).clamp(0.0, 1.0); + let hab = mosaic::select(mosaic::palette(class), field, wetness); + column.vegetation = hab.vegetation; + // Add standing water only where the family left it dry AND the chunk's + // hydrology gate allows it (T-1040: water placement must agree with + // has_active_channel — water never appears in a channel-free chunk). Wet + // habitats (bog / marsh / creek / brook) thus cluster near channels; away + // from them they render in their dry-ground form. + if column.water == Water::Dry && chunk.has_active_channel { + column.water = hab.water; + } + } } // ── 4. Seasonal cover overlay (D-239 §3, T-1030) ────────────────────── @@ -2433,16 +2502,22 @@ mod tests { fn alluvial_plain_vegetation_from_district_class() { let district = alluvial_district(); // vegetation_class = Forest let chunk = alluvial_chunk(&district); - // Multiple samples to account for sub-chunk scatter; all should be - // in the Forest/Scrub/Grass range (D-239 §8: no-skip law — no Barren in Forest zone). + // Multiple samples to account for the intra-class mosaic (T-1084, D-246), + // which textures a Forest district into dense stand / glade / deadfall / brook / + // fern. The binding invariant is the D-239 §8 no-skip law: NEVER Barren in a + // Forest zone — the Forest palette upholds it (no Barren entry). for tx in [100, 101, 102, 103, 104] { let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, 100); assert!( matches!( col.vegetation, - Vegetation::Forest | Vegetation::Scrub | Vegetation::Grass + Vegetation::Forest + | Vegetation::Scrub + | Vegetation::Grass + | Vegetation::Thicket + | Vegetation::Deadfall ), - "Forest district must produce Forest/Scrub/Grass vegetation, got {:?}", + "Forest district must stay non-Barren (§8 no-skip), got {:?}", col.vegetation ); } diff --git a/server/src/seed.rs b/server/src/seed.rs index d3ad9ad7d..252956880 100644 --- a/server/src/seed.rs +++ b/server/src/seed.rs @@ -115,6 +115,11 @@ pub enum SeedDomain { /// keyed by a single constant id (one field per body). Distinct domain so the /// relief lattice can never correlate with the per-voxel terrain or cover streams. VoxelRelief = 11, + /// Sub-chunk micro-habitat mosaic selection field (T-1084, D-246). A body-global, + /// position-keyed value-noise field at ≈8–64 m wavelengths (one field per body, + /// keyed by a single constant id). Distinct domain so the mosaic-selection lattice + /// can never correlate with the relief, cover, or per-voxel terrain streams. + VoxelMosaic = 12, } /// A position in the deterministic seed tree (D-224). @@ -275,6 +280,7 @@ mod tests { assert_eq!(SeedDomain::Voxel as u64, 9); assert_eq!(SeedDomain::Cover as u64, 10); assert_eq!(SeedDomain::VoxelRelief as u64, 11); + assert_eq!(SeedDomain::VoxelMosaic as u64, 12); } #[test] diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index bf7235830..da2796ca3 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -28,7 +28,8 @@ "morphology_zones": 9, "vegetation_classes": 3, "terrain_materials": 4, - "voxel_relief_m": 20 + "voxel_relief_m": 20, + "micro_habitat_distinct": 2 }, "coherence": { "water_districts": 49, @@ -68,7 +69,8 @@ "morphology_zones": 9, "vegetation_classes": 1, "terrain_materials": 4, - "voxel_relief_m": 19 + "voxel_relief_m": 19, + "micro_habitat_distinct": 0 }, "coherence": { "water_districts": 25, diff --git a/server/tests/golden/derivation_harness.json b/server/tests/golden/derivation_harness.json index 6bd180592..58944a998 100644 --- a/server/tests/golden/derivation_harness.json +++ b/server/tests/golden/derivation_harness.json @@ -6,7 +6,7 @@ "tile_x": 1375, "tile_y": 800, "terrain": 0, - "vegetation": 4, + "vegetation": 8, "water": 0, "elevation_m": 14, "cover": 0