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
This commit is contained in:
Stewart Howe 2026-08-23 04:45:10 -04:00
parent 89e0f85c9f
commit 03fe75b378
20 changed files with 856 additions and 314 deletions

View file

@ -26,6 +26,7 @@ namespace IslaApocalypse.Core
/// ISLA_CONFIG_PATH the generation config file default: user://config.json
/// ISLA_BLUEPRINT_PATH the blueprint read/written default: user://blueprints
/// ISLA_OUTPUT_DIR generation output (maps, batches) default: user://output
/// ISLA_CHAT the batch chat namespace default: the tool's authoring chat
///
/// ⚠ user:// RESOLUTION. Defaults sit under the project's own user data directory, which this
/// project pins away from the old prototype's — see project.godot's user:// isolation block.
@ -60,6 +61,61 @@ namespace IslaApocalypse.Core
/// <summary>Whether <see cref="Configure"/> has been called. Tooling should assert this before a run.</summary>
public static bool IsConfigured => _userDataDir != null;
// ═══ ⭐⭐ THE CHAT NAMESPACE (rivers/01) ═══════════════════════════════════════════════════
//
// ═══ WHY BATCHES ARE NAMESPACED BY CHAT ═══
//
// The batch prefix is the AUTHORING TASK NUMBER (see <see cref="BatchRoot"/>), and task
// numbers restart at 00 in every new build chat. So a flat batches/ directory COLLIDES the
// moment a second chat exists: chat 1's `02_pass1_port` and chat 2's `02_curve_continuous`
// are both "batch 02", and nothing in either name says which chat made it. Measured on the
// real pile at rivers/01: 25 batches, FOUR colliding prefixes (02, 03, 04, 06), 13 folders
// belonging to chat 1 and 12 to chat 2 — separable only by SLUG, never by number.
//
// > ### ⚠ The slug is WHO IS RUNNING, not who authored.
// > A tool carries its authoring chat as its default so that re-running it reproduces its own
// > batch in place. A different chat re-running it for its own purposes sets ISLA_CHAT and
// > writes under its own namespace — which is also what stops an acceptance run from
// > OVERWRITING THE VERY ANCHOR IT IS CHECKING AGAINST.
//
// ⚠ REQUIRED, exactly like <see cref="Configure"/>: with no slug set, <see cref="BatchRoot"/>
// throws rather than quietly writing to the un-namespaced root and re-creating the collision
// this exists to end.
public const string ChatVar = "ISLA_CHAT";
private static string _chatSlug;
/// <summary>
/// Set the chat namespace batches are written under. Called once at startup by every batch
/// tool, with its authoring chat as the fallback: <c>ConfigureChat(EnvStr(ChatVar, "chat2"))</c>.
/// </summary>
/// <param name="slug">
/// A short domain slug — `chat1`, `chat2`, `rivers`. ⚠ It becomes a single path SEGMENT, so
/// separators are refused rather than silently creating a nested tree nobody asked for.
/// </param>
public static void ConfigureChat(string slug)
{
if (string.IsNullOrWhiteSpace(slug))
throw new ArgumentException("A chat slug is required — batches are namespaced by chat.", nameof(slug));
string t = slug.Trim();
if (t.IndexOf('/') >= 0 || t.IndexOf('\\') >= 0 || t.IndexOf(Path.DirectorySeparatorChar) >= 0
|| t == "." || t == "..")
throw new ArgumentException(
$"Chat slug '{t}' is not a single path segment. The slug is ONE folder under batches/ — " +
"pass \"rivers\", not \"a/b\" or \"..\".", nameof(slug));
_chatSlug = t;
}
/// <summary>The chat namespace. ⚠ Throws if <see cref="ConfigureChat"/> has not been called.</summary>
public static string ChatSlug => _chatSlug ?? throw new InvalidOperationException(
"CHAT SLUG NOT SET. Batches are namespaced by chat (batches/<chat>/NN_slug/); a tool must call " +
"ToolingPaths.ConfigureChat(...) before composing a batch path. Writing to the un-namespaced root " +
$"is what collided task numbers across chats in the first place. (Override with {ChatVar}.) — rivers/01.");
/// <summary>Whether <see cref="ConfigureChat"/> has been called.</summary>
public static bool IsChatConfigured => _chatSlug != null;
/// <summary>The generation config file. Override: ISLA_CONFIG_PATH.</summary>
public static string ConfigPath =>
Override(ConfigPathVar) ?? Path.Combine(UserDataDir, "config.json");
@ -78,6 +134,13 @@ namespace IslaApocalypse.Core
/// INDEX.md and a persistent scratch/. A/B comparisons are browsed by a human, and a flat
/// directory of same-named PNGs is not browsable.
///
/// ⚠⚠ THIS IS THE ROOT, NOT A BATCH, AND THE DISTINCTION IS LOAD-BEARING. Batch WRITES go
/// through <see cref="BatchRoot"/>, which inserts the <see cref="ChatSlug"/> segment. Anchor
/// READS compose against THIS, so an anchor's source string must carry its own explicit
/// `chatN/` prefix (e.g. `"chat1/02_pass1_port"`). Changing only `BatchRoot` would namespace
/// every write and silently orphan every historical read — the exact trap rivers/01 had to
/// walk through, and why <c>ShapingOracle.LoadAnchor</c> now throws on a missing anchor.
///
/// ⚠ PROTECTED FROM DELETION. → <see cref="FileSafety"/>.
/// </summary>
public static string BatchesRoot => Path.Combine(OutputDir, "batches");
@ -90,7 +153,7 @@ namespace IslaApocalypse.Core
public static string BatchScratch(string batchDir) => Path.Combine(batchDir, "scratch");
/// <summary>
/// ⭐ A BATCH ROOT: <c>batches/&lt;task&gt;_&lt;descriptor&gt;/</c>.
/// ⭐ A BATCH ROOT: <c>batches/&lt;chat&gt;/&lt;task&gt;_&lt;descriptor&gt;/</c>.
///
/// ═══ ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER. ═══
///
@ -107,6 +170,14 @@ namespace IslaApocalypse.Core
///
/// The descriptor must NOT carry its own numeric prefix; that is the mistake this method
/// exists to prevent, so it is refused rather than silently accepted.
///
/// ═══ ⭐ THE &lt;chat&gt; SEGMENT (rivers/01) ═══
///
/// Prepended from <see cref="ChatSlug"/>, because the task-number prefix restarts at 00 in
/// every chat — see the note on <see cref="ConfigureChat"/>. It is a SEPARATE segment and is
/// never folded into the descriptor: the prefix guard below fires on a descriptor starting
/// with digits, so passing `"chat2/12_drainage"` as a descriptor would be a different kind
/// of wrong.
/// </summary>
public static string BatchRoot(int taskNumber, string descriptor)
{
@ -127,7 +198,7 @@ namespace IslaApocalypse.Core
$"taskNumber and the descriptor WITHOUT one (e.g. \"review\", not \"04_review\") — " +
"the prefix is composed here so it cannot drift.", nameof(descriptor));
return Path.Combine(BatchesRoot, $"{taskNumber:D2}_{d}");
return Path.Combine(BatchesRoot, ChatSlug, $"{taskNumber:D2}_{d}");
}
/// <summary>
@ -137,6 +208,13 @@ namespace IslaApocalypse.Core
public static string BatchDir(int taskNumber, string descriptor, long seed, string variant)
=> Path.Combine(BatchRoot(taskNumber, descriptor), $"{seed}_{variant}");
/// <summary>
/// Resolve a HISTORICAL batch by its namespaced name, e.g. <c>"chat1/02_pass1_port"</c> — the
/// form every `ISLA_*_SOURCE` anchor default takes since rivers/01. Kept beside
/// <see cref="BatchRoot"/> so a READ and a WRITE are visibly two different operations.
/// </summary>
public static string BatchSource(string namespacedName) => Path.Combine(BatchesRoot, namespacedName);
private static string Override(string variable)
{
string v = Environment.GetEnvironmentVariable(variable);
@ -149,7 +227,8 @@ namespace IslaApocalypse.Core
$"config : {ConfigPath}{Marker(ConfigPathVar)}\n" +
$"blueprints: {BlueprintPath}{Marker(BlueprintPathVar)}\n" +
$"output : {OutputDir}{Marker(OutputDirVar)}\n" +
$"batches : {BatchesRoot}";
$"batches : {BatchesRoot}\n" +
$"chat : {(IsChatConfigured ? ChatSlug : " NOT SET")} → writes land under batches/{(IsChatConfigured ? ChatSlug : "<chat>")}/NN_slug/";
private static string Marker(string variable) => Override(variable) != null ? $" [{variable}]" : "";
}

View file

@ -343,9 +343,15 @@ Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises.
### 4. Batch layout
```
batches/<task>_<descriptor>/<seed>_<variant>/
batches/<task>_<descriptor>/INDEX.md
batches/<task>_<descriptor>/scratch/ ← persistent; never cleaned
batches/<chat>/<task>_<descriptor>/<seed>_<variant>/
batches/<chat>/<task>_<descriptor>/INDEX.md
batches/<chat>/<task>_<descriptor>/scratch/ ← persistent; never cleaned
<chat> is required (rivers/01): task numbers restart per chat, so a flat batches/
collided across chat1 and chat2. Set by ToolingPaths.ConfigureChat(), overridable
with ISLA_CHAT. Historical anchor READS carry the prefix in their own source string
(e.g. "chat1/02_pass1_port"), because they compose against BatchesRoot, not BatchRoot.
→ Tools/batches/README.md
```
> ### ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER.

View file

