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;