From 869837f7284e7fff111917f112b42ad3cd22283a Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 16 Aug 2026 12:35:19 +0200 Subject: [PATCH] test(simulation): the conservation gate's monoculture check was a tautology (T-1213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D-258 invariant 2 says descending the ladder must reveal COMPOSITION — a cell reading forest must be able to contain the clearings and rock the vote suppressed. One assertion stood behind that, and it read: assert!(tally.len() > 1 || share == 1.0, ...) A single-class tally has a 100% share by definition, so both branches are always satisfiable: the check could never fail, including in the exact case its own message names, "or nothing was composed". The invariant had a test and no gate. Split into the two bounds the invariant actually has, because it is two-sided: conservation caps how much may be invented (majority > 50%, already asserted) and composition sets a floor on how little (minority >= 0.1%). Verified by raising the floor to 2% and watching it fail on the measured 1.07%, then restoring it — the floor is a tripwire for "did anything happen", deliberately far below the measurement rather than tuned to it. Measured at the descent ladder's own anchor on Ferrath: conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209} So composition IS working in the data and conservation holds. The map is flat anyway, and tooling/atlas-flatness (added here) says why the eye was not enough: rung distinct lum p1-p99 Global 1581 145.69 Region 2923 33.59 District 53 13.72 Quarter 46 11.01 Region carries almost TWICE Global's distinct-colour count while holding a quarter of its structure — the dither pass adds colour noise, not information, so a colour-count metric would have called the flattest rung the richest. Structure falls ~92% from Global to Quarter. The cause is a channel mismatch rather than a missing generator: composition perturbs moisture_q/slope_q, and the base map draws morphology hue x elev_q lightness. The ladder scenarios pass no overlays deliberately, so the composed fields are never rendered in the very shots that judge this work. Recorded on T-1213 with the three ways forward; the choice touches D-258 and is Jeroen's. The gate is still #[ignore]d — noted on the ticket as worth moving into a harness that runs, since believability and window-derivation already load real bodies in the normal cargo test path. Co-Authored-By: Claude Opus 5 (1M context) --- .pql/changelog/ticket_history/2026-08.sql | 158 ++++++++++++++++++++++ .pql/changelog/tickets/2026-08.sql | 124 +++++++++++++++++ server/src/atlas/composition.rs | 44 +++++- tooling/atlas-flatness | 125 +++++++++++++++++ 4 files changed, 449 insertions(+), 2 deletions(-) create mode 100755 tooling/atlas-flatness diff --git a/.pql/changelog/ticket_history/2026-08.sql b/.pql/changelog/ticket_history/2026-08.sql index 6c3ba2885..31780f2ef 100644 --- a/.pql/changelog/ticket_history/2026-08.sql +++ b/.pql/changelog/ticket_history/2026-08.sql @@ -174,3 +174,161 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'status', 'backlog', 'in_progress', NULL, '2026-08-14 21:41:02', '2026-08-14 21:41:02.688', '2026-08-14 21:41:02.688', NULL, '11035a92e7d4ab5c58e1f3654ba90fa0', 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'status', 'in_progress', 'in_progress', NULL, '2026-08-14 21:41:17', '2026-08-14 21:41:17.977', '2026-08-14 21:41:17.977', NULL, '96b6f8d466e161a1c87cd8cd349fcea2', 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'status', 'in_progress', 'done', NULL, '2026-08-14 22:06:55', '2026-08-14 22:06:55.412', '2026-08-14 22:06:55.412', NULL, 'b0695a841482c839c7e27a8f0c56ce0e', 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', 'description', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. + +--- +UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is +removed: that measurement priced a hydrology move that is not happening, and the +descent ladder showed there is no expansion artefact to measure yet anyway. + +BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of +D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally +computable" argument for the D-227 carve-out does not hold, because the whole-body +solve it cited already runs once per body in layer1.rs and is sampled at every +rung). So follow the mechanism that exists — D-255(f) mechanism B, +compute-once-sample-everywhere — measure it, and only argue for storage on those +numbers. Do NOT open with a cache. + +WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is +1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at +35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region +draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single +uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also +uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a +single source pixel must carry visible, deterministic, non-repeating composition +that still downsamples back to that pixel''s summary (D-258''s conservation +invariant, the binding gate). + +VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* +(Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) +in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 +via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot +the ladder and look at it; a green unit test proves nothing here. + +Note the ladder currently also reports courses=0 at every rung below Global — +rivers vanish on descent. That is tracked separately as T-1239 and is NOT this +ticket''s scope, but it will be visible in the same captures, so do not mistake it +for a failure of the un-summarisation work.', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. + +--- +UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is +removed: that measurement priced a hydrology move that is not happening, and the +descent ladder showed there is no expansion artefact to measure yet anyway. + +BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of +D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally +computable" argument for the D-227 carve-out does not hold, because the whole-body +solve it cited already runs once per body in layer1.rs and is sampled at every +rung). So follow the mechanism that exists — D-255(f) mechanism B, +compute-once-sample-everywhere — measure it, and only argue for storage on those +numbers. Do NOT open with a cache. + +WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is +1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at +35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region +draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single +uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also +uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a +single source pixel must carry visible, deterministic, non-repeating composition +that still downsamples back to that pixel''s summary (D-258''s conservation +invariant, the binding gate). + +VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* +(Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) +in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 +via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot +the ladder and look at it; a green unit test proves nothing here. + +Note the ladder currently also reports courses=0 at every rung below Global — +rivers vanish on descent. That is tracked separately as T-1239 and is NOT this +ticket''s scope, but it will be visible in the same captures, so do not mistake it +for a failure of the un-summarisation work. + +--- +LADDER RE-SHOT COLD 2026-08-16 — the first ladder whose numbers can be trusted. +Every previous ladder, including the one this ticket''s re-scope was written from, +was captured against the developer''s persistent user:// cache (T-1239). The +harness now isolates user:// per capture, so these are fresh derives. + +RESULT: the acceptance bar is NOT met. Region and below are still flat. + +Flatness, measured rather than eyeballed (.cache/t1213_flatness.py samples the +terrain area, excluding the panels): + + rung distinct R std G std B std lum p1-p99 + Global 1581 24.05 35.75 36.21 145.69 + Region 2923 4.80 8.31 4.19 33.59 + District 53 2.56 4.90 2.56 13.72 + Quarter 46 2.42 4.78 2.42 11.01 + +Note Region carries MORE distinct colours than Global (2923 vs 1581) while +holding a quarter of the structure. That is the dither/stipple pass adding colour +noise, not information — so distinct-count is precisely the wrong metric, and the +percentile luminance spread is the honest one. Structure falls ~92% from Global +to Quarter. + +WHY IT IS FLAT — a channel mismatch, not a missing generator. + +Composition IS working in the data. The conservation test, run explicitly against +real terrain, reports over a 2,048 m patch at the ladder''s own anchor: + + conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209} + +So D-258 invariant 3 (conservation) holds and invariant 2 (composition) is +happening: 1.07% of the patch is a minority class. + +But composition perturbs `moisture_q` and `slope_q`, which resolve into +`vegetation_class`. The base map draws neither. step_canvas_colorize.gd''s own +comment states it: "The base layer reads hue from morphology and lightness from +elev_q, and on a real body below Global that collapses: morphology resolves to +ONE zone". The ladder scenarios pass `[]` for overlays deliberately +(visual_scenarios.gd: "No overlays, deliberately: a colour ramp on top would mask +the very thing being judged"), so the composed fields are never rendered in the +shots that judge this work. + +The un-summarisation lands in channels the default view does not draw. That is +why perfectly correct composition and a flat map coexist. + +THE CONSERVATION TEST WAS NOT PROTECTING ANYTHING (fixed here). + +Its "not a monoculture" assertion read: + + assert!(tally.len() > 1 || share == 1.0, ...) + +which is a TAUTOLOGY — a single-class tally has a 100% share by definition, so +both branches are always satisfiable and the check could never fail, including in +the exact case its message names ("or nothing was composed"). It was the only +thing standing behind invariant 2, and it stood behind nothing. Now split into +the two bounds the invariant actually has: majority > 50% (conservation, the +ceiling on invention) and minority >= 0.1% (composition, the floor). Measured +1.07% against a 0.1% floor. + +It is also still `#[ignore]`d, so the binding acceptance gate for this epic runs +only when someone types it. Worth moving into a harness that runs — the +believability and window-derivation harnesses already load real bodies in the +normal `cargo test` path, so the precedent exists. + +courses=0 BELOW GLOBAL IS NOT A BUG — it is arithmetic, and the ticket''s note +attributing it to T-1239 is superseded. Global now reports courses=73 drawn=18 +(T-1239 fixed). Region/District/Quarter report courses=0 because the hydrology +network is 615 river cells on a 512x256 grid (74.4 km/cell) and a Region window +is 489 x 205 km = 0.0217% of a 462M km2 body: expected river cells in view = 615 +x 0.000217 = 0.13. Zero is the likely outcome, not a failure. The real +observation underneath it is that 615 river cells on an Earth-sized body is a +sparse network (~45,000 km of total watercourse), which is a believability +question for hydrology, not a rendering one. + +THE OPEN DECISION — three readings, materially different work: + + (A) Make the DISPLAYED channels carry sub-source-pixel composition. The literal + reading of this ticket''s acceptance bar ("at Region, ~98 gridunits across a + single source pixel must carry visible, deterministic, non-repeating + composition"). Means morphology and/or elev_q gain a fine tier, which is + D-227/D-258 territory and the largest change. + (B) Accept that the base view is morphology + elevation, and judge composition + through the overlays — i.e. the ladder gains VEG/MST companion shots. Test + change only, but it concedes that the default map stays flat below Global. + (C) Raise inclusion density so the existing vegetation texture reads at map + scale. Tuning only; 1.07% is near-invisible. + +Not chosen here — it needs Jeroen''s call, and it touches D-258.', NULL, '2026-08-16 10:32:31', '2026-08-16 10:32:31.457', '2026-08-16 10:32:31.457', NULL, '09a8f8872c230a04e1cb4a59d8fb52c8', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql index c5251524d..9e4f45677 100644 --- a/.pql/changelog/tickets/2026-08.sql +++ b/.pql/changelog/tickets/2026-08.sql @@ -307,3 +307,127 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'in_progress', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:41:02.683', NULL, 'efd83e7e6e689653b411d77629c19985', 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'in_progress', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:41:17.977', NULL, 'cb2da99199993e9c60322845ceaf22b8', 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'done', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 22:06:55.412', NULL, 'eb7ca0296fcb5255bdb5cc94e66c45d5', 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. + +--- +UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is +removed: that measurement priced a hydrology move that is not happening, and the +descent ladder showed there is no expansion artefact to measure yet anyway. + +BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of +D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally +computable" argument for the D-227 carve-out does not hold, because the whole-body +solve it cited already runs once per body in layer1.rs and is sampled at every +rung). So follow the mechanism that exists — D-255(f) mechanism B, +compute-once-sample-everywhere — measure it, and only argue for storage on those +numbers. Do NOT open with a cache. + +WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is +1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at +35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region +draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single +uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also +uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a +single source pixel must carry visible, deterministic, non-repeating composition +that still downsamples back to that pixel''s summary (D-258''s conservation +invariant, the binding gate). + +VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* +(Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) +in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 +via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot +the ladder and look at it; a green unit test proves nothing here. + +Note the ladder currently also reports courses=0 at every rung below Global — +rivers vanish on descent. That is tracked separately as T-1239 and is NOT this +ticket''s scope, but it will be visible in the same captures, so do not mistake it +for a failure of the un-summarisation work. + +--- +LADDER RE-SHOT COLD 2026-08-16 — the first ladder whose numbers can be trusted. +Every previous ladder, including the one this ticket''s re-scope was written from, +was captured against the developer''s persistent user:// cache (T-1239). The +harness now isolates user:// per capture, so these are fresh derives. + +RESULT: the acceptance bar is NOT met. Region and below are still flat. + +Flatness, measured rather than eyeballed (.cache/t1213_flatness.py samples the +terrain area, excluding the panels): + + rung distinct R std G std B std lum p1-p99 + Global 1581 24.05 35.75 36.21 145.69 + Region 2923 4.80 8.31 4.19 33.59 + District 53 2.56 4.90 2.56 13.72 + Quarter 46 2.42 4.78 2.42 11.01 + +Note Region carries MORE distinct colours than Global (2923 vs 1581) while +holding a quarter of the structure. That is the dither/stipple pass adding colour +noise, not information — so distinct-count is precisely the wrong metric, and the +percentile luminance spread is the honest one. Structure falls ~92% from Global +to Quarter. + +WHY IT IS FLAT — a channel mismatch, not a missing generator. + +Composition IS working in the data. The conservation test, run explicitly against +real terrain, reports over a 2,048 m patch at the ladder''s own anchor: + + conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209} + +So D-258 invariant 3 (conservation) holds and invariant 2 (composition) is +happening: 1.07% of the patch is a minority class. + +But composition perturbs `moisture_q` and `slope_q`, which resolve into +`vegetation_class`. The base map draws neither. step_canvas_colorize.gd''s own +comment states it: "The base layer reads hue from morphology and lightness from +elev_q, and on a real body below Global that collapses: morphology resolves to +ONE zone". The ladder scenarios pass `[]` for overlays deliberately +(visual_scenarios.gd: "No overlays, deliberately: a colour ramp on top would mask +the very thing being judged"), so the composed fields are never rendered in the +shots that judge this work. + +The un-summarisation lands in channels the default view does not draw. That is +why perfectly correct composition and a flat map coexist. + +THE CONSERVATION TEST WAS NOT PROTECTING ANYTHING (fixed here). + +Its "not a monoculture" assertion read: + + assert!(tally.len() > 1 || share == 1.0, ...) + +which is a TAUTOLOGY — a single-class tally has a 100% share by definition, so +both branches are always satisfiable and the check could never fail, including in +the exact case its message names ("or nothing was composed"). It was the only +thing standing behind invariant 2, and it stood behind nothing. Now split into +the two bounds the invariant actually has: majority > 50% (conservation, the +ceiling on invention) and minority >= 0.1% (composition, the floor). Measured +1.07% against a 0.1% floor. + +It is also still `#[ignore]`d, so the binding acceptance gate for this epic runs +only when someone types it. Worth moving into a harness that runs — the +believability and window-derivation harnesses already load real bodies in the +normal `cargo test` path, so the precedent exists. + +courses=0 BELOW GLOBAL IS NOT A BUG — it is arithmetic, and the ticket''s note +attributing it to T-1239 is superseded. Global now reports courses=73 drawn=18 +(T-1239 fixed). Region/District/Quarter report courses=0 because the hydrology +network is 615 river cells on a 512x256 grid (74.4 km/cell) and a Region window +is 489 x 205 km = 0.0217% of a 462M km2 body: expected river cells in view = 615 +x 0.000217 = 0.13. Zero is the likely outcome, not a failure. The real +observation underneath it is that 615 river cells on an Earth-sized body is a +sparse network (~45,000 km of total watercourse), which is a believability +question for hydrology, not a rendering one. + +THE OPEN DECISION — three readings, materially different work: + + (A) Make the DISPLAYED channels carry sub-source-pixel composition. The literal + reading of this ticket''s acceptance bar ("at Region, ~98 gridunits across a + single source pixel must carry visible, deterministic, non-repeating + composition"). Means morphology and/or elev_q gain a fine tier, which is + D-227/D-258 territory and the largest change. + (B) Accept that the base view is morphology + elevation, and judge composition + through the overlays — i.e. the ladder gains VEG/MST companion shots. Test + change only, but it concedes that the default map stays flat below Global. + (C) Raise inclusion density so the existing vegetation texture reads at map + scale. Tuning only; 1.07% is near-invisible. + +Not chosen here — it needs Jeroen''s call, and it touches D-258.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-16 10:32:31.457', NULL, '21d816226e574f3b6489810453220b5e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; diff --git a/server/src/atlas/composition.rs b/server/src/atlas/composition.rs index 83bcc5a7a..3be38f42d 100644 --- a/server/src/atlas/composition.rs +++ b/server/src/atlas/composition.rs @@ -110,6 +110,21 @@ pub const MOISTURE_COMPOSITION_CEILING_Q: i32 = 12; /// slope. pub const SLOPE_COMPOSITION_CEILING_Q: i32 = 6; +/// The floor on how much of a District patch the minority classes must hold for +/// composition to count as having happened (D-258 invariant 2's lower bound). +/// +/// Deliberately far below the measured value rather than tuned to it: at the +/// descent ladder's own anchor on Ferrath the minority holds 1.07% (175 of +/// 16,384 samples), and this floor is 0.1%. It is a "did anything happen at all" +/// tripwire, not a quality bar — the bar for whether the map READS as composed +/// is visual, and lives in the ladder captures rather than in an assertion. +/// +/// One-sided on purpose. The upper bound (conservation — the majority must +/// survive) is the invariant's other half and is asserted separately; a single +/// combined check is what let the previous version collapse into a tautology. +#[cfg(test)] +const MIN_COMPOSED_MINORITY_SHARE: f64 = 0.001; + /// The fine-band composition field at a world position, in `[-1, 1]`. /// /// Only [`VOXEL_OCTAVE_WAVELENGTHS_M`] (1,024–128 m) — the band that actually @@ -398,9 +413,34 @@ mod tests { tally ); // ...and it must not be a monoculture either, or nothing was composed. + // + // This assertion used to read `tally.len() > 1 || share == 1.0`, which is + // a TAUTOLOGY: a single-class tally has a 100% share by definition, so + // both branches were always satisfiable and the check could never fail — + // including in the exact case its own message describes, "nothing was + // composed". It was the only thing standing behind D-258's invariant 2 + // (descending reveals COMPOSITION), and it was standing behind nothing. + // + // Both bounds are now asserted separately, because the invariant is + // two-sided: conservation caps how much may be invented (above), and + // composition sets a floor on how little (here). Measured at this anchor: + // 2 classes, minority 175/16384 = 1.07%. assert!( - tally.len() > 1 || share == 1.0, - "tally {tally:?} — expected either composition or an honestly uniform patch" + tally.len() > 1, + "tally {tally:?} — a District patch resolved to ONE vegetation class. \ + D-258 invariant 2 says a summarised cell must be able to CONTAIN the \ + minority it suppressed; a monoculture here means the fine tier did \ + nothing at this scale, which is the flat map this work exists to fix." + ); + let minority = total - count; + let minority_share = minority as f64 / total as f64; + assert!( + minority_share >= MIN_COMPOSED_MINORITY_SHARE, + "minority classes hold {:.2}% of the patch, below the {:.2}% floor — \ + inclusions this sparse are indistinguishable from none at map scale. \ + tally {tally:?}", + 100.0 * minority_share, + 100.0 * MIN_COMPOSED_MINORITY_SHARE, ); eprintln!( "conservation: majority class {majority} at {:.1}% across {} classes {:?}", diff --git a/tooling/atlas-flatness b/tooling/atlas-flatness new file mode 100755 index 000000000..c777a7348 --- /dev/null +++ b/tooling/atlas-flatness @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Measure how much STRUCTURE an Atlas capture carries, per rung. + +"Flat" is the word T-1213/D-258 use for the defect the descent ladder exposes, +but a word cannot be an acceptance gate and an eyeball cannot be a regression +test. This turns the ladder into numbers. + +WHY NOT JUST COUNT COLOURS +-------------------------- +Because the count goes the wrong way. Measured on the 2026-08-16 cold ladder: + + rung distinct lum p1-p99 + Global 1581 145.69 + Region 2923 33.59 + District 53 13.72 + Quarter 46 11.01 + +Region carries almost TWICE Global's distinct-colour count while holding a +quarter of its structure — that is the dither/stipple pass (T-1194) adding colour +noise, not information. A metric that rewards speckle would have called the +flattest rung the richest. So the headline number here is the 1st-99th percentile +luminance spread, which ignores per-pixel noise and measures the large-scale +variation a map is actually read for; distinct-count is reported alongside +precisely so the two can be seen disagreeing. + +Usage: + tooling/atlas-flatness .cache/screenshots/atlas_GJ820Bc_land_Region.png [...] + tooling/atlas-flatness --ladder # the standard descent ladder +""" + +import argparse +import sys +from pathlib import Path + +try: + from PIL import Image +except ImportError: # pragma: no cover - environment guard + print("atlas-flatness: Pillow not installed", file=sys.stderr) + sys.exit(2) + +REPO_ROOT = Path(__file__).resolve().parent.parent +SHOTS = REPO_ROOT / ".cache" / "screenshots" + +# The standard descent ladder: one body, one world point, once per rung. +LADDER = [ + ("Global", "atlas_GJ820Bc_Global.png"), + ("Region", "atlas_GJ820Bc_land_Region.png"), + ("District", "atlas_GJ820Bc_land_District.png"), + ("Quarter", "atlas_GJ820Bc_land_Quarter.png"), +] + +# Terrain-only crop. The header/legend panels sit top-left and the overlay chips +# top-right; both are flat UI fills that would drag every statistic toward +# whatever the panel colour happens to be, and they do not vary with the rung. +CROP_LEFT = 700 +CROP_TOP = 120 + + +def luminance(px) -> float: + r, g, b = px[:3] + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def measure(path: Path) -> dict: + img = Image.open(path).convert("RGB") + w, h = img.size + if w <= CROP_LEFT or h <= CROP_TOP: + raise ValueError(f"{path.name} is {w}x{h}, smaller than the UI crop") + img = img.crop((CROP_LEFT, CROP_TOP, w, h)) + # get_flattened_data() is the Pillow 12+ name; getdata() is deprecated there + # and removed in 14, but is all that older Pillows have. + reader = getattr(img, "get_flattened_data", None) or img.getdata + pixels = list(reader()) + + stats = {} + for name, ch in zip("RGB", zip(*pixels)): + n = len(ch) + mean = sum(ch) / n + stats[name] = (sum((v - mean) ** 2 for v in ch) / n) ** 0.5 + + lums = sorted(luminance(p) for p in pixels) + n = len(lums) + return { + "distinct": len(set(pixels)), + "std": stats, + "spread": lums[int(n * 0.99)] - lums[int(n * 0.01)], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Measure Atlas capture structure") + parser.add_argument("images", nargs="*", type=Path) + parser.add_argument( + "--ladder", + action="store_true", + help="measure the standard descent ladder in .cache/screenshots/", + ) + args = parser.parse_args() + + targets = [] + if args.ladder: + targets = [(label, SHOTS / name) for label, name in LADDER] + targets += [(p.stem, p) for p in args.images] + + if not targets: + parser.print_help() + return 2 + + print(f"{'rung':22} {'distinct':>9} {'R std':>7} {'G std':>7} {'B std':>7} {'lum p1-p99':>11}") + missing = 0 + for label, path in targets: + if not path.exists(): + print(f"{label:22} MISSING {path}") + missing += 1 + continue + m = measure(path) + r, g, b = (m["std"][c] for c in "RGB") + print( + f"{label:22} {m['distinct']:>9} {r:>7.2f} {g:>7.2f} {b:>7.2f} {m['spread']:>11.2f}" + ) + return 1 if missing else 0 + + +if __name__ == "__main__": + sys.exit(main())