@ -38,7 +38,6 @@ namespace IslaApocalypse.Tools
/// ISLA_SPECK_FRAC the speck-revert threshold, fraction of map area (default 2.5e-7 ≈ 4 cells at 4096)
/// ISLA_PROBE=1 · ISLA_PROBE_FREQS · ISLA_PROBE_AMPS the probe sweep
/// ISLA_FRAG_BITES=1 bites-only noise ([0,1]) instead of zero-mean ([-1,1])
/// ISLA_SKIP_8K=1 (no 8192 check this batch — the 08 dump at 4096 is the baseline)
/// </summary>
public partial class CoastalFragmentTool : Node
{
@ -86,6 +85,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 9);
string descr = EnvStr("ISLA_BATCH", "coastal_fragment");
@ -101,9 +104,7 @@ namespace IslaApocalypse.Tools
float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs);
float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore");
string t08Level = EnvStr("ISLA_T08_LEVEL", "stretch_3"); // the 08 rung with stretch 2
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -132,16 +133,26 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, float amp, float fq, float st, bool revert)
{
return new TerrainGenConfig
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT:
// it was authored and judged before the shape family existed, and its regression checks
// hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the
// bare defaults family-ON, so without this pin every config here would silently acquire
// stretch + fragmentation and every anchor check would fail for a configuration reason.
// → TerrainGenConfig.WithFamilyOff().
var c = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(),
RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = speckFrac,
SouthStretch = st,
FragmentAmp = amp, FragmentFreqPerMapWidth = fq, FragmentBitesOnly = bitesOnly,
};
RegionLabeling = true,
}.WithFamilyOff();
// …then this tool's swept axes, AFTER the pin. (These were already explicit before
// rivers/01; the pin makes the tool's independence from the defaults total rather than
// field-by-field, so a future default can never leak in through a field nobody listed.)
c.SpeckRevert = revert; c.MinLandComponentFrac = speckFrac;
c.SouthStretch = st;
c.FragmentAmp = amp; c.FragmentFreqPerMapWidth = fq; c.FragmentBitesOnly = bitesOnly;
return c;
}
// ═══ PROBE ═══
@ -186,21 +197,17 @@ namespace IslaApocalypse.Tools
var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{seeds[0]}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)",
Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
// ⭐ a8 — frag OFF at the fixed stretch, revert OFF == the task-08 stretch-2 field (its dump at the plate size).
foreach (int seed in seeds)
{
string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_{t08Level}", "height.f32");
if (File.Exists(t08Dump) && mapSize == 4096)
{
var c8 = Cfg(mapSize, seed, "t08", 0f, freq, stretch, false);
Pass2Result q8 = Shaping.Shape(Topography.Generate(c8), c8);
hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch {stretch:G3}, revert OFF == task-08 {t08Level} dump (the baseline) [{seed}]", q8.Height, HeightField.Load(t08Dump, mapSize), mapSize, t08Dump));
}
else GD.Print($" a8 [{seed}]: ⚠ skipped — {(mapSize != 4096 ? "map size is not 4096" : $"no 08 dump at {t08Dump}")}");
}
// ⚑ RETIRED at rivers/01 — a8, the frag-OFF baseline == `08_southern_stretch_explore`.
// An EXPLORATION ladder, and off-shape: the 08 batch's dump is at stretch 3, the locked
// shape is stretch 2. Nothing should be pinned to a rung of a ladder that was climbed to
// find a value, then superseded by the value it found.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
foreach (var c in hard) GD.Print(" " + c);
}
@ -311,7 +318,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s));
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -324,11 +331,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -47,7 +47,7 @@ namespace IslaApocalypse.Tools
/// ISLA_SHOWPIECE_SIZE larger confirmation profile (default 8192)
/// ISLA_SHOWPIECE "0" to skip the big render
/// ISLA_VARIANTS "0" to skip the per-seed variants (calibration-only probe)
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "chat1/02_pass1_port")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
/// </summary>
public partial class CurveBaselineTool : Node
@ -86,6 +86,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 1);
string descr = EnvStr("ISLA_BATCH", "curve_baseline");
@ -94,7 +98,7 @@ namespace IslaApocalypse.Tools
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
bool variants = EnvStr("ISLA_VARIANTS", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
// ⚠ Composed by BatchRoot, never free-form — it refuses a descriptor carrying its own
@ -131,7 +135,7 @@ namespace IslaApocalypse.Tools
foreach (int seed in seeds)
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed };
var cfg = TerrainGenConfig.CalibrationPool(mapSize, seed);
Pass1Result p1 = Topography.Generate(cfg);
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
@ -289,8 +293,16 @@ namespace IslaApocalypse.Tools
// ---- configs --------------------------------------------------------
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored
/// and judged before the shape family existed, on the family-off distribution the knots are
/// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what
/// keeps this tool measuring the thing it was written to measure.
/// → <see cref="TerrainGenConfig.WithFamilyOff"/>.
/// </remarks>
private static TerrainGenConfig OffConfig(int mapSize, int seed) =>
new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "curve_off", Curve = false, ShelfDetail = false };
new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "curve_off", Curve = false, ShelfDetail = false }
.WithFamilyOff();
private static TerrainGenConfig OnConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a) =>
new TerrainGenConfig
@ -301,7 +313,7 @@ namespace IslaApocalypse.Tools
// bit-for-bit. chat2/02 moved the config DEFAULT to Continuous for its exploration;
// a control batch must not move with a default. (chat2/02.)
CurveMode = CurveModeKind.Staircase,
};
}.WithFamilyOff(); // ⭐ rivers/01 — see OffConfig: the staircase control is pre-family too
/// <summary>
/// Fraction of land below the CostaRica palette's third stop (0.310 raw). A blunt

View file

@ -48,8 +48,7 @@ namespace IslaApocalypse.Tools
/// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below)
/// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192)
/// ISLA_SHOWPIECE "0" to skip it
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
/// ISLA_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline")
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "chat1/02_pass1_port")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
/// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30)
/// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4)
@ -91,6 +90,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 2);
string descr = EnvStr("ISLA_BATCH", "curve_continuous");
@ -98,8 +101,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form
@ -128,7 +130,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed));
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms");
@ -187,15 +189,20 @@ namespace IslaApocalypse.Tools
{
Pass1Result pp1 = pass1[primary];
// (a1) curve off == Phase 1's own dump.
// (a1) curve off == Phase 1's own dump. ⭐ KEPT at rivers/01: the family-off pass-1 guard,
// the last link between today's generator and the Phase-1 port. The config is pinned
// family-off so it still means what it says. ⚠ A missing dump now THROWS.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump));
// (a2) staircase == task 01's own dump — the control is the control.
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump",
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
// ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control.
// The staircase curve is SUPERSEDED by the continuous grade (→ D-062). A control that
// reproduces a curve nothing ships is scaffolding, and holding it green cost a
// 4-variant batch run to prove a mode no design doc describes any more.
// The dump is NOT deleted (file-safety; it is regenerable and it is the record of what
// was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can
// pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4.
// (b) classify == raw, every seed × every variant.
long bFail = 0;
@ -340,7 +347,11 @@ namespace IslaApocalypse.Tools
LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f),
ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f),
SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f),
};
}
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/02 is CURVE development, measured on
// the family-off distribution the knots are percentiles of; the re-baseline flipped the bare
// defaults family-ON. → TerrainGenConfig.WithFamilyOff().
.WithFamilyOff();
// ---- output -----------------------------------------------------------

View file

