islaApocalypse-v2/Tools/Scripts/ShapingOracle.cs
beezm 03fe75b378 rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat
The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.

A1 — the defaults ARE the locked shape now. Five fields actually move:
SpeckRevert false->true, MinLandComponentFrac 3e-5->2.5e-7 (120x smaller; the
config default would have eaten real islands, not specks), SouthStretch 0->2,
FragmentAmp 0->0.5, Erosion false->true. Seven more were already correct via
SouthernStretch.Default* / CoastalFragment.Default* and are now pinned as
literals, because TerrainShapeV1 used to do that pinning and this default set
inherits the job. CoastShelf stays OFF — the locked shape has no shelf, and
evaluating it (D-041) is its own later task once water renders. Offshore stays
Off permanently (D-063): islands are organic-only, made by the stretch +
fragmentation and identified by the region layer, never placed.

A2 — the preserve mechanism. The curve knots are percentiles of the FAMILY-OFF
land distribution; flipping the defaults would have moved the pool, the knots,
and with them the render field of every batch including terrain-shape-v1
itself. So the pool is pinned family-off (TerrainGenConfig.WithFamilyOff /
CalibrationPool) rather than the knots being baked: calibration stays live, its
INPUT distribution is held still. The pin was a no-op by construction — it sets
the values the defaults carried the instant before the flip — and re-measuring
after confirms it: pool, all six knots, per-seed spread, shaped max,
monotonicity spikeMax and all seven band shares identical.

  Applied wider than "in CalibrateCurve": OffshoreIslandsTool,
  RegionLabelingTool and SouthernStretchTool generate their own family-off
  field for the Phase-1 anchor, so the pool pin alone would NOT have covered
  them and their a1 would have failed for a configuration reason. TerrainGenTool
  too — it AUTHORED 02_pass1_port and must stay able to regenerate its own
  anchor.

  Recorded as a judged-and-parked property: knots measured family-off, applied
  family-on. Deliberate, not an oversight. Same disposition as the mid-slope
  feather.

A4 — no oracle may pass against a superseded baseline. Six anchors retired
(01/03/04/06/08/09) with their checks and ISLA_T0x_SOURCE defaults; three kept
(chat1/02_pass1_port as the family-off pass-1 guard, chat2/10 and chat2/11 as
the shape and erosion acceptance anchors). Two invariants were RE-POINTED
rather than lost — the southern stretch's north-lock and the coastal-fragment
interior-lock now compare against SAME-RUN fields, which is scale-free and
cannot be invalidated by a moved dump. The retired dumps are kept, not deleted,
and marked superseded in their INDEX.md.

  A missing anchor is now LOUD. The old pattern skipped silently, so a moved
  anchor did not make its oracle fail — it made it not RUN, and a batch with a
  skipped check prints an all-PASS table that reads like a clean one. That is
  the INVERSE of the hazard the re-baseline guards against, and the migration
  below is exactly the event that would have triggered it, on nine anchors at
  once. ShapingOracle.LoadAnchor now separates the two cases: absent -> throw;
  present at another size -> loud INCONCLUSIVE, which is a fail, never a pass.

  TerrainShapeV1 inverted from PRESET to GUARD and moved to its own file.
  Apply() is gone — stamping the values on top of the defaults would MASK a
  drift instead of catching it. Its constants are now the assertion target, and
  Assert() refuses a run whose defaults have drifted off the locked shape.

B/C — batches are namespaced by chat: batches/<chat>/NN_slug/. Task numbers
restart at 00 per chat, so a flat root collided the moment a second chat
existed — four colliding prefixes across 25 batches, separable only by slug.
ToolingPaths.ChatSlug is REQUIRED (throws if unset) and defaults per tool to
its authoring chat, so re-running reproduces a batch in place while ISLA_CHAT
redirects — which is also what stops an acceptance run from overwriting the
very anchor it checks against. Writes go through BatchRoot; historical READS
compose against BatchesRoot and so carry the prefix in their own source string
("chat1/02_pass1_port"). The 25 existing batches were migrated moves-only.

ACCEPTANCE — 16 of 16 byte-identical, 0 failed. All 8 gallery seeds at 8192
from the bare defaults are byte-identical to chat2/10_frag4_seed_gallery
(= terrain-shape-v1, a59e52f); all 8 erosion fields byte-identical to
chat2/11_erosion (= ea291ea). Every gallery table row and every erosion
statistic reproduces its recorded value exactly. DrainageTool's a11 passes
bit-identical over 67,108,864 cells, and its analysis reproduces batch 12
exactly — so the whole chain rivers depends on (shape -> erosion -> drainage)
is unchanged. All 12 edited tools re-run clean; both new guards negative-tested.

The baseline moved in DEFAULTS, not in TERRAIN.

-> XX_Human/output/rivers/01_rebaseline_and_batch_namespace.report.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
2026-08-23 04:45:10 -04:00

