docs(meta): D-258 — the biome input is baked pre-seed and drives the climate model

Jeroen's rulings on the question the 2026-09-24 amendment left open:
the biome input wins where it and the server's climate derive disagree; it
is input TO the climate model, which now scatters sub-gridunit around it;
the full class range travels on a dedicated biome plane; and it is baked
pre-seed, so worlds stay recognisable across playthroughs while the ground
differs.

Not circular after all: reliefmap.png is palette[biome] x shading, and that
biome came from the Python simulator's own temperature and moisture model,
which the server has never seen. The bake takes the simulator's class id with
the temperature and moisture it was classified from, rather than decoding
pixels. Several classes differ only in brightness, so a shaded pixel can't be
decoded back to its class.

Filed under T-1211, in order: T-1295 (bake; measure the format first),
T-1215 (re-scoped: loader and per-class fractions), T-1296 (picker and biome
plane), T-1297 (climate takes the baked input), T-1298 (gate against baked
pixels). T-1216 waits on the plane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 12:56:00 +02:00
co-authored by Claude Opus 5.5
parent 5a45c69280
commit 112c099f51
5 changed files with 129 additions and 1 deletions
+5
View File
@@ -4,3 +4,8 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65T2N1WAT491WY8SV736J3G', '06G5FZCVZXH6M00C57EXMZD0SG', '2026-09-02 16:20:33.265', '2026-09-02 16:20:33.265', NULL, '8148b701e111a64a6921913e25cab437', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65NSJ4RG6SJ12TQE78S6TKR', '06G5FZCVZXH6M00C57EXMZD0SG', '2026-09-02 16:20:33.281', '2026-09-02 16:20:33.281', NULL, 'e41be4d7e61660697a2959307ebe1832', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G65TC359QGCVBEPXHJ4N0MRG', '06G5FZCVZXH6M00C57EXMZD0SG', '2026-09-02 16:20:33.298', '2026-09-02 16:20:33.298', NULL, 'cc5a897d45144c728fd035766d432373', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2G9K7C8SPH57RCNB8V8C', '06FT0V0WBYS20XNMEB67A6AT8G', '2026-09-25 10:55:42.024', '2026-09-25 10:55:42.024', NULL, '3c45a79dfeb8ee46dda9bc019fb2f54b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0V0WBYS20XNMEB67A6AT8G', '06GDGD2KKR0YNFA7N5CNE7HA7W', '2026-09-25 10:55:46.507', '2026-09-25 10:55:46.507', NULL, '020748df50ed302cb929aebed90a5a28', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0V0WBYS20XNMEB67A6AT8G', '06GDGD2Q9SK71WJVVFJA4PTBRM', '2026-09-25 10:55:47.038', '2026-09-25 10:55:47.038', NULL, 'd5e632de9c67a0b0b226967d96ec91ab', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2KKR0YNFA7N5CNE7HA7W', '06GDGD2T7KWVX0ATZKZ55Q39DM', '2026-09-25 10:55:47.636', '2026-09-25 10:55:47.636', NULL, '3cbf2ad00beca4acc288400769811444', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2KKR0YNFA7N5CNE7HA7W', '06FT0V18KE3QPZ8CYRTVD883VR', '2026-09-25 10:55:48.274', '2026-09-25 10:55:48.274', NULL, 'd13a52955c23a7b4eaabd5880fab542e', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at;
+47
View File
@@ -1108,3 +1108,50 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
PREMISE GONE 2026-09-24 (D-258 amendment of that date). The stored rung-0.5 layer this ticket targets will not be built: storage was measured and is not forced (T-1212, done). Hydrology already solves once per body (layer1.rs, 114 ms on Ferrath) and every rung samples it. Left open rather than cancelled; closing it is Jeroen''s call.', NULL, '2026-09-24 20:44:26', '2026-09-24 20:44:26.979', '2026-09-24 20:44:26.979', NULL, 'b13bf0e2759e85dc0fde83d74e262da4', 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'status', 'in_progress', 'done', NULL, '2026-09-24 20:44:27', '2026-09-24 20:44:27.521', '2026-09-24 20:44:27.521', NULL, '1e6bfb42eb3c80a764237a2858a848f6', 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 ('06FT0TYBD74TQNVKTJMKA8D9KM', 'status', 'backlog', 'done', NULL, '2026-09-24 20:44:27', '2026-09-24 20:44:27.526', '2026-09-24 20:44:27.526', NULL, '877d4937ffe478151391dfb754dd2dd2', 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 ('06GDGD2G9K7C8SPH57RCNB8V8C', 'description', NULL, 'D-258 amendment 2026-09-25, rulings 1-4. Bake, per body, the Python planet simulator''s biome class grid TOGETHER WITH the absolute temperature (kelvin, before the display normalisation in simulate()) and moisture [0,1] it was classified from. One model supplies all three, so they agree by construction.
PRE-SEED: simulate() is keyed on the body frontmatter''s seed only. The world seed must never enter this bake. Worlds stay recognisable across playthroughs; the ground varies server-side.
MEASURE FIRST, then choose the format. The server works at 512x256 (GRID_W/GRID_H downsample), so a 1024x512 bake may be twice the resolution the server can use on each axis. Temperature and moisture are smooth fields that compress far worse than a class grid. Across ~2,600 bodies, the repository cost of candidate formats must be measured: 8-bit class PNG, 16-bit temperature, 8-bit moisture, at 1024x512 and at 512x256. Report the numbers before committing a bake.
DO NOT re-render reliefmap.png. Its render drifts by a few hundred bytes (T-1291). The bake gets its own mode, a verb on reach atlas planet, that writes only the new file(s). The heightmap is byte-identical on re-simulation, so the simulator is deterministic.
Class ids become append-only (the gas-palette selection_order discipline in biomes.toml). Gas giants get nothing. Whatever the chosen files are, register them where the atlas and canvas registries expect: the canvas-version gate (tooling/canvas_sources.py) must see a change to them.', NULL, '2026-09-25 10:55:36', '2026-09-25 10:55:36.898', '2026-09-25 10:55:36.898', NULL, '7c499c7b9ec4f0f4a02675753f7358f8', 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 ('06FT0V0WBYS20XNMEB67A6AT8G', 'description', 'step_canvas.rs (and any other rung-derivation entry point) must be re-pointed so Region/District/Quarter/Block/Chunk all derive from the cached rung-0.5 layer rather than resolving heightmap.png/reliefmap.png directly. heightmap.rs (the rung-0 loader) gains the reliefmap as a second input if it does not already load it. This is the ''no deeper rung reads the source files'' invariant from D-258 -- cross-rung visual agreement becomes structural once this lands, rather than enforced by convention. Depends on T-1213 (generator) and T-1214 (hydrology) existing to re-point to. See governance/decisions/architecture.md#d-258 Implementation note (explicitly names step_canvas.rs and heightmap.rs as the consumers to re-point).
PREMISE GONE 2026-09-24 (D-258 amendment of that date). The stored rung-0.5 layer this ticket targets will not be built: storage was measured and is not forced (T-1212, done). Hydrology already solves once per body (layer1.rs, 114 ms on Ferrath) and every rung samples it. Left open rather than cancelled; closing it is Jeroen''s call.', 'step_canvas.rs (and any other rung-derivation entry point) must be re-pointed so Region/District/Quarter/Block/Chunk all derive from the cached rung-0.5 layer rather than resolving heightmap.png/reliefmap.png directly. heightmap.rs (the rung-0 loader) gains the reliefmap as a second input if it does not already load it. This is the ''no deeper rung reads the source files'' invariant from D-258 -- cross-rung visual agreement becomes structural once this lands, rather than enforced by convention. Depends on T-1213 (generator) and T-1214 (hydrology) existing to re-point to. See governance/decisions/architecture.md#d-258 Implementation note (explicitly names step_canvas.rs and heightmap.rs as the consumers to re-point).
PREMISE GONE 2026-09-24 (D-258 amendment of that date). The stored rung-0.5 layer this ticket targets will not be built: storage was measured and is not forced (T-1212, done). Hydrology already solves once per body (layer1.rs, 114 ms on Ferrath) and every rung samples it. Left open rather than cancelled; closing it is Jeroen''s call.
RE-SCOPED 2026-09-25 (D-258 amendment of that date). The stored rung-0.5 layer will not be built. What survives of this ticket is its own text: "heightmap.rs (the rung-0 loader) gains the reliefmap as a second input". That input is now the baked biome input from T-1295, not reliefmap.png''s pixels.
Scope:
- Load the baked class/temperature/moisture files next to heightmap.png, on the same path the step-canvas and window derives use (gen_queue DeriveStepCanvas, AnalyzeBody).
- At the 512x256 downsample, keep PER-CLASS FRACTIONS per working cell (the plurality, not the winner), plus the baked climate. Store them in TerrainAnalysis, computed once per body behind the same TerrainAnalysisCache as the hydrology solve.
- Expose bilinear sampling of fractions and climate at a position, through the same coastline-warped position elevation already uses. Biome edges then blend across cell lines (invariant 1).
- Bodies without a bake (airless, gas, not yet baked) fall back to today''s derive unchanged. Byte-identical for them: prove it with the goldens.
Blocked by T-1295. Unblocks T-1296 and T-1297.', NULL, '2026-09-25 10:55:37', '2026-09-25 10:55:37.504', '2026-09-25 10:55:37.504', NULL, 'aef546b26be21b07da17e26e57a58ea7', 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 ('06GDGD2KKR0YNFA7N5CNE7HA7W', 'description', NULL, 'D-258 amendment 2026-09-25, rulings 1 and 3.
PICK: the class at a point is chosen by thresholding WORLD-seeded noise against the local cumulative class fractions (T-1215). This is the pattern voxel_mosaic already uses (T-1084, D-246). Gate the noise by the rung''s octave cutoff, like every invented field. Where the rung cannot resolve it, return the plurality. Each class then covers its own fraction over any area, so conservation (invariant 3) holds by construction. Minorities appear only where their fraction is non-zero (invariant 2). Global shows the summary; descent reveals the mixture.
This supersedes composition.rs''s rare inclusions and smooth offsets for biome, on baked bodies. Decide explicitly what composition keeps doing there, if anything. Slope and outcrops are geology, not biome, so they may stay.
PLANE: add a dedicated dense biome plane (u8 class id, full range) to RawStepCanvas and EncodedStepCanvas, and decode it in client/scripts/protocol/step_canvas_protocol.gd. The dense-plane SET test must include it: relief_q once fell off the wire for weeks because no test asserted the set (T-1213). Protocol change: bump what the wire contract requires.
Blocked by T-1215. T-1216 (the Global biome-from-orbit palette) consumes this plane.', NULL, '2026-09-25 10:55:38', '2026-09-25 10:55:38.352', '2026-09-25 10:55:38.352', NULL, 'c1d88487ec006fcc2589379a3dda3c68', 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 ('06GDGD2Q9SK71WJVVFJA4PTBRM', 'description', NULL, 'D-258 amendment 2026-09-25, ruling 2: the baked biome input drives the climate model, not the other way round. The server''s climate model stops originating climate at gridunit scale on baked bodies. Today that origination is latitude, lapse, greenhouse and hydrosphere in derive_temperature_c and derive_moisture_q. It scatters SUB-GRIDUNIT around the baked temperature and moisture instead: lapse from invented relief, the D-243 edge-fuzz, and local noise.
COHERENCE WITH THE PICK (T-1296): correlate the scatter with the picked class, so a picked wet class does not sit on scattered-dry ground. One way is to order the local classes along the moisture axis and drive the pick and the moisture scatter from the same noise value. The steep-drains-dry correlation composition.rs already uses is the precedent.
UNITS: the Python bake is kelvin and moisture [0,1]; the server is deci-degrees C and moisture_q 0-100. Convert once at load.
D-239''s climate-to-class laws (the treeline, the riparian band) are NOT repealed; they operate inside the scatter. Airless, gas and unbaked bodies keep the current derive.
Blocked by T-1215. Expect every golden on baked bodies to move. Each move must be explained before regenerating, as in T-1213.', NULL, '2026-09-25 10:55:38', '2026-09-25 10:55:38.926', '2026-09-25 10:55:38.926', NULL, '5885a49c8665e7b3b0d3ae64968f28aa', 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 ('06GDGD2T7KWVX0ATZKZ55Q39DM', 'description', NULL, 'D-258 invariant 3 as written: "downsampling rung 0.5 must reproduce the rung-0 summary it came from." Once the baked input exists, the summary is the baked class pixel. Retarget composition_conserves_the_summary_on_real_terrain: derive canvases over real ground, downsample each source-pixel footprint to its plurality, and require it to equal the baked class at that pixel, across patches spread over the body.
Keep what T-1213 made it: it runs in plain cargo test, asserts the judged count and "anything changed at all" so it cannot pass vacuously, and is mutation-proven both ways (over-invention and no invention).
Blocked by T-1296.', NULL, '2026-09-25 10:55:39', '2026-09-25 10:55:39.363', '2026-09-25 10:55:39.363', NULL, '16b9e2ddf2334ddbe8051747ece340dd', 2) ON CONFLICT(hash) DO NOTHING;
+4
View File
@@ -9,3 +9,7 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCX9TTBJW4NPECEN6JM0V4HM', 'T-1292', '2026-09-23 14:24:29.282', '2026-09-23 14:24:29.282', NULL, '45174fb519d2deed098a244229d543e1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCYT8JBRCX13ZD51SY4SX1WW', 'T-1293', '2026-09-23 17:56:04.830', '2026-09-23 17:56:04.830', NULL, '5d8ca375541decd04d46a9932f80e2c8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCYTXYFNTZ828SM7KEREGHK0', 'T-1294', '2026-09-23 17:58:59.966', '2026-09-23 17:58:59.966', NULL, 'bcabb81af3c53e6d6f879da79ce6e627', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2G9K7C8SPH57RCNB8V8C', 'T-1295', '2026-09-25 10:55:02.222', '2026-09-25 10:55:02.222', NULL, 'a7c7f21dd198c639a3f8a296ddc08956', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2KKR0YNFA7N5CNE7HA7W', 'T-1296', '2026-09-25 10:55:03.070', '2026-09-25 10:55:03.070', NULL, '7eec0325777c31ea12cbf9d667be748c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2Q9SK71WJVVFJA4PTBRM', 'T-1297', '2026-09-25 10:55:04.014', '2026-09-25 10:55:04.014', NULL, 'e15649504be5d38332c282226a05c340', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GDGD2T7KWVX0ATZKZ55Q39DM', 'T-1298', '2026-09-25 10:55:04.770', '2026-09-25 10:55:04.770', NULL, '13c95cd1ce37eab63ea1082d207f2048', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
+49
View File
@@ -1298,3 +1298,52 @@ Measured with tests/rung_cost_bench.rs (release, 16 cores, Ferrath at the ladder
A stored layer at Region pitch would be ~5.05e9 cells, about 71 GB per body; District is 10^4x more. A layer at D-258''s own sizing can serve only Global, which already derives in 118 ms.
Storage is not forced. See the D-258 amendment of 2026-09-24.', 'done', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:06.825', '2026-09-24 20:44:27.526', NULL, '2b4b26704d58544024f7bd8b448c8aff', 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 ('06GDGD2G9K7C8SPH57RCNB8V8C', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Bake the biome input pre-seed: class id + absolute temperature + moisture per body (D-258 2026-09-25)', NULL, 'backlog', 'high', NULL, 'tooling', 'D-258', '2026-09-25 10:55:02.220', '2026-09-25 10:55:02.220', NULL, '324dcf3c9d3ec93582664492ae009b8a', 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 ('06GDGD2KKR0YNFA7N5CNE7HA7W', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Un-summarisation picker + dedicated biome plane on the step canvas (full class range)', NULL, 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:03.070', '2026-09-25 10:55:03.070', NULL, 'b6f9f73261416dffdbdb24df27bcc047', 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 ('06GDGD2Q9SK71WJVVFJA4PTBRM', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Climate model takes the baked input: temperature and moisture scatter sub-gridunit around it', NULL, 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:04.014', '2026-09-25 10:55:04.014', NULL, '1ffc740fa4e2bfc9d288e0fcfe42f16a', 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 ('06GDGD2T7KWVX0ATZKZ55Q39DM', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Conservation gate retargets to the baked class pixels', NULL, 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:04.764', '2026-09-25 10:55:04.764', NULL, '840915e89322f5065d98557f6a49f568', 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 ('06GDGD2G9K7C8SPH57RCNB8V8C', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Bake the biome input pre-seed: class id + absolute temperature + moisture per body (D-258 2026-09-25)', 'D-258 amendment 2026-09-25, rulings 1-4. Bake, per body, the Python planet simulator''s biome class grid TOGETHER WITH the absolute temperature (kelvin, before the display normalisation in simulate()) and moisture [0,1] it was classified from. One model supplies all three, so they agree by construction.
PRE-SEED: simulate() is keyed on the body frontmatter''s seed only. The world seed must never enter this bake. Worlds stay recognisable across playthroughs; the ground varies server-side.
MEASURE FIRST, then choose the format. The server works at 512x256 (GRID_W/GRID_H downsample), so a 1024x512 bake may be twice the resolution the server can use on each axis. Temperature and moisture are smooth fields that compress far worse than a class grid. Across ~2,600 bodies, the repository cost of candidate formats must be measured: 8-bit class PNG, 16-bit temperature, 8-bit moisture, at 1024x512 and at 512x256. Report the numbers before committing a bake.
DO NOT re-render reliefmap.png. Its render drifts by a few hundred bytes (T-1291). The bake gets its own mode, a verb on reach atlas planet, that writes only the new file(s). The heightmap is byte-identical on re-simulation, so the simulator is deterministic.
Class ids become append-only (the gas-palette selection_order discipline in biomes.toml). Gas giants get nothing. Whatever the chosen files are, register them where the atlas and canvas registries expect: the canvas-version gate (tooling/canvas_sources.py) must see a change to them.', 'backlog', 'high', NULL, 'tooling', 'D-258', '2026-09-25 10:55:02.220', '2026-09-25 10:55:36.898', NULL, 'abcb4175997e68f0e3a8e7d83170fdde', 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 ('06FT0V0WBYS20XNMEB67A6AT8G', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Re-point every Atlas rung to source from rung 0.5, not the raw heightmap/reliefmap', 'step_canvas.rs (and any other rung-derivation entry point) must be re-pointed so Region/District/Quarter/Block/Chunk all derive from the cached rung-0.5 layer rather than resolving heightmap.png/reliefmap.png directly. heightmap.rs (the rung-0 loader) gains the reliefmap as a second input if it does not already load it. This is the ''no deeper rung reads the source files'' invariant from D-258 -- cross-rung visual agreement becomes structural once this lands, rather than enforced by convention. Depends on T-1213 (generator) and T-1214 (hydrology) existing to re-point to. See governance/decisions/architecture.md#d-258 Implementation note (explicitly names step_canvas.rs and heightmap.rs as the consumers to re-point).
PREMISE GONE 2026-09-24 (D-258 amendment of that date). The stored rung-0.5 layer this ticket targets will not be built: storage was measured and is not forced (T-1212, done). Hydrology already solves once per body (layer1.rs, 114 ms on Ferrath) and every rung samples it. Left open rather than cancelled; closing it is Jeroen''s call.
RE-SCOPED 2026-09-25 (D-258 amendment of that date). The stored rung-0.5 layer will not be built. What survives of this ticket is its own text: "heightmap.rs (the rung-0 loader) gains the reliefmap as a second input". That input is now the baked biome input from T-1295, not reliefmap.png''s pixels.
Scope:
- Load the baked class/temperature/moisture files next to heightmap.png, on the same path the step-canvas and window derives use (gen_queue DeriveStepCanvas, AnalyzeBody).
- At the 512x256 downsample, keep PER-CLASS FRACTIONS per working cell (the plurality, not the winner), plus the baked climate. Store them in TerrainAnalysis, computed once per body behind the same TerrainAnalysisCache as the hydrology solve.
- Expose bilinear sampling of fractions and climate at a position, through the same coastline-warped position elevation already uses. Biome edges then blend across cell lines (invariant 1).
- Bodies without a bake (airless, gas, not yet baked) fall back to today''s derive unchanged. Byte-identical for them: prove it with the goldens.
Blocked by T-1295. Unblocks T-1296 and T-1297.', 'backlog', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:27.551', '2026-09-25 10:55:37.504', NULL, '61f241a213eaa8bd5befd30bbf726bd4', 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 ('06GDGD2KKR0YNFA7N5CNE7HA7W', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Un-summarisation picker + dedicated biome plane on the step canvas (full class range)', 'D-258 amendment 2026-09-25, rulings 1 and 3.
PICK: the class at a point is chosen by thresholding WORLD-seeded noise against the local cumulative class fractions (T-1215). This is the pattern voxel_mosaic already uses (T-1084, D-246). Gate the noise by the rung''s octave cutoff, like every invented field. Where the rung cannot resolve it, return the plurality. Each class then covers its own fraction over any area, so conservation (invariant 3) holds by construction. Minorities appear only where their fraction is non-zero (invariant 2). Global shows the summary; descent reveals the mixture.
This supersedes composition.rs''s rare inclusions and smooth offsets for biome, on baked bodies. Decide explicitly what composition keeps doing there, if anything. Slope and outcrops are geology, not biome, so they may stay.
PLANE: add a dedicated dense biome plane (u8 class id, full range) to RawStepCanvas and EncodedStepCanvas, and decode it in client/scripts/protocol/step_canvas_protocol.gd. The dense-plane SET test must include it: relief_q once fell off the wire for weeks because no test asserted the set (T-1213). Protocol change: bump what the wire contract requires.
Blocked by T-1215. T-1216 (the Global biome-from-orbit palette) consumes this plane.', 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:03.070', '2026-09-25 10:55:38.352', NULL, 'c38be981e5c89144f14d4cf19453fa99', 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 ('06GDGD2Q9SK71WJVVFJA4PTBRM', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Climate model takes the baked input: temperature and moisture scatter sub-gridunit around it', 'D-258 amendment 2026-09-25, ruling 2: the baked biome input drives the climate model, not the other way round. The server''s climate model stops originating climate at gridunit scale on baked bodies. Today that origination is latitude, lapse, greenhouse and hydrosphere in derive_temperature_c and derive_moisture_q. It scatters SUB-GRIDUNIT around the baked temperature and moisture instead: lapse from invented relief, the D-243 edge-fuzz, and local noise.
COHERENCE WITH THE PICK (T-1296): correlate the scatter with the picked class, so a picked wet class does not sit on scattered-dry ground. One way is to order the local classes along the moisture axis and drive the pick and the moisture scatter from the same noise value. The steep-drains-dry correlation composition.rs already uses is the precedent.
UNITS: the Python bake is kelvin and moisture [0,1]; the server is deci-degrees C and moisture_q 0-100. Convert once at load.
D-239''s climate-to-class laws (the treeline, the riparian band) are NOT repealed; they operate inside the scatter. Airless, gas and unbaked bodies keep the current derive.
Blocked by T-1215. Expect every golden on baked bodies to move. Each move must be explained before regenerating, as in T-1213.', 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:04.014', '2026-09-25 10:55:38.926', NULL, 'd85814062818ff22e2d2b60ab45c61dd', 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 ('06GDGD2T7KWVX0ATZKZ55Q39DM', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Conservation gate retargets to the baked class pixels', 'D-258 invariant 3 as written: "downsampling rung 0.5 must reproduce the rung-0 summary it came from." Once the baked input exists, the summary is the baked class pixel. Retarget composition_conserves_the_summary_on_real_terrain: derive canvases over real ground, downsample each source-pixel footprint to its plurality, and require it to equal the baked class at that pixel, across patches spread over the body.
Keep what T-1213 made it: it runs in plain cargo test, asserts the judged count and "anything changed at all" so it cannot pass vacuously, and is mutation-proven both ways (over-invention and no invention).
Blocked by T-1296.', 'backlog', 'high', NULL, 'server', 'D-258', '2026-09-25 10:55:04.764', '2026-09-25 10:55:39.363', NULL, '8948621ed882acc7de9de89813c2ff9e', 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;
+24 -1
View File
@@ -2438,10 +2438,33 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**Consequence.** The [D-227](#d-227) carve-out stays unused: everything below rung 0 is derive-don't-store, rung 0.5 included, and "rung 0.5" names the un-summarising *function* every rung calls, not an artefact. [T-1215](../../.pql) (re-point every rung to a stored layer) and [T-1214](../../.pql) (move hydrology onto it) lose their premise. [T-1216](../../.pql) (Global biome palette) and [T-1217](../../.pql) (lake shores through the shared shore path) are unaffected.
**Open, and not decided here: the reliefmap is still not an input.** This record names `reliefmap.png` as the biome input whose plurality rung 0.5 un-summarises. No server code reads it (`heightmap.rs` mentions it only to reject an RGB PNG). Biome is derived from climate — temperature, moisture, elevation — so the "summary" the conservation gate protects is the derive's own uncomposed verdict, not a reliefmap pixel. Whether the reliefmap should become an input at all (it is itself a render of the same heightmap, so reading it may be circular) is Jeroen's call.
**Open, and not decided here: the reliefmap is still not an input.** This record names `reliefmap.png` as the biome input whose plurality rung 0.5 un-summarises. No server code reads it (`heightmap.rs` mentions it only to reject an RGB PNG). Biome is derived from climate — temperature, moisture, elevation — so the "summary" the conservation gate protects is the derive's own uncomposed verdict, not a reliefmap pixel. Whether the reliefmap should become an input at all (it is itself a render of the same heightmap, so reading it may be circular) is Jeroen's call. *(Answered the next day — see the 2026-09-25 amendment: it is not circular, and it becomes the input.)*
**Two defects found by the same capture pass, both violations of invariant (1):** the terrain character stepped on the None→Light glaciation isotherm (a straight seam through Region's relief, drawn as a stair-stepped cliff) and composition's 192 m inclusions ignored the rung's octave cutoff (aliased speckle at Region). Both fixed in T-1213; the character now reads a continuous glacial weight.
- **Amendment (2026-09-25 — the biome input is baked pre-seed, and it drives the climate model):** Jeroen's rulings on the question the previous amendment left open.
**It is not circular.** `reliefmap.png` is not an elevation render. Each land pixel is `palette[biome] × elevation shade × hillshade` (`render_heightmap.py`), where `biome` is a class grid the Python planet simulator derived from **its own** absolute-kelvin temperature and moisture fields through the Whittaker table in `biomes.toml` — a model the server's climate derive has never seen. The reliefmap was the only place that model survived, flattened into colour.
**Rulings (Jeroen, 2026-09-25):**
1. **The biome input wins.** Where it and the server's climate derive disagree, the biome input is right.
2. **It is input to the climate model, not the other way round.** The server's climate model no longer originates climate at the gridunit scale; it *scatters sub-gridunit* around what the biome input says. Jeroen: *"the climate model scatters sub-gridunit. In effect the biomemap should be input for the climate model."*
3. **The full class range travels, on a dedicated biome plane.** No collapse of the simulator's classes into the server's seven vegetation classes; the canvas carries the class id on its own dense plane.
4. **Baked pre-seed.** The input depends on the body definition only, never on the world seed, so *"worlds remain recognizable across playthroughs, but not the same on the ground."* Verified: `planet_simulation.simulate` is keyed on the body frontmatter's `seed`; the world seed enters only on the server, where un-summarisation picks and scatters.
**Shape.** Bake the simulator's fields losslessly rather than decode the reliefmap's pixels. Shading multiplies brightness, and several classes differ only in brightness (the lunar greys, `ice_snow`/`ice_shelf`, the rock family), so a pixel cannot be decoded back to its class. The bake writes, per body, the **biome class id** together with the **absolute temperature and moisture** it was classified from. One model supplies all three, so they agree by construction. Classes the Whittaker table does not cover (ocean, coast, rock, exotic, ferric, lunar) need no inverse mapping, because their climate is baked directly. Class ids become append-only. On the server:
- The loader keeps **per-class fractions** per working-grid cell (the plurality, not the winner) alongside the baked climate, computed once per body like the hydrology solve.
- Each position samples fractions and climate **bilinearly**, through the same coastline-warped position as elevation, so no biome edge falls on a cell line (invariant 1).
- The class at a point is **picked** by thresholding world-seeded noise against the local cumulative fractions (the T-1084 mosaic pattern), gated by the rung's octave cutoff. Where the rung cannot resolve the noise it returns the plurality. Each class then covers its own fraction over any area, so conservation (3) holds **by construction**, and minorities appear only where they were voted (2).
- Temperature and moisture are the baked values plus sub-gridunit scatter (lapse from invented relief, edge-fuzz, local noise), correlated with the pick, so a picked wet class does not sit on scattered-dry ground.
- The conservation gate retargets to its real subject: a downsampled canvas against the baked class pixels.
**Supersedes, in part,** the server's gridunit-scale climate origination (latitude, lapse, greenhouse and hydrosphere in `derive_temperature_c`/`derive_moisture_q`) wherever a baked input exists. Airless and gas bodies, and bodies not yet baked, keep the current derive. [D-239](#d-239)'s climate-to-class laws (the treeline, the riparian band) are not repealed; they operate inside the scatter.
**Open, measured before the bake is committed:** the on-disk format and resolution. The server downsamples to 512×256 anyway, and the temperature and moisture fields compress far worse than a class grid, so the repository cost across ~2,600 bodies must be measured, not assumed.
---
### D-259: Enclosed settlements — open-air requires a breathable atmosphere