@ -54,6 +54,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 12);
string descr = EnvStr("ISLA_BATCH", "drainage_analysis");
@ -62,7 +66,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipT11 = EnvStr("ISLA_SKIP_T11_CHECK", "0") == "1";
string t11Source = EnvStr("ISLA_T11_SOURCE", "11_erosion");
string t11Source = EnvStr("ISLA_T11_SOURCE", "chat2/11_erosion");
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -77,7 +81,11 @@ namespace IslaApocalypse.Tools
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
GD.Print($"seeds : {string.Join(", ", seeds)}");
GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON (faithful tune) — the task-11 erosion_on field");
// ⚠ rivers/01: the shape AND erosion now come from the defaults, so both are asserted before
// anything generates. A drift here would silently re-baseline every river measurement.
TerrainShapeV1.Assert("DrainageTool");
TerrainShapeV1.AssertErosionDefaultOn("DrainageTool");
GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON by default (faithful tune) — the task-11 erosion_on field");
GD.Print($"params : endorheic depth ≥ {dp.EndorheicMinDepthM} m, area ≥ {dp.EndorheicMinAreaPx}, inflow ≥ {dp.EndorheicMinInflowPx}, max {dp.EndorheicMaxCount} · trunks {dp.TrunkCount} sep {dp.MinOutletSeparationPx} px · giants {dp.GiantCount} · stem ≥ {dp.StemMinAccPx} · tributary ≥ {dp.TributaryMinAccPx} (max {dp.TributaryMaxPerTrunk}) · exit grade {dp.ExitGradeMin} m/px over {dp.ExitWindowPx} px");
GD.Print($"batch : {batchRoot}");
GD.Print("==================================================================");
@ -88,14 +96,15 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed)
{
// ⭐ rivers/01: THE SHAPE AND EROSION COME FROM THE BARE DEFAULTS. `TerrainShapeV1.Apply(c)`
// and `c.Erosion = true` used to sit here; both are now what `new TerrainGenConfig()`
// carries. Only the CURVE (measured this run) is set.
var c = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = "drainage",
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
};
TerrainShapeV1.Apply(c);
c.Erosion = true; // the faithful tune — the defaults
return c;
}
@ -114,16 +123,17 @@ namespace IslaApocalypse.Tools
Pass2Result p2 = ero.Shaped;
GD.Print($" terrain ready ({p1.ElapsedMs} ms pass 1, erosion {ero.Ms / 1000.0:F1} s)");
// ⭐ a11 — THE EROSION ACCEPTANCE ANCHOR (rivers/01 keeps this one). Since the re-baseline
// the whole chain — shape AND erosion — comes from the bare defaults, so this is the
// standing proof that the terrain every river measurement rests on has not moved.
// ⚠ A missing dump now THROWS (ShapingOracle.LoadAnchor) instead of skipping silently.
if (!skipT11)
{
string dump = Path.Combine(ToolingPaths.BatchesRoot, t11Source, $"{seed}_erosion_on", "height.f32");
if (File.Exists(dump) && mapSize == 8192)
{
var a11 = ShapingOracle.DumpRegression("a11", $"the eroded render field == the task-11 erosion_on dump (the terrain the developer saw) [{seed}]", p2.Height, HeightField.Load(dump, mapSize), mapSize, dump);
var a11 = ShapingOracle.DumpRegression("a11", $"the eroded render field from the BARE DEFAULTS == the task-11 erosion_on dump (the terrain the developer saw) [{seed}]",
p2.Height, ShapingOracle.LoadAnchor("a11", "ISLA_T11_SOURCE", dump, mapSize), mapSize, dump);
hard.Add(a11); GD.Print(" " + a11);
}
else GD.Print($" a11 [{seed}]: ⚠ skipped — {(mapSize != 8192 ? "map size is not the 11 batch's 8192" : $"no dump at {dump}")}");
}
// ⭐ THE OCEAN IDENTITY — from the region layer, on the CLASSIFY field.
bool[] isOcean = RegionLabeling.OceanMask(p2.HeightClassify, mapSize, sea, out long oceanCells, out long enclosed);
@ -284,7 +294,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s));
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -297,11 +307,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -7,33 +7,6 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ THE LOCKED SHAPE — `terrain-shape-v1` (chat2/10 gallery-confirmed): the continuous curve + the
/// frag_4 organic islands. Every later pass (erosion, rivers, …) starts from exactly these values,
/// pinned here once so no tool re-types them.
/// </summary>
public static class TerrainShapeV1
{
public const float FragmentAmp = 0.5f, FragmentFreq = 12f, BandCentre = 0.66f, BandHalfWidth = 0.18f;
public const bool BitesOnly = false;
public const float Stretch = 2f, BandStart = 0.70f, BandFeather = 0.05f;
public const bool StretchSinker = true;
public const float SpeckFrac = 2.5e-7f;
/// <summary>Apply the locked shape to a config (curve settings are the caller's — they come from the calibration).</summary>
public static void Apply(TerrainGenConfig c)
{
c.CoastShelf = false; c.Offshore = new OffshoreSettings();
c.RegionLabeling = true; c.SpeckRevert = true; c.MinLandComponentFrac = SpeckFrac;
c.SouthStretch = Stretch; c.SouthBandStartFrac = BandStart; c.SouthBandFeatherFrac = BandFeather; c.StretchSinker = StretchSinker;
c.FragmentAmp = FragmentAmp; c.FragmentFreqPerMapWidth = FragmentFreq;
c.FragmentBandCentre = BandCentre; c.FragmentBandHalfWidth = BandHalfWidth; c.FragmentBitesOnly = BitesOnly;
}
public static string Describe() =>
$"terrain-shape-v1: frag amp {FragmentAmp} freq {FragmentFreq} window {BandCentre}±{BandHalfWidth} · stretch {Stretch} (band {BandStart}/{BandFeather}, sinker stretched) · speck revert {SpeckFrac:G2} · offshore OFF · shelf OFF · labeling ON";
}
/// <summary>
/// ⭐ THE EROSION BATCH (chat2/11) — the faithful droplet erosion on the locked shape, judged across
/// seeds, erosion OFF vs ON. 4 seeds from the task-10 gallery × {off, on} = 8 fields at showpiece
@ -85,6 +58,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 11);
string descr = EnvStr("ISLA_BATCH", "erosion");
@ -93,7 +70,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipTag = EnvStr("ISLA_SKIP_TAG_CHECK", "0") == "1";
string t10Source = EnvStr("ISLA_T10_SOURCE", "10_frag4_seed_gallery");
string t10Source = EnvStr("ISLA_T10_SOURCE", "chat2/10_frag4_seed_gallery");
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -107,6 +84,7 @@ namespace IslaApocalypse.Tools
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
GD.Print($"seeds : {string.Join(", ", seeds)}");
TerrainShapeV1.Assert("ErosionTool"); // ⚠ rivers/01: refuse to render if the defaults drifted off the locked shape
GD.Print($"shape : {TerrainShapeV1.Describe()}");
GD.Print($"batch : {batchRoot}");
GD.Print("==================================================================");
@ -117,13 +95,17 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, bool erosion)
{
// ⭐ rivers/01: THE SHAPE COMES FROM THE BARE DEFAULTS. `TerrainShapeV1.Apply(c)` used to
// sit here; the locked shape is now what `new TerrainGenConfig()` produces, so stamping
// a preset on top would MASK a default drift instead of catching it. Only the CURVE
// (measured this run) and the per-variant erosion flag are set.
// → TerrainShapeV1.Assert(), called before any generation below.
var c = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
};
TerrainShapeV1.Apply(c);
c.Erosion = erosion;
c.ErosionDropletCount = EnvInt("ISLA_ERO_COUNT", c.ErosionDropletCount);
c.ErosionDropletLifetime = EnvInt("ISLA_ERO_LIFETIME", c.ErosionDropletLifetime);
@ -156,12 +138,15 @@ namespace IslaApocalypse.Tools
var cOff = Cfg(mapSize, seed, "erosion_off", false);
Pass1Result p1Off = Topography.Generate(cOff);
Pass2Result p2Off = Shaping.Shape(p1Off, cOff);
// ⭐ a10 — THE SHAPE ACCEPTANCE ANCHOR (rivers/01 keeps this one). Since the re-baseline
// `p2Off` is generated from the BARE DEFAULTS, so this check is now the standing proof
// that the defaults still reproduce `terrain-shape-v1`.
// ⚠ A missing dump now THROWS (ShapingOracle.LoadAnchor) instead of skipping silently.
if (!skipTag)
{
string dump = Path.Combine(ToolingPaths.BatchesRoot, t10Source, $"{seed}", "height.f32");
if (File.Exists(dump) && mapSize == 8192)
hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF == terrain-shape-v1 (the task-10 gallery dump) [{seed}]", p2Off.Height, HeightField.Load(dump, mapSize), mapSize, dump));
else GD.Print($" a10 [{seed}]: ⚠ skipped — {(mapSize != 8192 ? "map size is not the gallery's 8192" : $"no gallery dump at {dump}")}");
hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF from the BARE DEFAULTS == terrain-shape-v1 (the task-10 gallery dump) [{seed}]",
p2Off.Height, ShapingOracle.LoadAnchor("a10", "ISLA_T10_SOURCE", dump, mapSize), mapSize, dump));
}
Image reliefOff = ReliefRenderer.Render(p2Off.Height, mapSize, look);
Image shadeOff = ShadeRenderer.Render(p2Off.Height, mapSize, sea, ShadeZ, look.LightAzimuth, look.LightAltitude);
@ -281,7 +266,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s));
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -294,11 +279,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -22,7 +22,7 @@ namespace IslaApocalypse.Tools
/// ISLA_MAPSIZE gallery size (default 8192)
/// ISLA_CALIB_SIZE curve calibration size (default 2048)
/// ISLA_SEEDS the gallery seeds (default: the 2 anchors + 6 fresh below)
/// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 bit-identity check against the 09 frag_4 dumps
/// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 interior-locked invariant check
/// </summary>
public partial class FragGalleryTool : Node
{
@ -35,17 +35,22 @@ namespace IslaApocalypse.Tools
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
// ═══ THE FROZEN frag_4 SETTING — every value pinned explicitly (chat2/09 batch, level 4) ═══
private const float FrozenFragmentAmp = 0.5f;
private const float FrozenFragmentFreq = 12f;
private const float FrozenBandCentre = 0.66f;
private const float FrozenBandHalfWidth = 0.18f;
private const bool FrozenBitesOnly = false;
private const float FrozenStretch = 2f;
private const float FrozenBandStart = 0.70f;
private const float FrozenBandFeather = 0.05f;
private const bool FrozenStretchSinker = true;
private const float FrozenSpeckFrac = 2.5e-7f; // 09's low speck revert (≈ 4 cells at 4096, ≈ 17 at 8192)
// ═══ THE FROZEN frag_4 SETTING (chat2/09 batch, level 4) ═══
//
// ⭐ rivers/01: these were the ONLY home of the locked values. They now ALIAS
// `TerrainShapeV1`, which is itself the assertion target for `TerrainGenConfig`'s defaults —
// so the chain is: bare defaults → asserted against TerrainShapeV1 → printed here. One value,
// one place, and a throw if the generator ever stops agreeing with it.
private const float FrozenFragmentAmp = TerrainShapeV1.FragmentAmp;
private const float FrozenFragmentFreq = TerrainShapeV1.FragmentFreq;
private const float FrozenBandCentre = TerrainShapeV1.BandCentre;
private const float FrozenBandHalfWidth = TerrainShapeV1.BandHalfWidth;
private const bool FrozenBitesOnly = TerrainShapeV1.BitesOnly;
private const float FrozenStretch = TerrainShapeV1.Stretch;
private const float FrozenBandStart = TerrainShapeV1.BandStart;
private const float FrozenBandFeather = TerrainShapeV1.BandFeather;
private const bool FrozenStretchSinker = TerrainShapeV1.StretchSinker;
private const float FrozenSpeckFrac = TerrainShapeV1.SpeckFrac; // ≈ 4 cells at 4096, ≈ 17 at 8192
private const int DefaultMapSize = 8192;
private const int DefaultCalibSize = 2048;
@ -79,6 +84,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 10);
string descr = EnvStr("ISLA_BATCH", "frag4_seed_gallery");
@ -87,9 +96,7 @@ namespace IslaApocalypse.Tools
int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipAnchor = EnvStr("ISLA_SKIP_ANCHOR_CHECK", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore");
string t09Source = EnvStr("ISLA_T09_SOURCE", "09_coastal_fragment");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
var seeds = new List<int>(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); }
var anchorSet = new HashSet<int>(AnchorSeeds);
@ -118,17 +125,40 @@ namespace IslaApocalypse.Tools
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
GD.Print($" {knots}");
TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true) => new TerrainGenConfig
// ⭐⭐ rivers/01: THE FROZEN SETTING IS NOW THE BARE DEFAULT.
//
// Every `Frozen*` constant above was re-homed into `TerrainGenConfig`'s defaults by the
// re-baseline, so this helper no longer SETS the shape — it only ABLATES it, for the
// family-off halves of the regression checks. That is the whole point: this batch is the
// `terrain-shape-v1` acceptance, and it can only prove the defaults reproduce the locked
// shape if it reads them instead of re-stating them.
//
// ⚠ The `frag` / `revert` / `stretch` flags are ABLATIONS ONLY. All three true = the bare
// default = the locked shape; `TerrainShapeV1.Assert` below is what keeps that claim
// honest if a default ever drifts.
TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true)
{
var c = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(),
RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = FrozenSpeckFrac,
SouthStretch = stretch ? FrozenStretch : 0f, SouthBandStartFrac = FrozenBandStart, SouthBandFeatherFrac = FrozenBandFeather, StretchSinker = FrozenStretchSinker,
FragmentAmp = frag ? FrozenFragmentAmp : 0f, FragmentFreqPerMapWidth = FrozenFragmentFreq,
FragmentBandCentre = FrozenBandCentre, FragmentBandHalfWidth = FrozenBandHalfWidth, FragmentBitesOnly = FrozenBitesOnly,
};
if (!stretch) c.SouthStretch = 0f;
if (!frag) c.FragmentAmp = 0f;
if (!revert) c.SpeckRevert = false;
// This batch is render-only shape: erosion is a later pass and never ran here.
c.Erosion = false;
return c;
}
// ⚠⚠ THE DEFAULT-DRIFT GUARD (rivers/01). The gallery above stopped STATING the locked shape
// and started READING it. If a default ever moves, every render silently moves with it and
// the batch still "passes" — so the claim is asserted, loudly, before a pixel is drawn.
// The `Frozen*` constants below are unchanged in value; their ROLE flipped from source to
// assertion target. → Tools/Scripts/TerrainShapeV1.cs
TerrainShapeV1.Assert("FragGallery");
GD.Print($" defaults : ✅ {TerrainShapeV1.Describe()}");
// ═══ 1. THE ORACLE — no code change, same setting ═══
GD.Print($"\n--- 1. ORACLE: the setting is the 09 frag_4 setting, and nothing upstream moved ---");
@ -137,25 +167,32 @@ namespace IslaApocalypse.Tools
var offCfg = Frozen(calibSize, AnchorSeeds[0], "off", frag: false, revert: false, stretch: false);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{AnchorSeeds[0]}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)",
Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
// ⚑ RETIRED at rivers/01 — a8 (`08_southern_stretch_explore`) and a9 (`09_coastal_fragment`).
// Both are EXPLORATION ladders this gallery was built to CONCLUDE: chat2/09 climbed the
// fragmentation ladder, chat2/10 froze rung 4 across 8 seeds, and the developer tagged the
// result `terrain-shape-v1`. Since rivers/01 that frozen setting IS the bare default, and
// `TerrainShapeV1.Assert` + a10 assert it directly — asserting it a third time through the
// rungs it was chosen from is circular, and 08's dump is at stretch 3 (off-shape) besides.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
if (!skipAnchor)
{
foreach (int seed in AnchorSeeds)
{
// ⭐ a9 — the frozen setting at the 09 batch's size reproduces the 09 frag_4 field bit for bit.
string t09Dump = Path.Combine(ToolingPaths.BatchesRoot, t09Source, $"{seed}_frag_4", "height.f32");
// ⭐ r KEPT and SELF-ANCHORED — the interior-locked invariant is coastal fragmentation's
// load-bearing claim (it must touch the coastal window and NOTHING else), and it needs
// no external dump: both fields are generated here, from the bare defaults and from the
// same defaults with frag ablated off. That is what let 08 and 09 retire intact.
var c9 = Frozen(AnchorCheckSize, seed, "frag_4");
Pass1Result q9p = Topography.Generate(c9);
Pass2Result q9 = Shaping.Shape(q9p, c9);
hard.Add(ShapingOracle.DumpRegression("a9", $"frozen frag_4 at {AnchorCheckSize} == task-09 frag_4 dump [{seed}] (no code change, same setting)", q9.Height, HeightField.Load(t09Dump, AnchorCheckSize), AnchorCheckSize, t09Dump));
// a8 — the stretch-2, frag-off baseline still equals the 08 field, and the interior is still locked against it.
string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_stretch_3", "height.f32");
var c8 = Frozen(AnchorCheckSize, seed, "t08", frag: false, revert: false);
Pass1Result q8p = Topography.Generate(c8);
hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch 2 at {AnchorCheckSize} == task-08 stretch_3 dump [{seed}]", Shaping.Shape(q8p, c8).Height, HeightField.Load(t08Dump, AnchorCheckSize), AnchorCheckSize, t08Dump));
var r = ShapingOracle.InteriorLocked(q8p, q9p, FrozenBandCentre, FrozenBandHalfWidth); r.Name += $" [{seed}, {AnchorCheckSize}]"; hard.Add(r);
}
// determinism at the check size
@ -203,7 +240,7 @@ namespace IslaApocalypse.Tools
foreach (var c in perSeed) if (!c.Passed) GD.PrintErr(" " + c);
WriteTable(batchRoot, mapSize, rows, big, speckCells);
WriteIndex(batchRoot, mapSize, calibSize, rows, big, speckCells, hard, perSeed, allOk);
WriteIndex(batchRoot, task, mapSize, calibSize, rows, big, speckCells, hard, perSeed, allOk);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
@ -262,7 +299,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s));
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -275,11 +312,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);
@ -356,11 +396,11 @@ namespace IslaApocalypse.Tools
WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
}
private static void WriteIndex(string batchRoot, int mapSize, int calibSize, List<Row> rows, long big, long speckCells,
private static void WriteIndex(string batchRoot, int task, int mapSize, int calibSize, List<Row> rows, long big, long speckCells,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perSeed, bool allOk)
{
var sb = new StringBuilder();
sb.AppendLine($"# Batch 10 — frag_4 seed gallery: does the look generalize? (render-only, {mapSize})");
sb.AppendLine($"# Batch {task:D2} — frag_4 seed gallery: does the look generalize? (render-only, {mapSize})");
sb.AppendLine();
sb.AppendLine("**A contact sheet, not a tune.** Every plate is the SAME setting — chat2/09's `frag_4`, frozen — across the two");
sb.AppendLine("seeds it was judged on (⭐ anchors) and six fresh seeds chosen before any render. The question: does a coherent");
@ -370,7 +410,13 @@ namespace IslaApocalypse.Tools
sb.AppendLine();
sb.AppendLine($"`FragmentAmp {FrozenFragmentAmp}` · `FragmentFreqPerMapWidth {FrozenFragmentFreq}` · window `{FrozenBandCentre} ± {FrozenBandHalfWidth}` · bites-only `{FrozenBitesOnly}` · " +
$"`SouthStretch {FrozenStretch}` (band `{FrozenBandStart}` / feather `{FrozenBandFeather}`, sinker stretched `{FrozenStretchSinker}`) · speck revert < {speckCells} cells (`{FrozenSpeckFrac:G2}` of the map) · " +
"offshore OFF · shelf OFF · region labeling ON · the tagged curve (calibrated on task 01's pool at " + calibSize + "). Pinned explicitly in `FragGalleryTool` — nothing is left to a default.");
"offshore OFF · shelf OFF · region labeling ON · the tagged curve (calibrated on task 01's pool at " + calibSize + ").");
sb.AppendLine();
sb.AppendLine("> ### ⭐ Since rivers/01, this setting IS the bare `TerrainGenConfig` default — it is not stated here, it is READ.");
sb.AppendLine("> That is what makes this batch the acceptance for the re-baseline rather than a restatement of it: if a default");
sb.AppendLine("> ever drifts, `TerrainShapeV1.Assert` refuses the run instead of rendering a gallery that would look right and");
sb.AppendLine("> mean nothing. The curve calibration pool is pinned FAMILY-OFF (`TerrainGenConfig.WithFamilyOff`), which is what");
sb.AppendLine("> keeps the knots — and therefore these renders — bit-identical across the flip.");
sb.AppendLine();
sb.AppendLine("## ⭐ The contact sheet");
sb.AppendLine();

View file

@ -44,7 +44,7 @@ namespace IslaApocalypse.Tools
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE
/// ISLA_PHASE1_SOURCE (default "02_pass1_port") · ISLA_T01_SOURCE (default "01_curve_baseline")
/// ISLA_PHASE1_SOURCE (default "chat1/02_pass1_port")
/// ISLA_SKIP_RAW
/// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35)
/// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5)
@ -77,6 +77,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 3);
string descr = EnvStr("ISLA_BATCH", "mountain_restore");
@ -84,8 +88,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f);
float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f);
@ -114,7 +117,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed));
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
}
@ -219,12 +222,17 @@ namespace IslaApocalypse.Tools
var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>();
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off).
// ⚠ A missing dump now THROWS instead of skipping silently.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
hard.Add(ShapingOracle.DumpRegression("a2", "staircase == task-01 curve_on .f32 dump",
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump));
// ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control.
// Superseded by the continuous grade (→ D-062) — see CurveContinuousTool for the full note.
// The dump is NOT deleted (file-safety; it is regenerable and it is the record of what
// was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can
// pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4.
long bFail = 0;
foreach (int seed in seeds)
@ -335,13 +343,20 @@ namespace IslaApocalypse.Tools
GetTree().Quit(hardOk ? 0 : 3);
}
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored
/// and judged before the shape family existed, on the family-off distribution the knots are
/// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what
/// keeps this tool measuring the thing it was written to measure.
/// → <see cref="TerrainGenConfig.WithFamilyOff"/>.
/// </remarks>
private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = 30f,
};
}.WithFamilyOff();
// ---- output ---------------------------------------------------------

