diff --git a/Core/Scripts/ToolingPaths.cs b/Core/Scripts/ToolingPaths.cs
index 63ccb63..9909a38 100644
--- a/Core/Scripts/ToolingPaths.cs
+++ b/Core/Scripts/ToolingPaths.cs
@@ -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
/// Whether has been called. Tooling should assert this before a run.
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 ), 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 : with no slug set,
+ // 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;
+
+ ///
+ /// Set the chat namespace batches are written under. Called once at startup by every batch
+ /// tool, with its authoring chat as the fallback: ConfigureChat(EnvStr(ChatVar, "chat2")).
+ ///
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ /// The chat namespace. ⚠ Throws if has not been called.
+ public static string ChatSlug => _chatSlug ?? throw new InvalidOperationException(
+ "CHAT SLUG NOT SET. Batches are namespaced by chat (batches//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.");
+
+ /// Whether has been called.
+ public static bool IsChatConfigured => _chatSlug != null;
+
/// The generation config file. Override: ISLA_CONFIG_PATH.
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 , which inserts the 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 ShapingOracle.LoadAnchor now throws on a missing anchor.
+ ///
/// ⚠ PROTECTED FROM DELETION. → .
///
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");
///
- /// ⭐ A BATCH ROOT: batches/<task>_<descriptor>/.
+ /// ⭐ A BATCH ROOT: batches/<chat>/<task>_<descriptor>/.
///
/// ═══ ⚠⚠ 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 <chat> SEGMENT (rivers/01) ═══
+ ///
+ /// Prepended from , because the task-number prefix restarts at 00 in
+ /// every chat — see the note on . 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.
///
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}");
}
///
@@ -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}");
+ ///
+ /// Resolve a HISTORICAL batch by its namespaced name, e.g. "chat1/02_pass1_port" — the
+ /// form every `ISLA_*_SOURCE` anchor default takes since rivers/01. Kept beside
+ /// so a READ and a WRITE are visibly two different operations.
+ ///
+ 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 : "")}/NN_slug/";
private static string Marker(string variable) => Override(variable) != null ? $" [{variable}]" : "";
}
diff --git a/Tools/README.md b/Tools/README.md
index ae92b7a..b962d3d 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -343,9 +343,15 @@ Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises.
### 4. Batch layout
```
-batches/_/_/
-batches/_/INDEX.md
-batches/_/scratch/ ← persistent; never cleaned
+batches//_/_/
+batches//_/INDEX.md
+batches//_/scratch/ ← persistent; never cleaned
+
+⚠ 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.
diff --git a/Tools/Scripts/CoastalFragmentTool.cs b/Tools/Scripts/CoastalFragmentTool.cs
index 8517962..36bc2c3 100644
--- a/Tools/Scripts/CoastalFragmentTool.cs
+++ b/Tools/Scripts/CoastalFragmentTool.cs
@@ -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)
///
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();
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);
diff --git a/Tools/Scripts/CurveBaselineTool.cs b/Tools/Scripts/CurveBaselineTool.cs
index 54c1835..e016df0 100644
--- a/Tools/Scripts/CurveBaselineTool.cs
+++ b/Tools/Scripts/CurveBaselineTool.cs
@@ -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
///
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 --------------------------------------------------------
+ ///
+ /// ⭐ 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.
+ /// → .
+ ///
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
///
/// Fraction of land below the CostaRica palette's third stop (0.310 raw). A blunt
diff --git a/Tools/Scripts/CurveContinuousTool.cs b/Tools/Scripts/CurveContinuousTool.cs
index 3fd1b31..9c44867 100644
--- a/Tools/Scripts/CurveContinuousTool.cs
+++ b/Tools/Scripts/CurveContinuousTool.cs
@@ -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();
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 -----------------------------------------------------------
diff --git a/Tools/Scripts/DrainageTool.cs b/Tools/Scripts/DrainageTool.cs
index fceb5d9..1d97c10 100644
--- a/Tools/Scripts/DrainageTool.cs
+++ b/Tools/Scripts/DrainageTool.cs
@@ -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,15 +123,16 @@ 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);
- 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}")}");
+ 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);
}
// ⭐ THE OCEAN IDENTITY — from the region layer, on the CLASSIFY field.
@@ -284,7 +294,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary();
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);
diff --git a/Tools/Scripts/ErosionTool.cs b/Tools/Scripts/ErosionTool.cs
index 88507f2..d19babf 100644
--- a/Tools/Scripts/ErosionTool.cs
+++ b/Tools/Scripts/ErosionTool.cs
@@ -7,33 +7,6 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
- ///
- /// ⭐ 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.
- ///
- 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;
-
- /// Apply the locked shape to a config (curve settings are the caller's — they come from the calibration).
- 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";
- }
-
///
/// ⭐ 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();
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);
diff --git a/Tools/Scripts/FragGalleryTool.cs b/Tools/Scripts/FragGalleryTool.cs
index 143d6ce..6a68237 100644
--- a/Tools/Scripts/FragGalleryTool.cs
+++ b/Tools/Scripts/FragGalleryTool.cs
@@ -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
///
public partial class FragGalleryTool : Node
{
@@ -35,17 +35,22 @@ namespace IslaApocalypse.Tools
/// ⚠ Task 01's pool, verbatim — the curve's identity.
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(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); }
var anchorSet = new HashSet(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)
{
- 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,
- };
+ 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,
+ };
+ 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();
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 rows, long big, long speckCells,
+ private static void WriteIndex(string batchRoot, int task, int mapSize, int calibSize, List rows, long big, long speckCells,
List hard, List 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();
diff --git a/Tools/Scripts/MountainRestoreTool.cs b/Tools/Scripts/MountainRestoreTool.cs
index afb4d56..af5e70c 100644
--- a/Tools/Scripts/MountainRestoreTool.cs
+++ b/Tools/Scripts/MountainRestoreTool.cs
@@ -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();
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();
var soft = new List();
+ // ⭐ 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);
}
+ ///
+ /// ⭐ 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.
+ /// → .
+ ///
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 ---------------------------------------------------------
diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs
index 5417d50..62bd0a0 100644
--- a/Tools/Scripts/OffshoreIslandsTool.cs
+++ b/Tools/Scripts/OffshoreIslandsTool.cs
@@ -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")
///
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
/// The consistency targets the table is read against: "a couple north, a few south".
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();
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);
}
+ ///
+ /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT
+ /// (chat2/05–06): 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 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.
+ ///
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 -----------------------------------------------------------
diff --git a/Tools/Scripts/RegionLabelingTool.cs b/Tools/Scripts/RegionLabelingTool.cs
index 2c44186..5687ed1 100644
--- a/Tools/Scripts/RegionLabelingTool.cs
+++ b/Tools/Scripts/RegionLabelingTool.cs
@@ -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")
///
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
{
@@ -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();
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);
diff --git a/Tools/Scripts/ReliefRenderTool.cs b/Tools/Scripts/ReliefRenderTool.cs
index 955866b..ad77ab7 100644
--- a/Tools/Scripts/ReliefRenderTool.cs
+++ b/Tools/Scripts/ReliefRenderTool.cs
@@ -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
///
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") = /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";
diff --git a/Tools/Scripts/ReviewBatchTool.cs b/Tools/Scripts/ReviewBatchTool.cs
index 0b218cc..7111eed 100644
--- a/Tools/Scripts/ReviewBatchTool.cs
+++ b/Tools/Scripts/ReviewBatchTool.cs
@@ -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)
///
@@ -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,
diff --git a/Tools/Scripts/SeedGalleryTool.cs b/Tools/Scripts/SeedGalleryTool.cs
index 30dddb8..d5df989 100644
--- a/Tools/Scripts/SeedGalleryTool.cs
+++ b/Tools/Scripts/SeedGalleryTool.cs
@@ -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();
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;
}
+ ///
+ /// ⭐ 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().
+ ///
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)
diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs
index 3d9842f..c252e20 100644
--- a/Tools/Scripts/ShapingOracle.cs
+++ b/Tools/Scripts/ShapingOracle.cs
@@ -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 `/` 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.
+
+ ///
+ /// ⭐ 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
+ /// 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.
+ ///
+ /// The oracle check this anchor feeds, e.g. "a10" — named in the message.
+ /// The env override that can re-point it, e.g. "ISLA_T10_SOURCE".
+ /// The resolved absolute path.
+ /// The field size to read.
+ 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);
+ }
+
/// Render the whole oracle as a markdown table for the INDEX and the report.
public static string ToMarkdownTable(IEnumerable checks)
{
diff --git a/Tools/Scripts/SouthernStretchTool.cs b/Tools/Scripts/SouthernStretchTool.cs
index cd0304c..934768e 100644
--- a/Tools/Scripts/SouthernStretchTool.cs
+++ b/Tools/Scripts/SouthernStretchTool.cs
@@ -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)
///
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();
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);
diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs
index c6dfe99..419c597 100644
--- a/Tools/Scripts/TerrainGenConfig.cs
+++ b/Tools/Scripts/TerrainGenConfig.cs
@@ -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.
///
/// The submarine coast shelf (IslandFalloff.CoastShelf). Below-sea only,
@@ -289,16 +295,27 @@ namespace IslaApocalypse.Tools
/// 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
+ /// (), so the knots are unmoved and the flip is terrain-only.
///
- public bool SpeckRevert = false;
+ public bool SpeckRevert = true;
- /// The revert threshold, as a fraction of the map's AREA (scale-free). → .
- public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
+ ///
+ /// 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 (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/09–10 froze and the
+ /// developer judged. Changing it changes `terrain-shape-v1`. (rivers/01, from
+ /// `FragGalleryTool.FrozenSpeckFrac`.)
+ ///
+ 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.
- /// ⭐ 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.
- public float SouthStretch = 0f;
+ ///
+ /// ⭐ 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).
+ ///
+ public float SouthStretch = 2f;
- /// 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.
- public float SouthBandStartFrac = SouthernStretch.DefaultBandStartFrac;
+ ///
+ /// 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.)
+ ///
+ public float SouthBandStartFrac = 0.70f;
- /// The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map. A constant for a whole batch.
- public float SouthBandFeatherFrac = SouthernStretch.DefaultBandFeatherFrac;
+ ///
+ /// 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 . (rivers/01.)
+ ///
+ public float SouthBandFeatherFrac = 0.05f;
///
/// 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; → .
///
- public bool StretchSinker = SouthernStretch.DefaultStretchSinker;
+ /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultStretchSinker` today). rivers/01.
+ 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.
- /// ⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the window's centre.
- public float FragmentAmp = 0f;
+ ///
+ /// ⭐ 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).
+ ///
+ public float FragmentAmp = 0.5f;
- /// The fragmentation noise's frequency, periods per map width — the neck/lobe scale. The secondary dial (fixed this round). → .
- public float FragmentFreqPerMapWidth = CoastalFragment.DefaultFreqPerMapWidth;
+ ///
+ /// 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.)
+ ///
+ public float FragmentFreqPerMapWidth = 12f;
- /// 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.
- public float FragmentBandCentre = CoastalFragment.DefaultBandCentre;
- public float FragmentBandHalfWidth = CoastalFragment.DefaultBandHalfWidth;
+ ///
+ /// 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.)
+ ///
+ public float FragmentBandCentre = 0.66f;
+ public float FragmentBandHalfWidth = 0.18f;
///
/// 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. → .
///
- public bool FragmentBitesOnly = CoastalFragment.DefaultBitesOnly;
+ /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultBitesOnly` today). rivers/01.
+ 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.
- /// ⭐ Erosion on/off. Render only. Default OFF (see above).
- public bool Erosion = false;
+ ///
+ /// ⭐ 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.
+ ///
+ public bool Erosion = true;
/// Governor 1 — droplet count. Reference 250000, clamp [0, 50,000,000].
public int ErosionDropletCount = 250000;
@@ -401,6 +460,75 @@ namespace IslaApocalypse.Tools
/// The scale object every distance and frequency in the generator derives from.
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.
+
+ ///
+ /// ⭐ 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: false · Off ·
+ /// false · 0 ·
+ /// 0 · false.
+ ///
+ /// ⚠ It deliberately does NOT touch 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.
+ ///
+ public TerrainGenConfig WithFamilyOff()
+ {
+ CoastShelf = false;
+ Offshore = new OffshoreSettings(); // Mode = Off
+ SpeckRevert = false;
+ SouthStretch = 0f;
+ FragmentAmp = 0f;
+ Erosion = false;
+ return this;
+ }
+
+ ///
+ /// ⭐ 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.
+ /// → .
+ ///
+ public static TerrainGenConfig CalibrationPool(int mapSize, int seed) =>
+ new TerrainGenConfig { MapSize = mapSize, Seed = seed }.WithFamilyOff();
+
///
/// ⚠ DEEP on . MemberwiseClone 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}]";
}
}
diff --git a/Tools/Scripts/TerrainGenTool.cs b/Tools/Scripts/TerrainGenTool.cs
index 799023e..f32057a 100644
--- a/Tools/Scripts/TerrainGenTool.cs
+++ b/Tools/Scripts/TerrainGenTool.cs
@@ -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));
}
diff --git a/Tools/Scripts/TerrainShapeV1.cs b/Tools/Scripts/TerrainShapeV1.cs
new file mode 100644
index 0000000..6e16eaa
--- /dev/null
+++ b/Tools/Scripts/TerrainShapeV1.cs
@@ -0,0 +1,116 @@
+using System;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// ⭐⭐ THE LOCKED SHAPE — `terrain-shape-v1` (commit a59e52f), AS AN ASSERTION.
+ ///
+ /// ═══ ⚠⚠ THIS TYPE INVERTED AT rivers/01. READ THIS BEFORE USING IT. ═══
+ ///
+ /// It used to be a PRESET: Apply(cfg) 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 Apply 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 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.
+ ///
+ public static class TerrainShapeV1
+ {
+ /// The tagged commit this shape is defined by. ⚠ Reference the COMMIT — the tag is annotated and local-only until the developer pushes it.
+ 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;
+
+ ///
+ /// ⭐ THE DEFAULT-DRIFT GUARD. Throws unless a bare 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 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.
+ ///
+ /// The calling tool, for the message.
+ 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).");
+ }
+
+ ///
+ /// 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.
+ ///
+ 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.");
+ }
+
+ ///
+ /// One line for a run header, READ FROM THE LIVE DEFAULTS rather than from the constants —
+ /// so the header states what actually ran, and states whether that is
+ /// still the locked shape.
+ ///
+ 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")}";
+ }
+ }
+}
diff --git a/Tools/batches/README.md b/Tools/batches/README.md
index 23e2826..58ecbb5 100644
--- a/Tools/batches/README.md
+++ b/Tools/batches/README.md
@@ -8,12 +8,33 @@ Real batch output is written under `ISLA_OUTPUT_DIR` (default `user://output/bat
## Layout
```
-batches/NN_/ NN = the task number that ran it
+batches//NN_/ = 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
└── _/ one directory per generated world
```
+### ⭐ The `` 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 ``. `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.