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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {:?}",
|
||||
|
||||
Executable
+125
@@ -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())
|
||||
Reference in New Issue
Block a user