View file

@ -57,8 +57,7 @@ namespace IslaApocalypse.Tools
/// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides)
/// ISLA_OFF_FREQ / ISLA_OFF_CORE / ISLA_OFF_SHARP / ISLA_OFF_CREST the shape (probe overrides)
/// ISLA_TABLE_ONLY=1 probe: diagnosis + count table only (no regressions, no plates)
/// ISLA_SKIP_8K=1 skip the 8192 regression against the 04 gallery dump (a4)
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches
/// ISLA_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port")
/// </summary>
public partial class OffshoreIslandsTool : Node
{
@ -77,7 +76,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192; // the 04 gallery's render size
/// <summary>The consistency targets the table is read against: "a couple north, a few south".</summary>
private const int TargetNorth = 2, TargetSouth = 3;
@ -112,6 +110,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 6);
string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune");
@ -121,12 +123,9 @@ namespace IslaApocalypse.Tools
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -185,14 +184,17 @@ namespace IslaApocalypse.Tools
var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff);
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump",
pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
pOff.Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
Pass2Result pRest = Shaping.Shape(p1, offCfg);
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)",
pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump));
// ⚑ RETIRED at rivers/01 — a3, `continuous_restored` == `03_mountain_restore`.
// A curve-development intermediate: it proved the chat2/03 climb restoration against
// chat2/02. Both are upstream of the locked shape, and `terrain-shape-v1` (a10) now
// asserts the whole chain end-to-end — subsuming it.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
// Shelf ON, islets OFF: land must be bit-identical (the shelf touches only sea).
var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
@ -205,22 +207,12 @@ namespace IslaApocalypse.Tools
// ⭐ a4 — offshore OFF at the 04 gallery's size == the terrain-curve-v1 tag's OWN output.
// The literal "offshore-off is bit-identical to terrain-curve-v1", at full size.
if (!skip8k)
{
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
if (File.Exists(t04Dump))
{
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …");
var gCfg = BaseConfig(GallerySize, plateSeed, knots, anchors, calibration, "off");
Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
hard.Add(a4);
GD.Print(" " + a4);
}
else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}");
}
else GD.Print(" a4: skipped (ISLA_SKIP_8K)");
// ⚑ RETIRED at rivers/01 — a4, offshore-OFF at 8192 == `04_seed_gallery` (`terrain-curve-v1`).
// The PRE-FAMILY committed curve. The locked shape is family-ON, so this dump is a
// baseline the generator is deliberately no longer on; `a10` replaced it as the 8192
// acceptance. (It also cost an 8192 generation on every run of this tool.)
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
}
// ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══
@ -402,7 +394,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF by default
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -416,11 +408,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);
@ -436,14 +431,21 @@ namespace IslaApocalypse.Tools
return (knots, cal, pass1);
}
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT
/// (chat2/0506): authored and judged before the shape family existed, and its regressions hold
/// pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the bare defaults
/// family-ON, so without <see cref="TerrainGenConfig.WithFamilyOff"/> every config here would
/// silently acquire stretch + fragmentation and the anchor checks would fail for a configuration
/// reason, not a regression. The variants re-enable offshore explicitly, after the pin.
/// </remarks>
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
ClimbCalibration cal, string label) => new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on
};
}.WithFamilyOff(); // ⭐ shelf / islets / speck / stretch / frag / erosion all OFF — pinned
// ---- output -----------------------------------------------------------

