test(simulation): the conservation gate's monoculture check was a tautology (T-1213)

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 12:35:19 +02:00
co-authored by Claude Opus 5
parent 6e6218d654
commit 869837f728
4 changed files with 449 additions and 2 deletions
+158
View File
@@ -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 <name> — 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 <name> — 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;
+124
View File
@@ -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 <name> — 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;
+42 -2
View File
@@ -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,024128 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 {:?}",
+125
View File
@@ -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())