765 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐⭐ THE ORACLE — the automatic correctness checks that let the developer's eye judge ONLY
/// relief.
///
/// ═══ WHY THIS EXISTS AT ALL ═══
///
/// The prototype's single most valuable terrain lesson was not about terrain:
///
/// > *"Five rounds of taste-iteration were safe BECAUSE correctness was not being judged by eye.
/// > Where a future phase has a subjective gate, ask first what the automatic invariant is."*
/// > — `Design - Tooling - Iteration and Batching.md`, "build the oracle before the
/// > taste-iteration, not after".
///
/// The curve is a subjective gate. So before a single render is looked at, four things are
/// proven mechanically:
///
/// (a) REGRESSION curve OFF is bit-identical to Phase 1's pass-1 output.
/// ⇒ the port disturbed nothing upstream.
/// (b) CLASSIFY FIDELITY the classify field is bit-identical to the raw pre-curve field,
/// curve on or off. ⇒ the oracle field is actually an oracle.
/// (c) MONOTONICITY the effective per-seed curve is strictly increasing everywhere.
/// ⇒ no peak has become a pit.
/// (d) BAND SHARES realized land shares match the 60/13/10/5/8/3/1 targets.
/// ⇒ the calibration did what it claimed.
///
/// ⚠ BIT-IDENTICAL MEANS BIT-IDENTICAL. These compare IEEE-754 bit patterns, not values within
/// an epsilon. "Close enough" is how a drift becomes a fact — and the whole point of the `.f32`
/// dump is that two generators agree or their dumps differ.
///
/// ⚠ Any failure fails the TASK, loudly. Nothing here papers over a mismatch: a check that
/// reports "mostly passed" is a check that has stopped working.
/// </summary>
public static class ShapingOracle
{
/// <summary>One check's verdict. <see cref="Detail"/> carries the evidence either way.</summary>
public sealed class Check
{
public string Id; // "a", "b", "c", "d"
public string Name;
public bool Passed;
public string Detail;
public override string ToString() => $"[{(Passed ? "PASS" : "FAIL")}] ({Id}) {Name} — {Detail}";
}
/// <summary>
/// Compare two float fields for BIT equality. Returns the number of differing cells and the
/// first difference found, so a failure is actionable rather than just red.
/// </summary>
public static (long differing, string firstDiff) CompareBitwise(float[,] a, float[,] b, int mapSize)
{
long differing = 0;
string first = null;
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
int ba = BitConverter.SingleToInt32Bits(a[x, y]);
int bb = BitConverter.SingleToInt32Bits(b[x, y]);
if (ba == bb) continue;
differing++;
first ??= $"first at [{x},{y}]: {a[x, y]:G9} (0x{ba:X8}) vs {b[x, y]:G9} (0x{bb:X8})";
}
}
return (differing, first);
}
/// <summary>
/// (a) REGRESSION — with the curve off, shaping must return the pass-1 field untouched.
///
/// ⚠ This is the WEAKER, always-available half of check (a): it proves pass 2 is a no-op when
/// gated off. The stronger half — that pass 1 ITSELF still matches Phase 1 byte-for-byte — is
/// <see cref="RegressionAgainstDump"/>, which needs a Phase-1 `.f32` on disk.
/// </summary>
public static Check RegressionCurveOff(Pass1Result p1, Pass2Result offResult)
{
var c = new Check { Id = "a", Name = "regression: curve OFF == pass-1 output" };
if (offResult.CurveOn)
{
c.Passed = false;
c.Detail = "the result handed in was generated with the curve ON — wrong variant.";
return c;
}
var (differing, firstDiff) = CompareBitwise(p1.Height, offResult.Height, p1.MapSize);
bool aliased = offResult.FieldsAreAliased;
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical over {(long)p1.MapSize * p1.MapSize:N0} cells" +
(aliased ? " (and the fields alias one array, as the reference did)" : "")
: $"{differing:N0} cells differ — {firstDiff}";
return c;
}
/// <summary>
/// (a) REGRESSION against a Phase-1 `.f32` dump — the cross-run half.
///
/// ⚠ A MISSING DUMP IS NOT A PASS. It is reported as INCONCLUSIVE and the caller says so; a
/// check that silently succeeds when its input is absent is worse than no check, because it
/// buys confidence that was never earned.
/// </summary>
public static Check RegressionAgainstDump(float[,] current, float[,] phase1Dump, int mapSize, string dumpPath)
{
var c = new Check { Id = "a", Name = "regression: pass-1 == Phase-1 .f32 dump" };
if (phase1Dump == null)
{
c.Passed = false;
c.Detail = $"INCONCLUSIVE — no readable Phase-1 dump at {dumpPath} for this seed/size. " +
"Not counted as a pass; set ISLA_PHASE1_SOURCE to a batch that has one.";
return c;
}
var (differing, firstDiff) = CompareBitwise(phase1Dump, current, mapSize);
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical to {dumpPath} over {(long)mapSize * mapSize:N0} cells"
: $"{differing:N0} cells differ from {dumpPath} — {firstDiff}";
return c;
}
/// <summary>
/// (b) CLASSIFY FIDELITY — the classify field is the raw pre-curve field, bit-for-bit, with
/// the curve on or off.
///
/// This is the invariant every later phase's oracle rests on: biomes and water will classify
/// from this field, so if it has drifted by even one ulp the "md5-identical across shaping
/// changes" guarantee is gone before it is ever used.
/// </summary>
public static Check ClassifyFidelity(Pass1Result p1, Pass2Result p2)
{
var c = new Check { Id = "b", Name = "classify field == raw pass-1 field" };
var (differing, firstDiff) = CompareBitwise(p1.Height, p2.HeightClassify, p1.MapSize);
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical over {(long)p1.MapSize * p1.MapSize:N0} cells (curve {(p2.CurveOn ? "ON" : "OFF")})"
: $"{differing:N0} cells differ — {firstDiff}";
return c;
}
/// <summary>
/// (c) MONOTONICITY — recorded rather than re-run.
///
/// <c>HeightCurve.AssertMonotonic</c> THROWS on violation and is called inside
/// <see cref="Shaping.Shape"/>, so reaching this code at all means the sweep passed. The
/// check exists so the oracle table states it explicitly instead of leaving the strongest
/// guarantee implicit in the absence of a crash.
/// </summary>
public static Check Monotonicity(Pass2Result p2)
{
var c = new Check { Id = "c", Name = "curve strictly monotonic (24-corner sweep)" };
if (!p2.CurveOn)
{
c.Passed = true;
c.Detail = "curve off — nothing to prove (identity is trivially monotonic).";
return c;
}
string note = p2.Notes.Find(n => n.Contains("Monotonicity assertion passed"));
c.Passed = note != null;
c.Detail = note ?? "no monotonicity confirmation recorded — AssertMonotonic did not run.";
return c;
}
/// <summary>
/// (d) BAND SHARES — the realized land shares against the M3 targets.
///
/// ⚠ MEASURED ON THE RAW (INPUT) DISTRIBUTION, because that is where the knots cut. The
/// shares are exact by construction IF the quantile machinery is right — so this check is
/// really a proof that <see cref="LandHistogram"/> measured what it claimed, which is the one
/// thing the reference could never verify about its own knots.
/// </summary>
/// <param name="tolerancePercentagePoints">
/// Allowed absolute deviation per band, in percentage points. The knots come from the SAME
/// histogram, so agreement is limited only by in-bin interpolation — tenths of a point, not
/// whole ones.
/// </param>
public static Check BandShares(LandHistogram raw, CurveKnots k, double tolerancePercentagePoints)
{
var c = new Check { Id = "d", Name = "realized land band shares == 60/13/10/5/8/3/1 targets" };
double[] realized = RealizedShares(raw, k);
double worst = 0.0;
int worstBand = -1;
for (int i = 0; i < realized.Length; i++)
{
double d = Math.Abs(realized[i] - CurveKnots.BandShareTargets[i]);
if (d > worst) { worst = d; worstBand = i; }
}
c.Passed = worst <= tolerancePercentagePoints;
c.Detail = $"worst band '{CurveKnots.BandNames[worstBand]}' off by {worst:F3} pp " +
$"(tolerance {tolerancePercentagePoints:F2} pp); realized " +
string.Join("/", Array.ConvertAll(realized, v => v.ToString("F2")));
return c;
}
/// <summary>
/// The fraction of land, in percent, falling in each of the curve's seven INPUT bands.
/// Band edges are sea, K1..K6, +∞.
/// </summary>
public static double[] RealizedShares(LandHistogram raw, CurveKnots k)
{
float[] edges = { raw.SeaLevel, k.K1, k.K2, k.K3, k.K4, k.K5, k.K6 };
var shares = new double[7];
for (int i = 0; i < 6; i++)
shares[i] = raw.FractionBetween(edges[i], edges[i + 1]) * 100.0;
shares[6] = Math.Max(0.0, (1.0 - raw.FractionBelow(k.K6)) * 100.0);
return shares;
}
/// <summary>
/// A bit-regression against any `.f32` dump, with the caller naming the check — the
/// chat2/02 generalization of <see cref="RegressionAgainstDump"/>, used to hold the
/// staircase mode against task 01's own batch output. The same rule applies: a missing dump
/// is INCONCLUSIVE and counted as a failure, never as a pass.
/// </summary>
public static Check DumpRegression(string id, string name, float[,] current, float[,] dump,
int mapSize, string dumpPath)
{
var c = new Check { Id = id, Name = name };
if (dump == null)
{
c.Passed = false;
c.Detail = $"INCONCLUSIVE — no readable dump at {dumpPath} for this seed/size. Not counted as a pass.";
return c;
}
var (differing, firstDiff) = CompareBitwise(dump, current, mapSize);
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical to {dumpPath} over {(long)mapSize * mapSize:N0} cells"
: $"{differing:N0} cells differ from {dumpPath} — {firstDiff}";
return c;
}
/// <summary>
/// (d) ⭐ LOWLANDS PRESERVED — the rev-3 task's load-bearing check. For every cell whose RAW
/// height is at or below K2 (the toe+red band the developer likes), the continuous mode's
/// output must be BIT-IDENTICAL to the staircase's. This mechanically enforces "do not lift
/// the lowlands": the low pile cannot move if its every column is the same float.
///
/// The identity is by construction — the continuous curve DELEGATES to the staircase's own
/// toe+red code path below K2 — and this check is what keeps that construction honest
/// against refactoring, float re-association, or a ceiling knob bug.
///
/// ⚠ Run against the LIFTED_WRONG bookend this check is EXPECTED to fail — the caller
/// reports that failure as confirmation of the wrong direction, not as a defect.
/// </summary>
public static Check LowlandsPreserved(Pass1Result p1, Pass2Result staircase, Pass2Result variant)
{
var c = new Check { Id = "d", Name = $"lowlands (raw ≤ K2) bit-identical to staircase [{variant.VariantLabel}]" };
float k2 = staircase.Knots.K2;
long compared = 0, differing = 0;
string first = null;
for (int x = 0; x < p1.MapSize; x++)
{
for (int y = 0; y < p1.MapSize; y++)
{
if (p1.Height[x, y] > k2) continue;
compared++;
int ba = BitConverter.SingleToInt32Bits(staircase.Height[x, y]);
int bb = BitConverter.SingleToInt32Bits(variant.Height[x, y]);
if (ba == bb) continue;
differing++;
first ??= $"first at [{x},{y}] raw {p1.Height[x, y]:G9}: " +
$"staircase {staircase.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
}
}
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical over all {compared:N0} lowland cells (raw ≤ K2 = {k2:F6})"
: $"{differing:N0} of {compared:N0} lowland cells differ — {first}";
return c;
}
/// <summary>
/// (e) UPPER CONTINUOUS — sample the climb's slope densely; no near-flat anywhere (a bench
/// reborn), no cliff below the summit onset (the summit itself may steepen — that is the
/// dramatic peak). ⚠ SOFT: this is an exploration batch, so a violation warns loudly and
/// lands in the report rather than failing the run — the tool decides the exit code.
/// </summary>
public static Check UpperClimbProfile(Pass2Result p2)
{
var c = new Check { Id = "e", Name = $"upper climb continuous — no flats, no low-mid cliffs [{p2.VariantLabel}]" };
if (p2.Continuous == null)
{
c.Passed = false;
c.Detail = "no continuous spline on this result — wrong mode handed in.";
return c;
}
var (minN, minAt, maxN, maxAt, nearFlat, cliff) = p2.Continuous.SampleClimbSlopes();
c.Passed = !nearFlat && !cliff;
c.Detail = $"slope (normalized, 1 = climb average): min {minN:F3} at raw {minAt:F4}" +
$"{(nearFlat ? " NEAR-FLAT (a bench reborn)" : "")}, " +
$"max below onset {maxN:F3} at raw {maxAt:F4}" +
$"{(cliff ? " CLIFF below the summit onset" : "")}";
return c;
}
/// <summary>
/// (f) SEA IDENTITY — per cell, not per count: a column is land in the variant exactly when
/// it is land with the curve off. Counting alone could hide two errors that cancel; this
/// cannot. The coastline is the one thing every mode, including the wrong one, must keep.
/// </summary>
public static Check SeaIdentity(Pass2Result off, Pass2Result variant, float seaLevel)
{
var c = new Check { Id = "f", Name = $"sea identity — per-cell landness unchanged [{variant.VariantLabel}]" };
long mismatches = 0;
string first = null;
for (int x = 0; x < off.MapSize; x++)
{
for (int y = 0; y < off.MapSize; y++)
{
bool a = off.Height[x, y] >= seaLevel;
bool b = variant.Height[x, y] >= seaLevel;
if (a == b) continue;
mismatches++;
first ??= $"first at [{x},{y}]: off {off.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
}
}
c.Passed = mismatches == 0;
c.Detail = c.Passed
? $"per-cell landness identical over {(long)off.MapSize * off.MapSize:N0} cells"
: $"{mismatches:N0} cells changed sides of the waterline — {first}";
return c;
}
/// <summary>
/// (g) ⭐ MOUNTAIN RESTORED (chat2/03) — how much land ends up above 100 m and 220 m,
/// against the staircase's own figures.
///
/// ═══ ⚠ REPORTED, NOT GATED ═══
///
/// This is a taste target the developer tunes, so a miss is a FINDING, not a build failure —
/// gating it would make `mountainLift` unusable as a knob, since every value but one would
/// fail the run. What it must never do is stay silent: chat2/02 lost two thirds of the
/// mountain and only found out because someone went looking at the dumps afterwards. This
/// check is that look, made automatic.
///
/// <paramref name="tolerancePp"/> only decides whether the row reads PASS or NOTE; the
/// numbers are always printed.
/// </summary>
public static Check MountainRestored(Pass2Result variant, Pass2Result staircase,
float seaLevel, double tolerancePp)
{
var c = new Check { Id = "g", Name = $"mountain restored vs staircase [{variant.VariantLabel}]" };
var (v100, v220) = LandAbove(variant, seaLevel);
var (s100, s220) = LandAbove(staircase, seaLevel);
double d100 = v100 - s100;
c.Passed = Math.Abs(d100) <= tolerancePp;
c.Detail = $">100 m: {v100:F2} % vs staircase {s100:F2} % ({d100:+0.00;-0.00} pp) · " +
$">220 m: {v220:F2} % vs {s220:F2} % ({v220 - s220:+0.00;-0.00} pp)";
return c;
}
/// <summary>Percentage of LAND above 100 m and 220 m of world height. Land = at/above sea.</summary>
public static (double above100, double above220) LandAbove(Pass2Result p2, float seaLevel)
{
float t100 = seaLevel + WorldScale.RawFromMetres(100f);
float t220 = seaLevel + WorldScale.RawFromMetres(220f);
long land = 0, a100 = 0, a220 = 0;
for (int x = 0; x < p2.MapSize; x++)
{
for (int y = 0; y < p2.MapSize; y++)
{
float h = p2.Height[x, y];
if (h < seaLevel) continue;
land++;
if (h > t100) a100++;
if (h > t220) a220++;
}
}
return land == 0 ? (0.0, 0.0) : (100.0 * a100 / land, 100.0 * a220 / land);
}
// ═══ chat2/05 — the offshore checks ═══
// (h) was the chat2/05 seeded-floor check — reverted out with the floor in chat2/06. No
// count is guaranteed any more, so there is nothing for an oracle to assert; the count
// table is the evidence, and it is statistics, not a check.
/// <summary>
/// (i) ⭐ MOAT INTACT — no offshore island is 8-connected to mainland land. The moat exists
/// to make a land bridge impossible; this is the proof that it did.
/// </summary>
public static Check MoatIntact(Pass1Result p1, List<IslandComponent> comps)
{
var c = new Check { Id = "i", Name = "moat intact — no island touches the mainland" };
int bridged = OffshoreAnalysis.BridgedCount(comps);
c.Passed = p1.HasIslandTag && bridged == 0;
c.Detail = !p1.HasIslandTag ? "no island tag — nothing to check"
: bridged == 0 ? $"all {comps.Count} islands are separated from mainland by water"
: $"{bridged} island(s) BRIDGE to mainland land";
return c;
}
/// <summary>
/// (j) ⭐ MAINLAND UNMOVED — with offshore on vs off, every cell that was LAND with it off is
/// BIT-IDENTICAL with it on. The shelf touches only below-sea cells, the islets only lift
/// below-sea cells; neither may touch existing land. (The falloff test and the moat did their
/// job if this holds.) Reports how many sea cells the shelf moved and how many were lifted.
/// </summary>
public static Check MainlandUnmoved(Pass1Result off, Pass1Result on, float sea)
{
var c = new Check { Id = "j", Name = "mainland unmoved — every offshore-OFF land cell bit-identical with offshore ON" };
long land = 0, landDiff = 0, seaChanged = 0, lifted = 0;
string first = null;
for (int x = 0; x < off.MapSize; x++)
{
for (int y = 0; y < off.MapSize; y++)
{
float a = off.Height[x, y], b = on.Height[x, y];
if (a >= sea)
{
land++;
if (BitConverter.SingleToInt32Bits(a) != BitConverter.SingleToInt32Bits(b))
{
landDiff++;
first ??= $"first at [{x},{y}]: {a:G9} → {b:G9}";
}
}
else
{
if (a != b) seaChanged++;
if (b >= sea) lifted++;
}
}
}
c.Passed = landDiff == 0;
c.Detail = landDiff == 0
? $"all {land:N0} land cells bit-identical; {seaChanged:N0} sea cells remapped by the shelf, {lifted:N0} lifted to land"
: $"{landDiff:N0} of {land:N0} land cells CHANGED — {first}";
return c;
}
/// <summary>
/// (k) TAG ↔ COASTLINE CONSISTENT — per cell, classify-land ⇔ render-land (the curve is
/// identity at sea and monotone above, so it must be — D-046), and every TAGGED cell is land
/// in both fields. This is what lets the tag be carried through pass 2 without recomputation.
/// </summary>
public static Check TagCoastlineConsistent(Pass2Result p2, float sea)
{
var c = new Check { Id = "k", Name = "offshore tag: classify/render coastline consistent, every tagged cell is land" };
long mismatch = 0, tagNotLand = 0, tagged = 0;
for (int x = 0; x < p2.MapSize; x++)
{
for (int y = 0; y < p2.MapSize; y++)
{
bool cl = p2.HeightClassify[x, y] >= sea;
bool rl = p2.Height[x, y] >= sea;
if (cl != rl) mismatch++;
if (p2.IsIsland != null && p2.IsIsland[x, y])
{
tagged++;
if (!cl || !rl) tagNotLand++;
}
}
}
c.Passed = mismatch == 0 && tagNotLand == 0;
c.Detail = $"{mismatch:N0} classify/render landness mismatches; {tagNotLand:N0} of {tagged:N0} tagged cells not land";
return c;
}
/// <summary>
/// (l) HMaxSeed RECOMPUTED AFTER SHELF + OFFSHORE — reported. Expected unchanged (a ~34 m
/// crest vs a ~290 m peak), but the ORDER is the fix (chat2/00 Drift §2), and the value is
/// measured rather than assumed. Always passes; the detail is the point.
/// </summary>
public static Check HMaxAfterOffshore(Pass1Result p1)
{
bool moved = p1.HMaxSeed != p1.HMaxSeedBeforeOffshore;
return new Check
{
Id = "l", Name = "HMaxSeed recomputed after shelf + offshore",
Passed = true,
Detail = $"before {p1.HMaxSeedBeforeOffshore:F6} → after {p1.HMaxSeed:F6} " +
(moved ? "— ⚠ MOVED (an islet outran the peak?)" : "— unchanged, as expected; the ORDER is now right by construction"),
};
}
// ═══ chat2/07 — the region checks ═══
/// <summary>(m) CENTRE IS LAND — the mainland definition held (the massif is centred); the fallback was not needed.</summary>
public static Check CentreIsLand(Pass1Result p1)
{
var c = new Check { Id = "m", Name = "mainland = centre component (centre cell is land, no fallback)" };
if (p1.Regions == null) { c.Passed = false; c.Detail = "no region labeling on this field"; return c; }
var m = p1.Regions.Mainland;
c.Passed = p1.Regions.CentreWasLand && m != null && (p1.RegionLedger == null || p1.RegionLedger.CentreWasLandPre);
c.Detail = c.Passed
? $"centre is land; mainland id {p1.Regions.MainlandId}, {m.SizeCells:N0} cells, centroid ({m.CentroidX:F0},{m.CentroidY:F0}); {p1.Regions.IslandCount} islands"
: "⚠ CENTRE CELL IS NOT LAND — fell back to the largest component";
return c;
}
/// <summary>
/// (n) ⭐ THE REVERT GUARDS, RE-PROVEN ON THE FIELDS — filter OFF vs ON: every cell of the OFF
/// field's MAINLAND component is bit-identical; every cell that changed was land in a
/// sub-threshold NON-MAINLAND component of the OFF labeling (component-only) and went DOWN, to
/// below sea (lower-only); nothing else moved. The pass asserted this as it ran; this is the
/// independent proof on the finished fields.
/// </summary>
public static Check RevertGuards(Pass1Result off, Pass1Result on, float sea, long thresholdCells)
{
var c = new Check { Id = "n", Name = "speck revert: mainland bit-identical, every change is in a sub-threshold island and lower-only" };
if (off.Regions == null) { c.Passed = false; c.Detail = "the OFF field has no region labeling"; return c; }
int n = off.MapSize;
var lab = off.Regions;
long mainland = 0, mainlandDiff = 0, changed = 0, notIsland = 0, notSmall = 0, raised = 0, notSea = 0;
string first = null;
for (int x = 0; x < n; x++)
{
for (int y = 0; y < n; y++)
{
float a = off.Height[x, y], b = on.Height[x, y];
int id = lab.Id[x * n + y];
bool isMain = id != 0 && id == lab.MainlandId;
if (isMain) mainland++;
if (BitConverter.SingleToInt32Bits(a) == BitConverter.SingleToInt32Bits(b)) continue;
changed++;
if (isMain) { mainlandDiff++; first ??= $"mainland cell [{x},{y}] {a:G9} → {b:G9}"; continue; }
if (id == 0) { notIsland++; first ??= $"sea cell [{x},{y}] changed {a:G9} → {b:G9}"; continue; }
if (lab.Regions[id - 1].SizeCells >= thresholdCells) { notSmall++; first ??= $"cell [{x},{y}] of component {id} ({lab.Regions[id - 1].SizeCells} cells ≥ {thresholdCells}) changed"; }
if (b >= a) { raised++; first ??= $"cell [{x},{y}] RAISED {a:G9} → {b:G9}"; }
if (b >= sea) { notSea++; first ??= $"cell [{x},{y}] still land after revert ({b:G9})"; }
}
}
c.Passed = mainlandDiff == 0 && notIsland == 0 && notSmall == 0 && raised == 0 && notSea == 0;
c.Detail = c.Passed
? $"all {mainland:N0} mainland cells bit-identical; {changed:N0} cells changed, every one in a sub-threshold island, lowered below sea"
: $"VIOLATION — mainland {mainlandDiff:N0} / non-island {notIsland:N0} / over-threshold {notSmall:N0} / raised {raised:N0} / still land {notSea:N0} — {first}";
return c;
}
/// <summary>(o) LABELS DETERMINISTIC — two generations of the same seed: id maps and component tables identical.</summary>
public static Check LabelsDeterministic(Pass1Result a, Pass1Result b)
{
var c = new Check { Id = "o", Name = "region ids deterministic per seed (two runs, id map + table identical)" };
if (a.Regions == null || b.Regions == null) { c.Passed = false; c.Detail = "no region labeling"; return c; }
long diff = 0; int n = a.MapSize;
for (int i = 0; i < n * n; i++) if (a.Regions.Id[i] != b.Regions.Id[i]) diff++;
bool table = a.Regions.Regions.Count == b.Regions.Regions.Count && a.Regions.MainlandId == b.Regions.MainlandId;
if (table)
for (int i = 0; i < a.Regions.Regions.Count; i++)
{
var ra = a.Regions.Regions[i]; var rb = b.Regions.Regions[i];
if (ra.SizeCells != rb.SizeCells || ra.CentroidX != rb.CentroidX || ra.CentroidY != rb.CentroidY || ra.Hemisphere != rb.Hemisphere || ra.IsMainland != rb.IsMainland) { table = false; break; }
}
c.Passed = diff == 0 && table;
c.Detail = c.Passed ? $"{a.Regions.Regions.Count} components, id map identical over {(long)n * n:N0} cells, tables identical"
: $"{diff:N0} id cells differ; tables {(table ? "identical" : "DIFFER")}";
return c;
}
// ═══ chat2/08 — the southern-stretch band checks ═══
/// <summary>
/// (p) ⭐ NORTH BIT-LOCKED, BOTH DIRECTIONS — every cell with y &lt; <paramref name="bandRow"/> is
/// bit-identical between the two fields (no land lowered, no sea raised); reports how many cells
/// differ at/below the band (allowed — that is the relaxation). The surgical guarantee, per cell.
/// </summary>
public static Check NorthLocked(string id, string name, float[,] a, float[,] b, int mapSize, int bandRow)
{
var c = new Check { Id = id, Name = name };
long northDiff = 0, southDiff = 0, north = 0; string first = null;
for (int x = 0; x < mapSize; x++)
for (int y = 0; y < mapSize; y++)
{
bool same = BitConverter.SingleToInt32Bits(a[x, y]) == BitConverter.SingleToInt32Bits(b[x, y]);
if (y < bandRow) { north++; if (!same) { northDiff++; first ??= $"[{x},{y}] {a[x, y]:G9} vs {b[x, y]:G9}"; } }
else if (!same) southDiff++;
}
c.Passed = northDiff == 0;
c.Detail = c.Passed
? $"all {north:N0} cells north of row {bandRow} bit-identical; {southDiff:N0} cells differ in the band or below (the relaxation)"
: $"{northDiff:N0} cells north of row {bandRow} DIFFER — {first}";
return c;
}
/// <summary>(q) ⭐ NORTHERN ISLANDS INVARIANT — the multiset of north-hemisphere island components (size, centroid) is identical between two labelings.</summary>
public static Check NorthIslandsInvariant(string name, RegionLabels a, RegionLabels b)
{
var c = new Check { Id = "q", Name = name };
if (a == null || b == null) { c.Passed = false; c.Detail = "no region labeling"; return c; }
var sa = NorthSet(a); var sb = NorthSet(b);
bool same = sa.Count == sb.Count;
if (same) for (int i = 0; i < sa.Count; i++) if (sa[i] != sb[i]) { same = false; break; }
c.Passed = same;
c.Detail = same ? $"{sa.Count} northern islands, identical (size + centroid)" : $"DIFFER — {sa.Count} vs {sb.Count} northern islands, or a size/centroid moved";
return c;
}
private static List<string> NorthSet(RegionLabels l)
{
var list = new List<string>();
foreach (var r in l.Regions)
if (!r.IsMainland && r.Hemisphere == RegionLabeling.HemiNorth)
list.Add($"{r.SizeCells}:{r.CentroidX:F3}:{r.CentroidY:F3}");
list.Sort(StringComparer.Ordinal);
return list;
}
// ═══ chat2/09 — the coastal-fragmentation checks ═══
/// <summary>
/// (r) ⭐ INTERIOR LOCKED — every cell whose BASELINE pre-trench falloff is clear of the coastal
/// window (weight exactly 0: the interior, the massif, the deep sea) is bit-identical between the
/// baseline and the fragmented field. Reports how many cells changed inside the window (the
/// coast, allowed). The proof that fragmentation cannot reach inland.
/// </summary>
public static Check InteriorLocked(Pass1Result baseline, Pass1Result frag, float centre, float halfWidth)
{
var c = new Check { Id = "r", Name = "interior locked — every cell clear of the coastal window bit-identical (classify)" };
int n = baseline.MapSize; long outside = 0, outsideDiff = 0, inside = 0, insideDiff = 0; string first = null;
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
bool inWindow = MathF.Abs(baseline.PreTrenchFalloff[x, y] - centre) < halfWidth;
bool same = BitConverter.SingleToInt32Bits(baseline.Height[x, y]) == BitConverter.SingleToInt32Bits(frag.Height[x, y]);
if (inWindow) { inside++; if (!same) insideDiff++; }
else { outside++; if (!same) { outsideDiff++; first ??= $"[{x},{y}] f {baseline.PreTrenchFalloff[x, y]:F3}: {baseline.Height[x, y]:G9} → {frag.Height[x, y]:G9}"; } }
}
c.Passed = outsideDiff == 0;
c.Detail = c.Passed
? $"all {outside:N0} cells outside the window bit-identical; {insideDiff:N0} of {inside:N0} window cells changed (the coast)"
: $"{outsideDiff:N0} cells OUTSIDE the window changed — {first}";
return c;
}
/// <summary>
/// (s) informational — HIGH GROUND: of the cells whose BASELINE raw height is at or above
/// <paramref name="rawThreshold"/>, how many changed, and the largest change. A coastal hill
/// inside the window may legitimately move in height without flipping; this reports it.
/// </summary>
public static Check HighGroundReport(Pass1Result baseline, Pass1Result frag, float rawThreshold, string thresholdLabel)
{
var c = new Check { Id = "s", Name = $"(informational) high ground ≥ {thresholdLabel}: cells changed / largest |Δ|", Passed = true };
int n = baseline.MapSize; long high = 0, changed = 0; float maxAbs = 0f;
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
float a = baseline.Height[x, y]; if (a < rawThreshold) continue;
high++;
float b = frag.Height[x, y];
if (a != b) { changed++; float d = MathF.Abs(b - a); if (d > maxAbs) maxAbs = d; }
}
c.Detail = $"{changed:N0} of {high:N0} high cells changed; largest |Δ| {maxAbs:G4} raw ({Core.WorldScale.MetresFromRaw(maxAbs):F1} m)";
return c;
}
// ═══ ⭐⭐ ANCHOR RESOLUTION — A MISSING ANCHOR IS LOUD (rivers/01) ═══════════════════════════
//
// ═══ THE FAILURE MODE THIS CLOSES ═══
//
// Every anchor check used to be written as:
//
// if (File.Exists(dump) && mapSize == 8192) hard.Add(DumpRegression(...));
// else GD.Print(" ⚠ skipped — no dump at …");
//
// so a moved, renamed or deleted anchor did not make the oracle FAIL. It made the oracle
// NOT RUN — and a batch with a silently-skipped check prints an all-PASS table and reads
// exactly like a clean one. The `INDEX.md` is then evidence for a claim nothing checked.
//
// > ### ⚠ This is the INVERSE of the hazard the re-baseline guards against.
// > The re-baseline stops an oracle PASSING FOR THE WRONG REASON. This stops one
// > DISAPPEARING FOR NO REASON. Both end with a green table and an unproven claim, and the
// > rivers/01 batch-root migration is exactly the event that would have triggered the second
// > one — nine anchors moved under `<chat>/` in a single commit.
//
// The two cases the old code conflated are now separated, in ONE place (`LoadAnchor`) so every
// anchor site behaves identically:
// MISSING FILE the anchor moved, was renamed, or was never written. THROWS — the check
// cannot run, and dropping it quietly is the failure this exists to close.
// WRONG SIZE the anchor exists, but only at the size it was captured. A legitimate case
// (a probe run at another size); reported loudly and recorded INCONCLUSIVE,
// which is a FAIL in the table. ⚠ Deliberately NOT a throw: a guard that fires
// on ordinary small-map probe work is a guard people learn to route around.
/// <summary>
/// ⭐ Load a regression anchor's `.f32`, separating the two failures the old code conflated.
///
/// FILE ABSENT → THROWS. The anchor moved, was renamed, or was never written.
/// This is the migration hazard, and it is not survivable: the
/// check cannot run and must not be quietly dropped.
/// PRESENT, WRONG SIZE → returns null, LOUDLY. A legitimate case — an anchor exists only
/// at the size it was captured, and a batch run at another size
/// genuinely cannot check against it. The caller's
/// <see cref="DumpRegression"/> records it INCONCLUSIVE, which is
/// a FAIL in the table, never a pass.
///
/// ⚠ The distinction matters because only ONE of them means something is broken. Throwing on a
/// size mismatch would make every small-map probe run refuse, and a guard that fires on ordinary
/// work is a guard people route around.
/// </summary>
/// <param name="checkId">The oracle check this anchor feeds, e.g. "a10" — named in the message.</param>
/// <param name="envVar">The env override that can re-point it, e.g. "ISLA_T10_SOURCE".</param>
/// <param name="dumpPath">The resolved absolute path.</param>
/// <param name="mapSize">The field size to read.</param>
public static float[,] LoadAnchor(string checkId, string envVar, string dumpPath, int mapSize)
{
if (!System.IO.File.Exists(dumpPath))
throw new InvalidOperationException(
$"[Oracle] MISSING ANCHOR for check '{checkId}' — nothing at:\n" +
$" {dumpPath}\n" +
"An oracle whose anchor is absent does not fail, it does not RUN — and a batch with a " +
"silently-skipped check prints an all-PASS table that reads exactly like a clean one. " +
"Refusing to render evidence for a claim nothing checked.\n" +
$"→ Re-point it with {envVar}, or regenerate the anchor. If the anchor is genuinely " +
"retired, DELETE THE CHECK — never leave one aimed at nothing. (rivers/01.)");
long expected = (long)mapSize * mapSize * 4;
long actual = new System.IO.FileInfo(dumpPath).Length;
if (actual != expected)
{
int anchorSize = (int)System.Math.Round(System.Math.Sqrt(actual / 4.0));
Godot.GD.PrintErr(
$" ⚠⚠ {checkId}: NOT CHECKED — the anchor exists but was captured at {anchorSize}, " +
$"and this run is at {mapSize} ({actual:N0} bytes, expected {expected:N0}). This is a size " +
$"mismatch, NOT a missing file: the batch is simply not proven against it at this size. " +
$"Run at {anchorSize} to check it. The oracle records INCONCLUSIVE, which is a FAIL — never a pass.");
return null;
}
return HeightField.Load(dumpPath, mapSize);
}
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
public static string ToMarkdownTable(IEnumerable<Check> checks)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("| | Check | Result | Evidence |");
sb.AppendLine("|---|---|---|---|");
foreach (Check c in checks)
sb.AppendLine($"| `{c.Id}` | {c.Name} | **{(c.Passed ? "PASS" : "FAIL")}** | {c.Detail} |");
return sb.ToString();
}
}
}