View file

@ -42,8 +42,7 @@ namespace IslaApocalypse.Tools
/// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands)
/// ISLA_THR_LOW / ISLA_THR_MID / ISLA_THR_HIGH thresholds, fraction of map area (probe overrides)
/// ISLA_TABLE_ONLY=1 probe: table only (no regressions, no plates)
/// ISLA_SKIP_8K=1 skip the 8192 regression (a4)
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches
/// ISLA_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port")
/// </summary>
public partial class RegionLabelingTool : Node
{
@ -57,7 +56,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192;
public override void _Ready()
{
@ -89,6 +87,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 7);
string descr = EnvStr("ISLA_BATCH", "region_labeling");
@ -98,13 +100,9 @@ namespace IslaApocalypse.Tools
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int secondEnv = EnvInt("ISLA_SECOND_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
string t06Source = EnvStr("ISLA_T06_SOURCE", "06_offshore_organic_tune");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
var levels = new List<Level>
{
@ -141,7 +139,14 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac)
{
var c = BaseConfig(size, seed, knots, anchors, calibration, label);
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT:
// it was authored and judged before the shape family existed, and its regression checks
// hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the
// bare defaults family-ON, so without this pin every config here would silently acquire
// stretch + fragmentation and every anchor check would fail for a configuration reason.
// → TerrainGenConfig.WithFamilyOff().
var c = BaseConfig(size, seed, knots, anchors, calibration, label).WithFamilyOff();
// …then this tool's own axes, AFTER the pin (the pin would otherwise clear them).
if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); }
c.RegionLabeling = true;
c.SpeckRevert = revertOn;
@ -159,26 +164,17 @@ namespace IslaApocalypse.Tools
var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff);
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF, revert OFF (labeling on) == Phase-1 .f32 dump",
pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
pOff.Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
Pass2Result pRest = Shaping.Shape(p1, offCfg);
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32");
float[,] t03 = HeightField.Load(t03Dump, calibSize);
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump",
pRest.Height, t03, calibSize, t03Dump));
// Informational: offshore OFF, revert ON — how many NATURAL speck cells the revert removes
// from the bare field. Allowed to differ (the revert may change terrain); reported, not asserted.
var revCfg = Cfg(calibSize, plateSeed, "off_revert", offshoreOn: false, revertOn: true, mid.Frac);
Pass1Result p1Rev = Topography.Generate(revCfg);
Pass2Result pRev = Shaping.Shape(p1Rev, revCfg);
var info = ShapingOracle.DumpRegression("a3r", "(informational) offshore OFF, revert ON at threshold_mid vs task-03 dump — the natural specks removed", pRev.Height, t03, calibSize, t03Dump);
info.Detail = (info.Passed ? "no natural speck below the threshold on this seed — " : "") + info.Detail +
$" · reverted {p1Rev.RegionLedger.RevertedComponents} natural components / {p1Rev.RegionLedger.RevertedCells:N0} cells";
info.Passed = true;
hard.Add(info);
// ⚑ RETIRED at rivers/01 — a3 and a3r, both against `03_mountain_restore`.
// A curve-development intermediate, subsumed by the `terrain-shape-v1` acceptance (a10).
// a3r was informational only, and its subject — how many natural specks the revert takes —
// is now reported by the region ledger every run, with the revert ON by default.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
Pass1Result p1Shelf = Topography.Generate(shelfCfg);
@ -188,34 +184,13 @@ namespace IslaApocalypse.Tools
hard.Add(ShapingOracle.CentreIsLand(p1));
foreach (var c in hard) GD.Print(" " + c);
if (!skip8k)
{
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
if (File.Exists(t04Dump))
{
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …");
var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac);
Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF, revert OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
hard.Add(a4); GD.Print(" " + a4);
}
else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}");
}
else GD.Print(" a4: skipped (ISLA_SKIP_8K)");
// ⭐ a6 — labeling ON, revert OFF, on the chat2/06 preset: bit-identical to the 06 batch's
// render field. Labeling is pure analysis; only the revert may change terrain.
string t06Dump = Path.Combine(ToolingPaths.BatchesRoot, t06Source, $"{plateSeed}_density_mid", "height.f32");
if (File.Exists(t06Dump) && mapSize == 4096)
{
var c6 = Cfg(mapSize, plateSeed, "density_mid", offshoreOn: true, revertOn: false, mid.Frac);
Pass2Result p6 = Shaping.Shape(Topography.Generate(c6), c6);
var a6 = ShapingOracle.DumpRegression("a6", "offshore density_mid ON, labeling ON, revert OFF == task-06 .f32 dump (labeling is pure analysis)",
p6.Height, HeightField.Load(t06Dump, mapSize), mapSize, t06Dump);
hard.Add(a6); GD.Print(" " + a6);
}
else GD.Print($" a6: ⚠ skipped — {(mapSize != 4096 ? "map size is not the 06 batch's 4096" : $"no 06 dump at {t06Dump}")}");
// ⚑ RETIRED at rivers/01 — a4 (`04_seed_gallery`) and a6 (`06_offshore_organic_tune`).
// a4 held the PRE-FAMILY committed curve; the locked shape is family-ON, and a10 is the
// 8192 acceptance now. a6 held the ORGANIC ISLET layer — the DROPPED mechanism (→ D-063):
// islands are organic-only, made by the stretch + fragmentation and identified here, never
// placed. An oracle pinning islet output is an oracle defending a design that was reversed.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
}
// ═══ 2. DETERMINISM ═══
@ -354,7 +329,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF, revert OFF by default
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -368,11 +343,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -26,7 +26,7 @@ namespace IslaApocalypse.Tools
/// ISLA_LOOKS comma-separated look names (default: atlas,relief,dusk)
/// ISLA_TASK authoring task number (default 3)
/// ISLA_BATCH descriptor, NO prefix (default "relief_taste")
/// ISLA_SOURCE batch to read .f32 from (default 02_pass1_port)
/// ISLA_SOURCE batch to read .f32 from (default "chat1/02_pass1_port")
/// ISLA_DUMP_RAW "1" to also dump .f32 when a field had to be generated
/// </summary>
public partial class ReliefRenderTool : Node
@ -36,6 +36,10 @@ namespace IslaApocalypse.Tools
public override void _Ready()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1"));
int mapSize = EnvInt("ISLA_MAPSIZE", 2048);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -43,8 +47,8 @@ namespace IslaApocalypse.Tools
string batch = EnvStr("ISLA_BATCH", "relief_taste");
// ⚠ A FULL batch folder name, prefix included — it names an EXISTING folder rather than
// composing a new one, so it is not run through BatchRoot. Tracks TerrainGenTool's
// default output: BatchRoot(task 2, "pass1_port") = 02_pass1_port.
string source = EnvStr("ISLA_SOURCE", "02_pass1_port");
// default output: BatchRoot(task 2, "pass1_port") = <chat>/02_pass1_port (rivers/01: chat-namespaced).
string source = EnvStr("ISLA_SOURCE", "chat1/02_pass1_port");
bool dumpRaw = EnvStr("ISLA_DUMP_RAW", "0") == "1";
LookConfig[] looks = SelectLooks(EnvStr("ISLA_LOOKS", null));
@ -83,7 +87,9 @@ namespace IslaApocalypse.Tools
}
else
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" };
// ⭐ rivers/01: family-off PINNED — this regenerates a PHASE-1 field to stand in for a
// missing `02_pass1_port` dump, so it must reproduce that dump, not the new default.
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }.WithFamilyOff();
Pass1Result r = Topography.Generate(cfg);
height = r.Height;
origin = "generated";

View file

@ -25,7 +25,7 @@ namespace IslaApocalypse.Tools
///
/// ISLA_TASK authoring task number (default 4)
/// ISLA_BATCH descriptor, NO prefix (default "review")
/// ISLA_SOURCE batch holding the .f32 (default 02_pass1_port)
/// ISLA_SOURCE batch holding the .f32 (default "chat1/02_pass1_port")
/// ISLA_MAPSIZE side in columns (default 2048)
/// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds)
/// </summary>
@ -59,16 +59,21 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1"));
int task = EnvInt("ISLA_TASK", 4);
string descr = EnvStr("ISLA_BATCH", "review");
// ⚠ A FULL batch folder name, prefix included — this NAMES AN EXISTING FOLDER rather than
// composing a new one, so it is not run through BatchRoot. The default tracks where
// TerrainGenTool writes by default: BatchRoot(task 2, "pass1_port") = 02_pass1_port.
// TerrainGenTool writes by default: BatchRoot(task 2, "pass1_port") = chat1/02_pass1_port
// (rivers/01 namespaced batches by chat; the READ side carries the chatN/ prefix explicitly).
// It was "01_pass1_port" until chat1/05 renamed the folder to its authoring-task number;
// a stale default here does not fail loudly, it just silently regenerates instead of
// loading — which is exactly the kind of quiet cost worth pinning to the real name.
string source = EnvStr("ISLA_SOURCE", "02_pass1_port");
string source = EnvStr("ISLA_SOURCE", "chat1/02_pass1_port");
int mapSize = EnvInt("ISLA_MAPSIZE", 2048);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -103,7 +108,7 @@ namespace IslaApocalypse.Tools
if (h == null)
{
GD.PrintErr($" ⚠ no full dump for {seed} at {mapSize} — generating deterministically.");
h = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed }).Height;
h = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed)).Height;
}
notes.Add(Gray(h, mapSize, batchRoot, "2_height_grayscale", seed,

View file

@ -126,6 +126,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 4);
string descr = EnvStr("ISLA_BATCH", "seed_gallery");
@ -180,7 +184,7 @@ namespace IslaApocalypse.Tools
var poolPass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s });
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s));
poolPass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -288,13 +292,18 @@ namespace IslaApocalypse.Tools
public ulong ElapsedMs;
}
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/04 is the PRE-FAMILY committed-curve
/// gallery (`terrain-curve-v1`); the re-baseline flipped the bare defaults family-ON, and this
/// tool must keep producing the curve gallery it was judged as. → TerrainGenConfig.WithFamilyOff().
/// </remarks>
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = LowlandCeilingM,
};
}.WithFamilyOff();
private static SeedMetrics WriteSeed(string batchRoot, Pass1Result p1, Pass2Result p2,
float sea, CurveAnchors anchors, bool skipRaw)

View file

@ -675,6 +675,82 @@ namespace IslaApocalypse.Tools
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)
{

View file

@ -41,7 +41,6 @@ namespace IslaApocalypse.Tools
/// ISLA_BAND_START / ISLA_BAND_FEATHER the fixed band (fractions of the map; constants for the batch)
/// ISLA_STRETCH_SINKER 1 = the sinker rides the stretched distance (default), 0 = real y
/// ISLA_DIAG_ONLY=1 diagnostic only
/// ISLA_SKIP_8K=1 skip the 8192 band regression (a4b)
/// </summary>
public partial class SouthernStretchTool : Node
{
@ -64,7 +63,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192;
public override void _Ready()
{
@ -91,6 +89,10 @@ namespace IslaApocalypse.Tools
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
int task = EnvInt("ISLA_TASK", 8);
string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore");
@ -103,11 +105,8 @@ namespace IslaApocalypse.Tools
float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac);
bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1";
bool diagOnly = EnvStr("ISLA_DIAG_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -136,16 +135,26 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, float stretch, bool sinkerStretched, bool sinkerOn = true, bool edgeOn = true)
{
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT:
// it was authored and judged before the shape family existed, and its regression checks
// hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the
// bare defaults family-ON, so without this pin every config here would silently acquire
// stretch + fragmentation and every anchor check would fail for a configuration reason.
// → TerrainGenConfig.WithFamilyOff().
var c = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(),
RegionLabeling = true, SpeckRevert = false,
SouthStretch = stretch, SouthBandStartFrac = bandStart, SouthBandFeatherFrac = bandFeather, StretchSinker = sinkerStretched,
RegionLabeling = true,
SouthernSinker = sinkerOn, EdgeNoise = edgeOn,
};
}.WithFamilyOff();
// …then the swept axis, AFTER the pin. Coastal fragmentation stays OFF here: chat2/08
// predates it, and this tool's ladder measures the stretch ALONE.
c.SouthStretch = stretch;
c.SouthBandStartFrac = bandStart;
c.SouthBandFeatherFrac = bandFeather;
c.StretchSinker = sinkerStretched;
return c;
}
@ -255,34 +264,31 @@ namespace IslaApocalypse.Tools
var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plate}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plate}_continuous_restored", "height.f32");
float[,] t03 = HeightField.Load(t03Dump, calibSize);
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, stretch OFF == task-03 .f32 dump", Shaping.Shape(p1, offCfg).Height, t03, calibSize, t03Dump));
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump",
Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
// ⭐ a3b — stretch ON at the ladder's TOP: north of the band bit-identical to the tag's own dump.
// ⭐ a3c KEPT and RE-POINTED — the north-locked invariant is the load-bearing claim of the
// southern stretch (→ D-065) and it does NOT need an external anchor: the unstretched field
// is generated right here. Re-pointing it off `03_mountain_restore` is what let that dump
// retire without losing the guarantee.
var topCfg = Cfg(calibSize, plate, "top", maxStretch, stretchSinker);
Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg);
if (t03 != null)
hard.Add(ShapingOracle.NorthLocked("a3b", $"stretch {maxStretch:G3} ON: north of the band bit-identical to task-03 dump (terrain-curve-v1); changes only in/below the band", pTop.Height, t03, calibSize, bandRowC));
Pass2Result pUnstretched = Shaping.Shape(p1, offCfg);
hard.Add(ShapingOracle.NorthLocked("a3c",
$"stretch {maxStretch:G3} ON: north of the band bit-identical to the SAME-RUN unstretched field; changes only in/below the band",
pTop.Height, pUnstretched.Height, calibSize, bandRowC));
foreach (var c in hard) GD.Print(" " + c);
if (!skip8k)
{
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plate}", "height.f32");
if (File.Exists(t04Dump))
{
GD.Print($" a4b: generating {plate} at {GallerySize}, stretch {maxStretch:G3} …");
var g = Cfg(GallerySize, plate, "top", maxStretch, stretchSinker);
Pass2Result pG = Shaping.Shape(Topography.Generate(g), g);
var a4b = ShapingOracle.NorthLocked("a4b", $"stretch {maxStretch:G3} ON at {GallerySize}: north of the band bit-identical to terrain-curve-v1's 04 gallery dump",
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, (int)(bandStart * GallerySize));
hard.Add(a4b); GD.Print(" " + a4b);
}
else GD.Print($" a4b: ⚠ skipped — no 04 gallery dump at {t04Dump}");
}
else GD.Print(" a4b: skipped (ISLA_SKIP_8K)");
// ⚑ RETIRED at rivers/01 — a3 (`03_mountain_restore`) and a4b (`04_seed_gallery`).
// a3 was a curve-development intermediate, subsumed by the terrain-shape-v1 acceptance.
// a4b asserted the north-lock against the PRE-FAMILY 8192 gallery — but the north-lock is
// now proven scale-free against a same-run field (a3c above), so the external anchor bought
// nothing except an 8192 generation on every run.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
}
// ═══ 4. THE LADDER — 5 levels × 2 seeds ═══
@ -420,7 +426,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // bare default: offshore / revert / stretch OFF
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
@ -433,11 +439,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
}.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -242,14 +242,20 @@ namespace IslaApocalypse.Tools
// ---- PASS 1b — the coast shelf + offshore islets (chat2/05) ----------
//
// ⚠⚠ BOTH DEFAULT OFF, DELIBERATELY — and that is a decision to revisit, not an oversight.
// ⚠⚠ BOTH STAY OFF AFTER THE rivers/01 RE-BASELINE — and each for its own reason.
//
// Every oracle in this phase holds pass 1 against Phase 1's `.f32` dumps (curve-off ==
// `02_pass1_port`), and the curve tools hold it against task 01/03's. The shelf changes every
// below-sea cell and the islets ADD LAND, so the moment either defaults ON, every one of
// those regression anchors goes stale at once. The batch tools that want them turn them on
// explicitly. FLIPPING THESE DEFAULTS IS THE ACT THAT RETIRES THE PHASE-1 REGRESSION DUMPS —
// do it deliberately, in a task that re-baselines the oracles, not as a side effect here.
// SHELF OFF because the LOCKED SHAPE has no shelf. `terrain-shape-v1` (a59e52f) was
// judged with `CoastShelf = false`, so turning it on here would produce terrain
// the developer never approved and would break the bit-identity this default set
// exists to guarantee. The shelf is below-sea only and invisible until water
// renders — EVALUATING IT IS ITS OWN LATER TASK (→ D-041), once water renders.
// (The rivers kickoff's "set shelf ON" was a mis-statement; corrected by the
// developer via master before rivers/01 ran.)
// ISLETS OFF because it is the DROPPED mechanism (→ D-063): islands are ORGANIC-ONLY,
// produced by the southern stretch + coastal fragmentation and then IDENTIFIED by
// the region layer — never placed. The pass is kept, not deleted, because
// `OffshoreSettings.Faithful` is a live port-fidelity control and `Pass1Result`
// carries the offshore seam. ⚠ NEVER RE-ENABLE IT IN THE BARE DEFAULT.
/// <summary>
/// The submarine coast shelf (<c>IslandFalloff.CoastShelf</c>). Below-sea only,
@ -289,16 +295,27 @@ namespace IslaApocalypse.Tools
/// <see cref="MinLandComponentFrac"/> of the map to seabed. Origin-blind; lower-only and
/// component-only, asserted; mainland never a candidate.
///
/// ⚠ DEFAULT OFF IN THE BARE CONFIG, for exactly the reason the shelf and the islets are: the
/// raw field has small natural nubs, so with this ON the calibration pool's land histogram, the
/// curve knots and every Phase-1 / task-03 / task-04 regression dump would move at once.
/// The region batch turns it on explicitly (its preset is ON); flipping the bare default is
/// the act that re-baselines the oracles — own task, not a side effect.
/// ⭐ ON BY DEFAULT since rivers/01 — it is part of the LOCKED SHAPE (`terrain-shape-v1`).
///
/// ⚠ It was default-OFF through chat 2 because turning it on moves the calibration pool's land
/// histogram, the curve knots and every pre-family regression dump at once. rivers/01 is the
/// task that owned that flip: the calibration pool is now pinned FAMILY-OFF
/// (<see cref="WithFamilyOff"/>), so the knots are unmoved and the flip is terrain-only.
/// </summary>
public bool SpeckRevert = false;
public bool SpeckRevert = true;
/// <summary>The revert threshold, as a fraction of the map's AREA (scale-free). → <see cref="RegionPass.ThresholdMidFrac"/>.</summary>
public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
/// <summary>
/// The revert threshold, as a fraction of the map's AREA (scale-free).
///
/// ⚠⚠ THE LOCKED-SHAPE VALUE, PINNED AS A LITERAL — 2.5e-7 ≈ 4 cells at 4096, ≈ 17 at 8192.
/// It is deliberately NOT <see cref="RegionPass.ThresholdMidFrac"/> (3e-5, ~2,013 cells at
/// 8192), which was the pre-re-baseline default and is 120× larger: at that threshold the
/// revert eats real islands rather than specks. The three named `RegionPass.Threshold*Frac`
/// values are the chat2/07 exploration ladder; this is the value chat2/0910 froze and the
/// developer judged. Changing it changes `terrain-shape-v1`. (rivers/01, from
/// `FragGalleryTool.FrozenSpeckFrac`.)
/// </summary>
public float MinLandComponentFrac = 2.5e-7f;
// ---- PASS 1 — THE SOUTHERN STRETCH (chat2/08, exploration) ----------------
//
@ -311,14 +328,30 @@ namespace IslaApocalypse.Tools
// organically. Cells north of the band take the UNTOUCHED code path, so the classify field
// there is bit-identical by construction (asserted). Nothing is stamped.
/// <summary>⭐ THE SWEPT AXIS. 0 = off (bit-identical to the unstretched field everywhere). Stretch factor inside the band: 1 ⇒ the southward distance is halved, 3 ⇒ quartered.</summary>
public float SouthStretch = 0f;
/// <summary>
/// ⭐ THE SWEPT AXIS. 0 = off (bit-identical to the unstretched field everywhere). Stretch
/// factor inside the band: 1 ⇒ the southward distance is halved, 3 ⇒ quartered.
///
/// ⭐ 2 IS THE LOCKED-SHAPE VALUE since rivers/01 (→ D-065). With coastal fragmentation it is
/// one of the two mechanisms that MAKE the islands (→ D-063).
/// </summary>
public float SouthStretch = 2f;
/// <summary>The band's FIXED latitude line, fraction of the map (y runs south). Sea identity is hard above it. A constant for a whole batch.</summary>
public float SouthBandStartFrac = SouthernStretch.DefaultBandStartFrac;
/// <summary>
/// The band's FIXED latitude line, fraction of the map (y runs south). Sea identity is hard
/// above it. A constant for a whole batch.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandStartFrac`
/// today). The literal is the pin: retuning that constant must not silently move
/// `terrain-shape-v1`. (rivers/01.)
/// </summary>
public float SouthBandStartFrac = 0.70f;
/// <summary>The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map. A constant for a whole batch.</summary>
public float SouthBandFeatherFrac = SouthernStretch.DefaultBandFeatherFrac;
/// <summary>
/// The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandFeatherFrac`
/// today) — same reason as <see cref="SouthBandStartFrac"/>. (rivers/01.)
/// </summary>
public float SouthBandFeatherFrac = 0.05f;
/// <summary>
/// Does the SOUTHERN SINKER ride the stretched distance (true — it is part of the southern
@ -326,7 +359,8 @@ namespace IslaApocalypse.Tools
/// it keeps pulling the extended mass down where it always did)? The chat2/08 diagnostic
/// measured both; → <see cref="SouthernStretch"/>.
/// </summary>
public bool StretchSinker = SouthernStretch.DefaultStretchSinker;
/// <remarks>⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultStretchSinker` today). rivers/01.</remarks>
public bool StretchSinker = true;
// ---- PASS 1 — COASTAL FRAGMENTATION (chat2/09, exploration) ------------------
//
@ -336,34 +370,59 @@ namespace IslaApocalypse.Tools
// into islands while the interior — window weight exactly zero — is bit-identical by
// construction. Nothing is detected, nothing is stamped. → CoastalFragment.
/// <summary>⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the window's centre.</summary>
public float FragmentAmp = 0f;
/// <summary>
/// ⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the
/// window's centre.
///
/// ⭐ 0.5 IS THE LOCKED-SHAPE VALUE since rivers/01 — chat2/09's `frag_4`, frozen by chat2/10
/// across an 8-seed gallery and tagged `terrain-shape-v1`. With the southern stretch it is one
/// of the two mechanisms that MAKE the islands (→ D-063).
/// </summary>
public float FragmentAmp = 0.5f;
/// <summary>The fragmentation noise's frequency, periods per map width — the neck/lobe scale. The secondary dial (fixed this round). → <see cref="CoastalFragment.DefaultFreqPerMapWidth"/>.</summary>
public float FragmentFreqPerMapWidth = CoastalFragment.DefaultFreqPerMapWidth;
/// <summary>
/// The fragmentation noise's frequency, periods per map width — the neck/lobe scale.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultFreqPerMapWidth`
/// today); the literal is the pin. (rivers/01.)
/// </summary>
public float FragmentFreqPerMapWidth = 12f;
/// <summary>The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the centre, smooth to 0 at ± half-width; exactly 0 beyond.</summary>
public float FragmentBandCentre = CoastalFragment.DefaultBandCentre;
public float FragmentBandHalfWidth = CoastalFragment.DefaultBandHalfWidth;
/// <summary>
/// The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the
/// centre, smooth to 0 at ± half-width; exactly 0 beyond.
/// ⚠ Pinned as LITERALS at the locked-shape values (equal `CoastalFragment.DefaultBandCentre` /
/// `DefaultBandHalfWidth` today). (rivers/01.)
/// </summary>
public float FragmentBandCentre = 0.66f;
public float FragmentBandHalfWidth = 0.18f;
/// <summary>
/// false (default) ⇒ zero-mean noise: the margin is redrawn — bites AND builds (which can also
/// bridge an island back onto the mainland). true ⇒ bites only ((noise+1)/2 ≥ 0): land can only
/// recede, necks are cut, nothing is bridged, the coast net-recedes. → <see cref="CoastalFragment"/>.
/// </summary>
public bool FragmentBitesOnly = CoastalFragment.DefaultBitesOnly;
/// <remarks>⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultBitesOnly` today). rivers/01.</remarks>
public bool FragmentBitesOnly = false;
// ---- PASS 2b — HYDRAULIC EROSION (chat2/11) — RENDER MAP ONLY ------------------
//
// The reference's droplet erosion, ported verbatim (Core.HydraulicErosion), run on the render
// field AFTER shaping (after detail, before the crater carve — which does not exist yet). The
// classify field never sees it (D-046); the caller's flood guard proves no waterline moved.
// ⚠ DEFAULT OFF in the bare config for the usual reason (regression anchors); the batch turns
// it on. The governors + physics are the reference ConfigManager's declared defaults, clamped
// as it clamped them (→ ErosionPass).
// ⭐ DEFAULT ON since rivers/01 — the locked baseline the rivers epic routes on is the ERODED
// render field. The governors + physics are the reference ConfigManager's declared defaults,
// clamped as it clamped them (→ ErosionPass).
//
// ⚠ This flag is INERT unless a caller explicitly runs `ErosionPass.Apply` — nothing in
// `Topography.Generate` or `Shaping.Shape` reads it. So flipping it moves no field on its own;
// it makes "erode by default" the answer for the callers that DO ask.
/// <summary>⭐ Erosion on/off. Render only. Default OFF (see above).</summary>
public bool Erosion = false;
/// <summary>
/// ⭐ Erosion on/off. RENDER MAP ONLY — the classify field never sees it (→ D-046), proven by
/// `ErosionPass`'s flood guard. Default ON since rivers/01 (see above); the erosion A/B sets
/// it to false explicitly for its OFF half.
/// </summary>
public bool Erosion = true;
/// <summary>Governor 1 — droplet count. Reference 250000, clamp [0, 50,000,000].</summary>
public int ErosionDropletCount = 250000;
@ -401,6 +460,75 @@ namespace IslaApocalypse.Tools
/// <summary>The scale object every distance and frequency in the generator derives from.</summary>
public GenerationScale Scale => new GenerationScale(MapSize);
// ═══ ⭐⭐ THE FAMILY-OFF PIN (rivers/01) ═══════════════════════════════════════════════════
//
// ═══ WHY THIS EXISTS — the preserve mechanism for `terrain-shape-v1` ═══
//
// The locked shape's curve knots are PERCENTILES OF THE FAMILY-OFF LAND DISTRIBUTION, measured
// by chat2/01 over a 6-seed pool at 2048 and applied to FAMILY-ON generation. That was not a
// choice at the time — it was simply what the bare defaults produced, because the shape family
// defaulted off.
//
// rivers/01 flipped those defaults ON. Left alone, every calibration pool would have moved
// with them (fragmentation removes coastal land, the stretch adds southern land — both change
// the land CDF), the six knots would have moved, and with them the render field of EVERY
// batch, including `10_frag4_seed_gallery` (= `terrain-shape-v1`) and `11_erosion`. The
// developer's ruling was to PRESERVE the locked terrain bit-identically, so the pool is pinned
// here instead of the knots being baked: CALIBRATION STAYS LIVE, its INPUT DISTRIBUTION is
// what is held still.
//
// ⚠ At the moment it was introduced this was a NO-OP BY CONSTRUCTION: it sets exactly the
// values the bare defaults carried the instant before the flip. That is what made the flip
// provably terrain-only — and what the rivers/01 acceptance confirmed byte-for-byte at 8192².
//
// > ### ⚑ THE JUDGED-AND-PARKED PROPERTY (recorded for the vault, rivers/01)
// > The curve knots are percentiles of the FAMILY-OFF land distribution, applied to FAMILY-ON
// > terrain. That is a real asymmetry and it is DELIBERATE, not an oversight: re-pooling on
// > family-on land would move the locked shape the developer judged. Same disposition as the
// > mid-slope feather — documented, revisit only at the final palette / in the mesher if it
// > ever visibly bothers. → `Vision - Threads - Open Questions.md`.
//
// ═══ WHAT IT IS FOR, AND WHAT IT IS NOT FOR ═══
//
// USE IT for a config whose job is to REPRODUCE A PRE-FAMILY FIELD: a curve-calibration
// pool, or the "off" half of a regression check against a family-off `.f32` dump.
// DO NOT use it for generation — the locked shape IS the family, and the bare defaults now
// carry it.
/// <summary>
/// ⭐ Pin the SHAPE FAMILY and erosion OFF on this config, independent of this class's
/// evolving defaults, and return it for chaining. → the block above for why.
///
/// Sets: <see cref="CoastShelf"/> false · <see cref="Offshore"/> Off ·
/// <see cref="SpeckRevert"/> false · <see cref="SouthStretch"/> 0 ·
/// <see cref="FragmentAmp"/> 0 · <see cref="Erosion"/> false.
///
/// ⚠ It deliberately does NOT touch <see cref="MinLandComponentFrac"/> or the band/window
/// shape dials: with the revert off and the amplitudes at zero those are unread, so pinning
/// them would assert an independence that does not exist. It also does not touch the CURVE
/// (knots, anchors, calibration, climb knobs) — the family and the curve are separate axes,
/// and a calibration pool is pass-1 only.
/// </summary>
public TerrainGenConfig WithFamilyOff()
{
CoastShelf = false;
Offshore = new OffshoreSettings(); // Mode = Off
SpeckRevert = false;
SouthStretch = 0f;
FragmentAmp = 0f;
Erosion = false;
return this;
}
/// <summary>
/// ⭐ A bare pass-1 config with the shape family pinned OFF — THE CALIBRATION POOL'S CONFIG.
/// Every curve-calibration pool in `Tools/` builds its fields through this, so there is one
/// place where "what distribution were the knots measured on?" is answered.
/// → <see cref="WithFamilyOff"/>.
/// </summary>
public static TerrainGenConfig CalibrationPool(int mapSize, int seed) =>
new TerrainGenConfig { MapSize = mapSize, Seed = seed }.WithFamilyOff();
/// <summary>
/// ⚠ DEEP on <see cref="Anchors"/>. <c>MemberwiseClone</c> is shallow, so two configs cloned
/// from one parent would share a single mutable anchor object and an A/B that edited one
@ -421,6 +549,10 @@ namespace IslaApocalypse.Tools
$"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " +
$"trench={Trench} spine={MountainSpine}] " +
$"[curve={Curve} detail={ShelfDetail} relief={ShelfReliefAmpM:F1}m edge={ShelfEdgeVariationM:F1}m " +
$"knots={(Knots == null ? "-" : Knots.Name)}]";
$"knots={(Knots == null ? "-" : Knots.Name)}] " +
// rivers/01: the shape family is now a DEFAULT, so a run header must state it — otherwise
// "the defaults" stops being a readable claim the moment anyone asks which defaults.
$"[stretch={SouthStretch:G3} frag={FragmentAmp:G3} speck={(SpeckRevert ? $"{MinLandComponentFrac:G3}" : "off")} " +
$"shelf={CoastShelf} islets={Offshore?.Mode} erosion={Erosion}]";
}
}

View file

@ -43,6 +43,10 @@ namespace IslaApocalypse.Tools
public override void _Ready()
{
ToolingPaths.Configure(OS.GetUserDataDir());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1"));
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -81,7 +85,11 @@ namespace IslaApocalypse.Tools
GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---");
foreach (var (label, mutate) in Ladder())
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label };
// ⭐⭐ rivers/01 — FAMILY-OFF PINNED. This tool AUTHORED `02_pass1_port`, the Phase-1
// regression anchor that survived the re-baseline. If it picked up the family-on
// defaults it could no longer regenerate its own dump, and the last link between
// today's generator and the Phase-1 port would break silently.
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label }.WithFamilyOff();
mutate(cfg);
rows.Add(RunOne(cfg, batchRoot, skipRaw));
}
@ -92,7 +100,7 @@ namespace IslaApocalypse.Tools
GD.Print("\n--- SEED BATCH (full pass-1) ---");
foreach (int seed in seeds)
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" };
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }.WithFamilyOff(); // ⭐ see the ladder above
rows.Add(RunOne(cfg, batchRoot, skipRaw));
}

View file

@ -0,0 +1,116 @@
using System;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐⭐ THE LOCKED SHAPE — `terrain-shape-v1` (commit <c>a59e52f</c>), AS AN ASSERTION.
///
/// ═══ ⚠⚠ THIS TYPE INVERTED AT rivers/01. READ THIS BEFORE USING IT. ═══
///
/// It used to be a PRESET: <c>Apply(cfg)</c> stamped the locked shape onto a bare config, because
/// the bare defaults did not reproduce the terrain the developer had judged. That was the single
/// most expensive fact in the codebase — *"run the default generator" ≠ "the terrain the developer
/// locked"* — and a fresh chat comparing bare-default output against the locked renders would see
/// differences that were CONFIGURATION, not regression.
///
/// **rivers/01 re-baselined the defaults so that `new TerrainGenConfig()` IS the locked shape.**
/// So <c>Apply</c> is GONE — there is nothing left to apply, and re-stamping the values on top of
/// the defaults would mask a default drift instead of catching it.
///
/// > ### What survives is the OPPOSITE job: these constants are now the ASSERTION TARGET.
/// > They are the values the developer judged, written down once, and <see cref="Assert"/> holds
/// > the live defaults against them. If a default is ever edited, the batch tools that claim to
/// > render the locked shape REFUSE TO RUN rather than quietly rendering something else.
///
/// ⚠ DO NOT "fix" a drift by editing these constants — they are the record of what was approved,
/// and `10_frag4_seed_gallery` / `11_erosion` are its pixels. A deliberate shape change moves the
/// defaults AND these constants AND re-runs the acceptance, in one task, as rivers/01 did.
///
/// ⚠ THE SHELF IS OFF, DELIBERATELY. The locked shape has no coast shelf; evaluating it (→ D-041)
/// is its own later task, once water renders. ⚠ THE ISLETS ARE OFF, PERMANENTLY (→ D-063): islands
/// are organic-only, made by the stretch + fragmentation and identified by the region layer.
///
/// ⚠ EROSION IS NOT PART OF THIS SHAPE. `terrain-shape-v1` is the pass-1/1c/2a field; erosion is
/// pass 2b, on top, render-only (`11_erosion` = `ea291ea`). It is asserted separately.
/// </summary>
public static class TerrainShapeV1
{
/// <summary>The tagged commit this shape is defined by. ⚠ Reference the COMMIT — the tag is annotated and local-only until the developer pushes it.</summary>
public const string Commit = "a59e52f";
public const float FragmentAmp = 0.5f, FragmentFreq = 12f, BandCentre = 0.66f, BandHalfWidth = 0.18f;
public const bool BitesOnly = false;
public const float Stretch = 2f, BandStart = 0.70f, BandFeather = 0.05f;
public const bool StretchSinker = true;
public const float SpeckFrac = 2.5e-7f;
/// <summary>
/// ⭐ THE DEFAULT-DRIFT GUARD. Throws unless a bare <see cref="TerrainGenConfig"/> carries the
/// locked shape exactly. Every batch tool that renders or asserts `terrain-shape-v1` calls this
/// before it generates anything.
///
/// ⚠ It is a THROW, not a warning, for the same reason <see cref="FileSafety"/> is: a batch
/// that renders the wrong terrain still produces beautiful, browsable, wrong PNGs, and a human
/// gate cannot see a default from a picture.
/// </summary>
/// <param name="who">The calling tool, for the message.</param>
public static void Assert(string who)
{
var d = new TerrainGenConfig();
string bad = null;
void Want(string name, object got, object want)
{
if (!Equals(got, want)) bad = (bad == null ? "" : bad + "; ") + $"{name} = {got}, expected {want}";
}
Want(nameof(d.SouthStretch), d.SouthStretch, Stretch);
Want(nameof(d.SouthBandStartFrac), d.SouthBandStartFrac, BandStart);
Want(nameof(d.SouthBandFeatherFrac), d.SouthBandFeatherFrac, BandFeather);
Want(nameof(d.StretchSinker), d.StretchSinker, StretchSinker);
Want(nameof(d.FragmentAmp), d.FragmentAmp, FragmentAmp);
Want(nameof(d.FragmentFreqPerMapWidth), d.FragmentFreqPerMapWidth, FragmentFreq);
Want(nameof(d.FragmentBandCentre), d.FragmentBandCentre, BandCentre);
Want(nameof(d.FragmentBandHalfWidth), d.FragmentBandHalfWidth, BandHalfWidth);
Want(nameof(d.FragmentBitesOnly), d.FragmentBitesOnly, BitesOnly);
Want(nameof(d.SpeckRevert), d.SpeckRevert, true);
Want(nameof(d.MinLandComponentFrac), d.MinLandComponentFrac, SpeckFrac);
Want(nameof(d.CoastShelf), d.CoastShelf, false);
Want(nameof(d.RegionLabeling), d.RegionLabeling, true);
Want("Offshore.Mode", d.Offshore.Mode, OffshoreMode.Off);
if (bad != null)
throw new InvalidOperationException(
$"[{who}] LOCKED-SHAPE DRIFT: the bare TerrainGenConfig no longer reproduces " +
$"terrain-shape-v1 ({Commit}) — {bad}. Refusing to run: this tool's output is only " +
"meaningful if the defaults ARE the locked shape. → Tools/Scripts/TerrainShapeV1.cs (rivers/01).");
}
/// <summary>
/// The same guard for the EROSION default, kept separate because erosion is pass 2b and is not
/// part of the shape. Tools that render the erosion A/B set the flag per variant and call this
/// only if they rely on the default.
/// </summary>
public static void AssertErosionDefaultOn(string who)
{
if (!new TerrainGenConfig().Erosion)
throw new InvalidOperationException(
$"[{who}] EROSION DEFAULT DRIFT: bare TerrainGenConfig.Erosion is false; rivers/01 " +
"made it true (the rivers baseline routes on the eroded render field). Refusing to run.");
}
/// <summary>
/// One line for a run header, READ FROM THE LIVE DEFAULTS rather than from the constants —
/// so the header states what actually ran, and <see cref="Assert"/> states whether that is
/// still the locked shape.
/// </summary>
public static string Describe()
{
var d = new TerrainGenConfig();
return $"terrain-shape-v1 ({Commit}) from the BARE DEFAULTS: frag amp {d.FragmentAmp} freq {d.FragmentFreqPerMapWidth} " +
$"window {d.FragmentBandCentre}±{d.FragmentBandHalfWidth} · stretch {d.SouthStretch} " +
$"(band {d.SouthBandStartFrac}/{d.SouthBandFeatherFrac}, sinker {(d.StretchSinker ? "stretched" : "real-y")}) · " +
$"speck revert {d.MinLandComponentFrac:G2} · offshore {d.Offshore.Mode} · shelf {(d.CoastShelf ? "ON" : "OFF")} · labeling {(d.RegionLabeling ? "ON" : "OFF")}";
}
}
}

View file

@ -8,12 +8,33 @@ Real batch output is written under `ISLA_OUTPUT_DIR` (default `user://output/bat
## Layout
```
batches/NN_<name>/ NN = the task number that ran it
batches/<chat>/NN_<name>/ <chat> = the chat namespace · NN = the task number that ran it
├── INDEX.md what varied, what to look at, what was concluded
├── scratch/ intermediates — PERSISTENT, never cleaned
└── <seed>_<variant>/ one directory per generated world
```
### ⭐ The `<chat>` segment (rivers/01)
**`NN` is the AUTHORING TASK NUMBER, and task numbers restart at 00 in every build chat** — so a flat
`batches/` collides the moment a second chat exists. It did: chat 1's `02_pass1_port` and chat 2's
`02_curve_continuous` are both "batch 02", and nothing in either name says which chat made it. On the
real pile there were **four colliding prefixes (02, 03, 04, 06)** across 25 batches, separable only by
SLUG.
The slug is set by `ToolingPaths.ConfigureChat(...)` and is **required** — with none set, `BatchRoot`
throws rather than writing to the un-namespaced root. Each tool defaults to its own authoring chat, so
re-running it reproduces its batch in place; **`ISLA_CHAT` redirects a run to another namespace**, which
is what keeps an acceptance run from overwriting the very anchor it is checking against.
> ### ⚠ Writes are namespaced; historical READS carry the prefix themselves.
> `BatchRoot(task, descriptor)` inserts `<chat>`. `BatchesRoot` is the plain root, and every
> `ISLA_*_SOURCE` anchor composes against it — so an anchor default is written out in full, e.g.
> `"chat1/02_pass1_port"`. Namespacing only the writes would silently orphan every historical read.
> **That is why a missing anchor now THROWS** (`ShapingOracle.LoadAnchor`): a moved anchor used to make
> its oracle not RUN, and a batch with a silently-skipped check prints an all-PASS table that reads
> exactly like a clean one.
`INDEX.md` is not optional. A/B comparisons are browsed by a human, and a flat directory of
same-named PNGs is not browsable.