rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat

The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.

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

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

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

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

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

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

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

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

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

The baseline moved in DEFAULTS, not in TERRAIN.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
This commit is contained in:
Stewart Howe 2026-08-23 04:45:10 -04:00
parent 89e0f85c9f
commit 03fe75b378
20 changed files with 856 additions and 314 deletions

View file

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

View file

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

View file

@ -38,7 +38,6 @@ namespace IslaApocalypse.Tools
/// ISLA_SPECK_FRAC the speck-revert threshold, fraction of map area (default 2.5e-7 ≈ 4 cells at 4096) /// ISLA_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_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_FRAG_BITES=1 bites-only noise ([0,1]) instead of zero-mean ([-1,1])
/// ISLA_SKIP_8K=1 (no 8192 check this batch — the 08 dump at 4096 is the baseline)
/// </summary> /// </summary>
public partial class CoastalFragmentTool : Node public partial class CoastalFragmentTool : Node
{ {
@ -86,6 +85,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 9);
string descr = EnvStr("ISLA_BATCH", "coastal_fragment"); string descr = EnvStr("ISLA_BATCH", "coastal_fragment");
@ -101,9 +104,7 @@ namespace IslaApocalypse.Tools
float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs); float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs);
float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps); float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/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 batchRoot = ToolingPaths.BatchRoot(task, descr); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); 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) 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, MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(), RegionLabeling = true,
RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = speckFrac, }.WithFamilyOff();
SouthStretch = st, // …then this tool's swept axes, AFTER the pin. (These were already explicit before
FragmentAmp = amp, FragmentFreqPerMapWidth = fq, FragmentBitesOnly = bitesOnly, // 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 ═══ // ═══ PROBE ═══
@ -186,21 +197,17 @@ namespace IslaApocalypse.Tools
var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false); var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false);
Pass1Result p1 = Topography.Generate(offCfg); Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false; 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"); 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). // ⚑ RETIRED at rivers/01 — a8, the frag-OFF baseline == `08_southern_stretch_explore`.
foreach (int seed in seeds) // 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
string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_{t08Level}", "height.f32"); // find a value, then superseded by the value it found.
if (File.Exists(t08Dump) && mapSize == 4096) // 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 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}")}");
}
foreach (var c in hard) GD.Print(" " + c); foreach (var c in hard) GD.Print(" " + c);
} }
@ -311,7 +318,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -324,11 +331,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

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

View file

@ -48,8 +48,7 @@ namespace IslaApocalypse.Tools
/// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below) /// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below)
/// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192) /// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192)
/// ISLA_SHOWPIECE "0" to skip it /// ISLA_SHOWPIECE "0" to skip it
/// 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_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps /// ISLA_SKIP_RAW "1" to skip the .f32 dumps
/// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30) /// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30)
/// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4) /// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4)
@ -91,6 +90,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 2);
string descr = EnvStr("ISLA_BATCH", "curve_continuous"); string descr = EnvStr("ISLA_BATCH", "curve_continuous");
@ -98,8 +101,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form
@ -128,7 +130,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds) 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; pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize); rawPool.Accumulate(p1.Height, mapSize);
GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms"); 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]; 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"); string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump", 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. // ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control.
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32"); // The staircase curve is SUPERSEDED by the continuous grade (→ D-062). A control that
hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump", // reproduces a curve nothing ships is scaffolding, and holding it green cost a
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump)); // 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. // (b) classify == raw, every seed × every variant.
long bFail = 0; long bFail = 0;
@ -340,7 +347,11 @@ namespace IslaApocalypse.Tools
LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f), LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f),
ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f), ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f),
SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f), 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 ----------------------------------------------------------- // ---- output -----------------------------------------------------------

View file

@ -54,6 +54,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 12);
string descr = EnvStr("ISLA_BATCH", "drainage_analysis"); string descr = EnvStr("ISLA_BATCH", "drainage_analysis");
@ -62,7 +66,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipT11 = EnvStr("ISLA_SKIP_T11_CHECK", "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); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -77,7 +81,11 @@ namespace IslaApocalypse.Tools
GD.Print("=================================================================="); GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}"); GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
GD.Print($"seeds : {string.Join(", ", seeds)}"); 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($"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($"batch : {batchRoot}");
GD.Print("=================================================================="); GD.Print("==================================================================");
@ -88,14 +96,15 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed) 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 var c = new TerrainGenConfig
{ {
MapSize = size, Seed = seed, VariantLabel = "drainage", MapSize = size, Seed = seed, VariantLabel = "drainage",
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
}; };
TerrainShapeV1.Apply(c);
c.Erosion = true; // the faithful tune — the defaults
return c; return c;
} }
@ -114,15 +123,16 @@ namespace IslaApocalypse.Tools
Pass2Result p2 = ero.Shaped; Pass2Result p2 = ero.Shaped;
GD.Print($" terrain ready ({p1.ElapsedMs} ms pass 1, erosion {ero.Ms / 1000.0:F1} s)"); 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) if (!skipT11)
{ {
string dump = Path.Combine(ToolingPaths.BatchesRoot, t11Source, $"{seed}_erosion_on", "height.f32"); 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 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);
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);
hard.Add(a11); GD.Print(" " + a11);
}
else GD.Print($" a11 [{seed}]: ⚠ skipped — {(mapSize != 8192 ? "map size is not the 11 batch's 8192" : $"no dump at {dump}")}");
} }
// ⭐ THE OCEAN IDENTITY — from the region layer, on the CLASSIFY field. // ⭐ THE OCEAN IDENTITY — from the region layer, on the CLASSIFY field.
@ -284,7 +294,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -297,11 +307,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -7,33 +7,6 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
/// <summary>
/// ⭐ THE LOCKED SHAPE — `terrain-shape-v1` (chat2/10 gallery-confirmed): the continuous curve + the
/// frag_4 organic islands. Every later pass (erosion, rivers, …) starts from exactly these values,
/// pinned here once so no tool re-types them.
/// </summary>
public static class TerrainShapeV1
{
public const float FragmentAmp = 0.5f, FragmentFreq = 12f, BandCentre = 0.66f, BandHalfWidth = 0.18f;
public const bool BitesOnly = false;
public const float Stretch = 2f, BandStart = 0.70f, BandFeather = 0.05f;
public const bool StretchSinker = true;
public const float SpeckFrac = 2.5e-7f;
/// <summary>Apply the locked shape to a config (curve settings are the caller's — they come from the calibration).</summary>
public static void Apply(TerrainGenConfig c)
{
c.CoastShelf = false; c.Offshore = new OffshoreSettings();
c.RegionLabeling = true; c.SpeckRevert = true; c.MinLandComponentFrac = SpeckFrac;
c.SouthStretch = Stretch; c.SouthBandStartFrac = BandStart; c.SouthBandFeatherFrac = BandFeather; c.StretchSinker = StretchSinker;
c.FragmentAmp = FragmentAmp; c.FragmentFreqPerMapWidth = FragmentFreq;
c.FragmentBandCentre = BandCentre; c.FragmentBandHalfWidth = BandHalfWidth; c.FragmentBitesOnly = BitesOnly;
}
public static string Describe() =>
$"terrain-shape-v1: frag amp {FragmentAmp} freq {FragmentFreq} window {BandCentre}±{BandHalfWidth} · stretch {Stretch} (band {BandStart}/{BandFeather}, sinker stretched) · speck revert {SpeckFrac:G2} · offshore OFF · shelf OFF · labeling ON";
}
/// <summary> /// <summary>
/// ⭐ THE EROSION BATCH (chat2/11) — the faithful droplet erosion on the locked shape, judged across /// ⭐ 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 /// 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() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 11);
string descr = EnvStr("ISLA_BATCH", "erosion"); string descr = EnvStr("ISLA_BATCH", "erosion");
@ -93,7 +70,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipTag = EnvStr("ISLA_SKIP_TAG_CHECK", "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); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -107,6 +84,7 @@ namespace IslaApocalypse.Tools
GD.Print("=================================================================="); GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}"); GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
GD.Print($"seeds : {string.Join(", ", seeds)}"); 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($"shape : {TerrainShapeV1.Describe()}");
GD.Print($"batch : {batchRoot}"); GD.Print($"batch : {batchRoot}");
GD.Print("=================================================================="); GD.Print("==================================================================");
@ -117,13 +95,17 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, bool erosion) 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 var c = new TerrainGenConfig
{ {
MapSize = size, Seed = seed, VariantLabel = label, MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
}; };
TerrainShapeV1.Apply(c);
c.Erosion = erosion; c.Erosion = erosion;
c.ErosionDropletCount = EnvInt("ISLA_ERO_COUNT", c.ErosionDropletCount); c.ErosionDropletCount = EnvInt("ISLA_ERO_COUNT", c.ErosionDropletCount);
c.ErosionDropletLifetime = EnvInt("ISLA_ERO_LIFETIME", c.ErosionDropletLifetime); c.ErosionDropletLifetime = EnvInt("ISLA_ERO_LIFETIME", c.ErosionDropletLifetime);
@ -156,12 +138,15 @@ namespace IslaApocalypse.Tools
var cOff = Cfg(mapSize, seed, "erosion_off", false); var cOff = Cfg(mapSize, seed, "erosion_off", false);
Pass1Result p1Off = Topography.Generate(cOff); Pass1Result p1Off = Topography.Generate(cOff);
Pass2Result p2Off = Shaping.Shape(p1Off, 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) if (!skipTag)
{ {
string dump = Path.Combine(ToolingPaths.BatchesRoot, t10Source, $"{seed}", "height.f32"); string dump = Path.Combine(ToolingPaths.BatchesRoot, t10Source, $"{seed}", "height.f32");
if (File.Exists(dump) && mapSize == 8192) hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF from the BARE DEFAULTS == terrain-shape-v1 (the task-10 gallery dump) [{seed}]",
hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF == terrain-shape-v1 (the task-10 gallery dump) [{seed}]", p2Off.Height, HeightField.Load(dump, mapSize), mapSize, dump)); p2Off.Height, ShapingOracle.LoadAnchor("a10", "ISLA_T10_SOURCE", 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}")}");
} }
Image reliefOff = ReliefRenderer.Render(p2Off.Height, mapSize, look); Image reliefOff = ReliefRenderer.Render(p2Off.Height, mapSize, look);
Image shadeOff = ShadeRenderer.Render(p2Off.Height, mapSize, sea, ShadeZ, look.LightAzimuth, look.LightAltitude); Image shadeOff = ShadeRenderer.Render(p2Off.Height, mapSize, sea, ShadeZ, look.LightAzimuth, look.LightAltitude);
@ -281,7 +266,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -294,11 +279,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -22,7 +22,7 @@ namespace IslaApocalypse.Tools
/// ISLA_MAPSIZE gallery size (default 8192) /// ISLA_MAPSIZE gallery size (default 8192)
/// ISLA_CALIB_SIZE curve calibration size (default 2048) /// ISLA_CALIB_SIZE curve calibration size (default 2048)
/// ISLA_SEEDS the gallery seeds (default: the 2 anchors + 6 fresh below) /// ISLA_SEEDS the gallery seeds (default: the 2 anchors + 6 fresh below)
/// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 bit-identity check against the 09 frag_4 dumps /// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 interior-locked invariant check
/// </summary> /// </summary>
public partial class FragGalleryTool : Node public partial class FragGalleryTool : Node
{ {
@ -35,17 +35,22 @@ namespace IslaApocalypse.Tools
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary> /// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; 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) ═══ // ═══ THE FROZEN frag_4 SETTING (chat2/09 batch, level 4) ═══
private const float FrozenFragmentAmp = 0.5f; //
private const float FrozenFragmentFreq = 12f; // ⭐ rivers/01: these were the ONLY home of the locked values. They now ALIAS
private const float FrozenBandCentre = 0.66f; // `TerrainShapeV1`, which is itself the assertion target for `TerrainGenConfig`'s defaults —
private const float FrozenBandHalfWidth = 0.18f; // so the chain is: bare defaults → asserted against TerrainShapeV1 → printed here. One value,
private const bool FrozenBitesOnly = false; // one place, and a throw if the generator ever stops agreeing with it.
private const float FrozenStretch = 2f; private const float FrozenFragmentAmp = TerrainShapeV1.FragmentAmp;
private const float FrozenBandStart = 0.70f; private const float FrozenFragmentFreq = TerrainShapeV1.FragmentFreq;
private const float FrozenBandFeather = 0.05f; private const float FrozenBandCentre = TerrainShapeV1.BandCentre;
private const bool FrozenStretchSinker = true; private const float FrozenBandHalfWidth = TerrainShapeV1.BandHalfWidth;
private const float FrozenSpeckFrac = 2.5e-7f; // 09's low speck revert (≈ 4 cells at 4096, ≈ 17 at 8192) 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 DefaultMapSize = 8192;
private const int DefaultCalibSize = 2048; private const int DefaultCalibSize = 2048;
@ -79,6 +84,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 10);
string descr = EnvStr("ISLA_BATCH", "frag4_seed_gallery"); string descr = EnvStr("ISLA_BATCH", "frag4_seed_gallery");
@ -87,9 +96,7 @@ namespace IslaApocalypse.Tools
int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null); int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipAnchor = EnvStr("ISLA_SKIP_ANCHOR_CHECK", "0") == "1"; bool skipAnchor = EnvStr("ISLA_SKIP_ANCHOR_CHECK", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore");
string t09Source = EnvStr("ISLA_T09_SOURCE", "09_coastal_fragment");
var seeds = new List<int>(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); } var seeds = new List<int>(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); }
var anchorSet = new HashSet<int>(AnchorSeeds); var anchorSet = new HashSet<int>(AnchorSeeds);
@ -118,17 +125,40 @@ namespace IslaApocalypse.Tools
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors); var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
GD.Print($" {knots}"); 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, var c = new TerrainGenConfig
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, {
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, MapSize = size, Seed = seed, VariantLabel = label,
CoastShelf = false, Offshore = new OffshoreSettings(), Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = FrozenSpeckFrac, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
SouthStretch = stretch ? FrozenStretch : 0f, SouthBandStartFrac = FrozenBandStart, SouthBandFeatherFrac = FrozenBandFeather, StretchSinker = FrozenStretchSinker, };
FragmentAmp = frag ? FrozenFragmentAmp : 0f, FragmentFreqPerMapWidth = FrozenFragmentFreq, if (!stretch) c.SouthStretch = 0f;
FragmentBandCentre = FrozenBandCentre, FragmentBandHalfWidth = FrozenBandHalfWidth, FragmentBitesOnly = FrozenBitesOnly, 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 ═══ // ═══ 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 ---"); 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); var offCfg = Frozen(calibSize, AnchorSeeds[0], "off", frag: false, revert: false, stretch: false);
Pass1Result p1 = Topography.Generate(offCfg); Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false; 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"); 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) if (!skipAnchor)
{ {
foreach (int seed in AnchorSeeds) foreach (int seed in AnchorSeeds)
{ {
// ⭐ a9 — the frozen setting at the 09 batch's size reproduces the 09 frag_4 field bit for bit. // ⭐ r KEPT and SELF-ANCHORED — the interior-locked invariant is coastal fragmentation's
string t09Dump = Path.Combine(ToolingPaths.BatchesRoot, t09Source, $"{seed}_frag_4", "height.f32"); // 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"); var c9 = Frozen(AnchorCheckSize, seed, "frag_4");
Pass1Result q9p = Topography.Generate(c9); 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); var c8 = Frozen(AnchorCheckSize, seed, "t08", frag: false, revert: false);
Pass1Result q8p = Topography.Generate(c8); 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); var r = ShapingOracle.InteriorLocked(q8p, q9p, FrozenBandCentre, FrozenBandHalfWidth); r.Name += $" [{seed}, {AnchorCheckSize}]"; hard.Add(r);
} }
// determinism at the check size // determinism at the check size
@ -203,7 +240,7 @@ namespace IslaApocalypse.Tools
foreach (var c in perSeed) if (!c.Passed) GD.PrintErr(" " + c); foreach (var c in perSeed) if (!c.Passed) GD.PrintErr(" " + c);
WriteTable(batchRoot, mapSize, rows, big, speckCells); 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("\n==================================================================");
GD.Print($" DONE — {batchRoot}"); GD.Print($" DONE — {batchRoot}");
@ -262,7 +299,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -275,11 +312,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.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()); WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
} }
private static void WriteIndex(string batchRoot, int mapSize, int calibSize, List<Row> rows, long big, long speckCells, private static void WriteIndex(string batchRoot, int task, int mapSize, int calibSize, List<Row> rows, long big, long speckCells,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perSeed, bool allOk) List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perSeed, bool allOk)
{ {
var sb = new StringBuilder(); 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();
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("**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"); 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();
sb.AppendLine($"`FragmentAmp {FrozenFragmentAmp}` · `FragmentFreqPerMapWidth {FrozenFragmentFreq}` · window `{FrozenBandCentre} ± {FrozenBandHalfWidth}` · bites-only `{FrozenBitesOnly}` · " + 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) · " + $"`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();
sb.AppendLine("## ⭐ The contact sheet"); sb.AppendLine("## ⭐ The contact sheet");
sb.AppendLine(); sb.AppendLine();

View file

@ -44,7 +44,7 @@ namespace IslaApocalypse.Tools
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn
/// ///
/// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE /// 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_SKIP_RAW
/// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35) /// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35)
/// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5) /// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5)
@ -77,6 +77,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 3);
string descr = EnvStr("ISLA_BATCH", "mountain_restore"); string descr = EnvStr("ISLA_BATCH", "mountain_restore");
@ -84,8 +88,7 @@ namespace IslaApocalypse.Tools
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f); float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f);
float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f); float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f);
@ -114,7 +117,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds) 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; pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize); rawPool.Accumulate(p1.Height, mapSize);
} }
@ -219,12 +222,17 @@ namespace IslaApocalypse.Tools
var hard = new List<ShapingOracle.Check>(); var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>(); var soft = new List<ShapingOracle.Check>();
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off).
// ⚠ A missing dump now THROWS instead of skipping silently.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32"); string 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", 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));
hard.Add(ShapingOracle.DumpRegression("a2", "staircase == 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.
// 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; long bFail = 0;
foreach (int seed in seeds) foreach (int seed in seeds)
@ -335,13 +343,20 @@ namespace IslaApocalypse.Tools
GetTree().Quit(hardOk ? 0 : 3); GetTree().Quit(hardOk ? 0 : 3);
} }
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored
/// and judged before the shape family existed, on the family-off distribution the knots are
/// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what
/// keeps this tool measuring the thing it was written to measure.
/// → <see cref="TerrainGenConfig.WithFamilyOff"/>.
/// </remarks>
private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label) private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig => new TerrainGenConfig
{ {
MapSize = mapSize, Seed = seed, VariantLabel = label, MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a, Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = 30f, LowlandCeilingM = 30f,
}; }.WithFamilyOff();
// ---- output --------------------------------------------------------- // ---- output ---------------------------------------------------------

View file

@ -57,8 +57,7 @@ namespace IslaApocalypse.Tools
/// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides) /// ISLA_OFF_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_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_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 the Phase-1 regression dump's batch (default "chat1/02_pass1_port")
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches
/// </summary> /// </summary>
public partial class OffshoreIslandsTool : Node public partial class OffshoreIslandsTool : Node
{ {
@ -77,7 +76,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096; private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048; private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192; // the 04 gallery's render size
/// <summary>The consistency targets the table is read against: "a couple north, a few south".</summary> /// <summary>The consistency targets the table is read against: "a couple north, a few south".</summary>
private const int TargetNorth = 2, TargetSouth = 3; private const int TargetNorth = 2, TargetSouth = 3;
@ -112,6 +110,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 6);
string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune"); string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune");
@ -121,12 +123,9 @@ namespace IslaApocalypse.Tools
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0); int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
string batchRoot = ToolingPaths.BatchRoot(task, descr); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -185,14 +184,17 @@ namespace IslaApocalypse.Tools
var curveOff = offCfg.Clone(); curveOff.Curve = false; var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff); 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"); string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump", 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); // ⚑ RETIRED at rivers/01 — a3, `continuous_restored` == `03_mountain_restore`.
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); // A curve-development intermediate: it proved the chat2/03 climb restoration against
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)", // chat2/02. Both are upstream of the locked shape, and `terrain-shape-v1` (a10) now
pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump)); // 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). // 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"; 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. // ⭐ 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. // The literal "offshore-off is bit-identical to terrain-curve-v1", at full size.
if (!skip8k) // ⚑ 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
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); // baseline the generator is deliberately no longer on; `a10` replaced it as the 8192
if (File.Exists(t04Dump)) // 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);
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …"); // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
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)");
} }
// ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══ // ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══
@ -402,7 +394,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -416,11 +408,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.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); return (knots, cal, pass1);
} }
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT
/// (chat2/0506): authored and judged before the shape family existed, and its regressions hold
/// pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the bare defaults
/// family-ON, so without <see cref="TerrainGenConfig.WithFamilyOff"/> every config here would
/// silently acquire stretch + fragmentation and the anchor checks would fail for a configuration
/// reason, not a regression. The variants re-enable offshore explicitly, after the pin.
/// </remarks>
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
ClimbCalibration cal, string label) => new TerrainGenConfig ClimbCalibration cal, string label) => new TerrainGenConfig
{ {
MapSize = mapSize, Seed = seed, VariantLabel = label, MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f, 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 ----------------------------------------------------------- // ---- output -----------------------------------------------------------

View file

@ -42,8 +42,7 @@ namespace IslaApocalypse.Tools
/// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands) /// 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_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_TABLE_ONLY=1 probe: table only (no regressions, no plates)
/// ISLA_SKIP_8K=1 skip the 8192 regression (a4) /// ISLA_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port")
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches
/// </summary> /// </summary>
public partial class RegionLabelingTool : Node public partial class RegionLabelingTool : Node
{ {
@ -57,7 +56,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096; private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048; private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192;
public override void _Ready() public override void _Ready()
{ {
@ -89,6 +87,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 7);
string descr = EnvStr("ISLA_BATCH", "region_labeling"); string descr = EnvStr("ISLA_BATCH", "region_labeling");
@ -98,13 +100,9 @@ namespace IslaApocalypse.Tools
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int secondEnv = EnvInt("ISLA_SECOND_SEED", 0); int secondEnv = EnvInt("ISLA_SECOND_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/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");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
var levels = new List<Level> var levels = new List<Level>
{ {
@ -141,7 +139,14 @@ namespace IslaApocalypse.Tools
TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac) 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(); } if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); }
c.RegionLabeling = true; c.RegionLabeling = true;
c.SpeckRevert = revertOn; c.SpeckRevert = revertOn;
@ -159,26 +164,17 @@ namespace IslaApocalypse.Tools
var curveOff = offCfg.Clone(); curveOff.Curve = false; var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff); 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"); 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", 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); // ⚑ RETIRED at rivers/01 — a3 and a3r, both against `03_mountain_restore`.
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); // A curve-development intermediate, subsumed by the `terrain-shape-v1` acceptance (a10).
float[,] t03 = HeightField.Load(t03Dump, calibSize); // a3r was informational only, and its subject — how many natural specks the revert takes —
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump", // is now reported by the region ledger every run, with the revert ON by default.
pRest.Height, t03, calibSize, t03Dump)); // 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.
// 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);
var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only"; var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
Pass1Result p1Shelf = Topography.Generate(shelfCfg); Pass1Result p1Shelf = Topography.Generate(shelfCfg);
@ -188,34 +184,13 @@ namespace IslaApocalypse.Tools
hard.Add(ShapingOracle.CentreIsLand(p1)); hard.Add(ShapingOracle.CentreIsLand(p1));
foreach (var c in hard) GD.Print(" " + c); foreach (var c in hard) GD.Print(" " + c);
if (!skip8k) // ⚑ 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
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); // 8192 acceptance now. a6 held the ORGANIC ISLET layer — the DROPPED mechanism (→ D-063):
if (File.Exists(t04Dump)) // 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.
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …"); // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac); // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
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}")}");
} }
// ═══ 2. DETERMINISM ═══ // ═══ 2. DETERMINISM ═══
@ -354,7 +329,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -368,11 +343,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

@ -26,7 +26,7 @@ namespace IslaApocalypse.Tools
/// ISLA_LOOKS comma-separated look names (default: atlas,relief,dusk) /// ISLA_LOOKS comma-separated look names (default: atlas,relief,dusk)
/// ISLA_TASK authoring task number (default 3) /// ISLA_TASK authoring task number (default 3)
/// ISLA_BATCH descriptor, NO prefix (default "relief_taste") /// 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 /// ISLA_DUMP_RAW "1" to also dump .f32 when a field had to be generated
/// </summary> /// </summary>
public partial class ReliefRenderTool : Node public partial class ReliefRenderTool : Node
@ -36,6 +36,10 @@ namespace IslaApocalypse.Tools
public override void _Ready() public override void _Ready()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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 mapSize = EnvInt("ISLA_MAPSIZE", 2048);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -43,8 +47,8 @@ namespace IslaApocalypse.Tools
string batch = EnvStr("ISLA_BATCH", "relief_taste"); string batch = EnvStr("ISLA_BATCH", "relief_taste");
// ⚠ A FULL batch folder name, prefix included — it names an EXISTING folder rather than // ⚠ 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 // composing a new one, so it is not run through BatchRoot. Tracks TerrainGenTool's
// default output: BatchRoot(task 2, "pass1_port") = 02_pass1_port. // default output: BatchRoot(task 2, "pass1_port") = <chat>/02_pass1_port (rivers/01: chat-namespaced).
string source = EnvStr("ISLA_SOURCE", "02_pass1_port"); string source = EnvStr("ISLA_SOURCE", "chat1/02_pass1_port");
bool dumpRaw = EnvStr("ISLA_DUMP_RAW", "0") == "1"; bool dumpRaw = EnvStr("ISLA_DUMP_RAW", "0") == "1";
LookConfig[] looks = SelectLooks(EnvStr("ISLA_LOOKS", null)); LookConfig[] looks = SelectLooks(EnvStr("ISLA_LOOKS", null));
@ -83,7 +87,9 @@ namespace IslaApocalypse.Tools
} }
else 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); Pass1Result r = Topography.Generate(cfg);
height = r.Height; height = r.Height;
origin = "generated"; origin = "generated";

View file

@ -25,7 +25,7 @@ namespace IslaApocalypse.Tools
/// ///
/// ISLA_TASK authoring task number (default 4) /// ISLA_TASK authoring task number (default 4)
/// ISLA_BATCH descriptor, NO prefix (default "review") /// 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_MAPSIZE side in columns (default 2048)
/// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds) /// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds)
/// </summary> /// </summary>
@ -59,16 +59,21 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 4);
string descr = EnvStr("ISLA_BATCH", "review"); string descr = EnvStr("ISLA_BATCH", "review");
// ⚠ A FULL batch folder name, prefix included — this NAMES AN EXISTING FOLDER rather than // ⚠ 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 // 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; // 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 // 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. // 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 mapSize = EnvInt("ISLA_MAPSIZE", 2048);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -103,7 +108,7 @@ namespace IslaApocalypse.Tools
if (h == null) if (h == null)
{ {
GD.PrintErr($" ⚠ no full dump for {seed} at {mapSize} — generating deterministically."); 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, notes.Add(Gray(h, mapSize, batchRoot, "2_height_grayscale", seed,

View file

@ -126,6 +126,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 4);
string descr = EnvStr("ISLA_BATCH", "seed_gallery"); string descr = EnvStr("ISLA_BATCH", "seed_gallery");
@ -180,7 +184,7 @@ namespace IslaApocalypse.Tools
var poolPass1 = new Dictionary<int, Pass1Result>(); var poolPass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; poolPass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -288,13 +292,18 @@ namespace IslaApocalypse.Tools
public ulong ElapsedMs; public ulong ElapsedMs;
} }
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/04 is the PRE-FAMILY committed-curve
/// gallery (`terrain-curve-v1`); the re-baseline flipped the bare defaults family-ON, and this
/// tool must keep producing the curve gallery it was judged as. → TerrainGenConfig.WithFamilyOff().
/// </remarks>
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label) private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig => new TerrainGenConfig
{ {
MapSize = mapSize, Seed = seed, VariantLabel = label, MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a, Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = LowlandCeilingM, LowlandCeilingM = LowlandCeilingM,
}; }.WithFamilyOff();
private static SeedMetrics WriteSeed(string batchRoot, Pass1Result p1, Pass2Result p2, private static SeedMetrics WriteSeed(string batchRoot, Pass1Result p1, Pass2Result p2,
float sea, CurveAnchors anchors, bool skipRaw) float sea, CurveAnchors anchors, bool skipRaw)

View file

@ -675,6 +675,82 @@ namespace IslaApocalypse.Tools
return c; return c;
} }
// ═══ ⭐⭐ ANCHOR RESOLUTION — A MISSING ANCHOR IS LOUD (rivers/01) ═══════════════════════════
//
// ═══ THE FAILURE MODE THIS CLOSES ═══
//
// Every anchor check used to be written as:
//
// if (File.Exists(dump) && mapSize == 8192) hard.Add(DumpRegression(...));
// else GD.Print(" ⚠ skipped — no dump at …");
//
// so a moved, renamed or deleted anchor did not make the oracle FAIL. It made the oracle
// NOT RUN — and a batch with a silently-skipped check prints an all-PASS table and reads
// exactly like a clean one. The `INDEX.md` is then evidence for a claim nothing checked.
//
// > ### ⚠ This is the INVERSE of the hazard the re-baseline guards against.
// > The re-baseline stops an oracle PASSING FOR THE WRONG REASON. This stops one
// > DISAPPEARING FOR NO REASON. Both end with a green table and an unproven claim, and the
// > rivers/01 batch-root migration is exactly the event that would have triggered the second
// > one — nine anchors moved under `<chat>/` in a single commit.
//
// The two cases the old code conflated are now separated, in ONE place (`LoadAnchor`) so every
// anchor site behaves identically:
// MISSING FILE the anchor moved, was renamed, or was never written. THROWS — the check
// cannot run, and dropping it quietly is the failure this exists to close.
// WRONG SIZE the anchor exists, but only at the size it was captured. A legitimate case
// (a probe run at another size); reported loudly and recorded INCONCLUSIVE,
// which is a FAIL in the table. ⚠ Deliberately NOT a throw: a guard that fires
// on ordinary small-map probe work is a guard people learn to route around.
/// <summary>
/// ⭐ Load a regression anchor's `.f32`, separating the two failures the old code conflated.
///
/// FILE ABSENT → THROWS. The anchor moved, was renamed, or was never written.
/// This is the migration hazard, and it is not survivable: the
/// check cannot run and must not be quietly dropped.
/// PRESENT, WRONG SIZE → returns null, LOUDLY. A legitimate case — an anchor exists only
/// at the size it was captured, and a batch run at another size
/// genuinely cannot check against it. The caller's
/// <see cref="DumpRegression"/> records it INCONCLUSIVE, which is
/// a FAIL in the table, never a pass.
///
/// ⚠ The distinction matters because only ONE of them means something is broken. Throwing on a
/// size mismatch would make every small-map probe run refuse, and a guard that fires on ordinary
/// work is a guard people route around.
/// </summary>
/// <param name="checkId">The oracle check this anchor feeds, e.g. "a10" — named in the message.</param>
/// <param name="envVar">The env override that can re-point it, e.g. "ISLA_T10_SOURCE".</param>
/// <param name="dumpPath">The resolved absolute path.</param>
/// <param name="mapSize">The field size to read.</param>
public static float[,] LoadAnchor(string checkId, string envVar, string dumpPath, int mapSize)
{
if (!System.IO.File.Exists(dumpPath))
throw new InvalidOperationException(
$"[Oracle] MISSING ANCHOR for check '{checkId}' — nothing at:\n" +
$" {dumpPath}\n" +
"An oracle whose anchor is absent does not fail, it does not RUN — and a batch with a " +
"silently-skipped check prints an all-PASS table that reads exactly like a clean one. " +
"Refusing to render evidence for a claim nothing checked.\n" +
$"→ Re-point it with {envVar}, or regenerate the anchor. If the anchor is genuinely " +
"retired, DELETE THE CHECK — never leave one aimed at nothing. (rivers/01.)");
long expected = (long)mapSize * mapSize * 4;
long actual = new System.IO.FileInfo(dumpPath).Length;
if (actual != expected)
{
int anchorSize = (int)System.Math.Round(System.Math.Sqrt(actual / 4.0));
Godot.GD.PrintErr(
$" ⚠⚠ {checkId}: NOT CHECKED — the anchor exists but was captured at {anchorSize}, " +
$"and this run is at {mapSize} ({actual:N0} bytes, expected {expected:N0}). This is a size " +
$"mismatch, NOT a missing file: the batch is simply not proven against it at this size. " +
$"Run at {anchorSize} to check it. The oracle records INCONCLUSIVE, which is a FAIL — never a pass.");
return null;
}
return HeightField.Load(dumpPath, mapSize);
}
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary> /// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
public static string ToMarkdownTable(IEnumerable<Check> checks) public static string ToMarkdownTable(IEnumerable<Check> checks)
{ {

View file

@ -41,7 +41,6 @@ namespace IslaApocalypse.Tools
/// ISLA_BAND_START / ISLA_BAND_FEATHER the fixed band (fractions of the map; constants for the batch) /// ISLA_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_STRETCH_SINKER 1 = the sinker rides the stretched distance (default), 0 = real y
/// ISLA_DIAG_ONLY=1 diagnostic only /// ISLA_DIAG_ONLY=1 diagnostic only
/// ISLA_SKIP_8K=1 skip the 8192 band regression (a4b)
/// </summary> /// </summary>
public partial class SouthernStretchTool : Node public partial class SouthernStretchTool : Node
{ {
@ -64,7 +63,6 @@ namespace IslaApocalypse.Tools
private const int DefaultMapSize = 4096; private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048; private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192;
public override void _Ready() public override void _Ready()
{ {
@ -91,6 +89,10 @@ namespace IslaApocalypse.Tools
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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); int task = EnvInt("ISLA_TASK", 8);
string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore"); string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore");
@ -103,11 +105,8 @@ namespace IslaApocalypse.Tools
float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac); float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac);
bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1"; bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1";
bool diagOnly = EnvStr("ISLA_DIAG_ONLY", "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"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
string batchRoot = ToolingPaths.BatchRoot(task, descr); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); 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) 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 var c = new TerrainGenConfig
{ {
MapSize = size, Seed = seed, VariantLabel = label, MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(), RegionLabeling = true,
RegionLabeling = true, SpeckRevert = false,
SouthStretch = stretch, SouthBandStartFrac = bandStart, SouthBandFeatherFrac = bandFeather, StretchSinker = sinkerStretched,
SouthernSinker = sinkerOn, EdgeNoise = edgeOn, 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; return c;
} }
@ -255,34 +264,31 @@ namespace IslaApocalypse.Tools
var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker); var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker);
Pass1Result p1 = Topography.Generate(offCfg); Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false; 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"); 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)); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump",
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plate}_continuous_restored", "height.f32"); Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
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));
// ⭐ 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); var topCfg = Cfg(calibSize, plate, "top", maxStretch, stretchSinker);
Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg); Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg);
if (t03 != null) Pass2Result pUnstretched = Shaping.Shape(p1, offCfg);
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)); 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); foreach (var c in hard) GD.Print(" " + c);
if (!skip8k) // ⚑ 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.
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plate}", "height.f32"); // a4b asserted the north-lock against the PRE-FAMILY 8192 gallery — but the north-lock is
if (File.Exists(t04Dump)) // now proven scale-free against a same-run field (a3c above), so the external anchor bought
{ // nothing except an 8192 generation on every run.
GD.Print($" a4b: generating {plate} at {GallerySize}, stretch {maxStretch:G3} …"); // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
var g = Cfg(GallerySize, plate, "top", maxStretch, stretchSinker); // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
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)");
} }
// ═══ 4. THE LADDER — 5 levels × 2 seeds ═══ // ═══ 4. THE LADDER — 5 levels × 2 seeds ═══
@ -420,7 +426,7 @@ namespace IslaApocalypse.Tools
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds) 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; pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize); rawPool.Accumulate(p1.Height, calibSize);
} }
@ -433,11 +439,14 @@ namespace IslaApocalypse.Tools
var outAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds) 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 var scfg = new TerrainGenConfig
{ {
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}; }.WithFamilyOff();
Pass2Result st = Shaping.Shape(pass1[s], scfg); Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);

View file

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

View file

@ -43,6 +43,10 @@ namespace IslaApocalypse.Tools
public override void _Ready() public override void _Ready()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); 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 mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
@ -81,7 +85,11 @@ namespace IslaApocalypse.Tools
GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---"); GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---");
foreach (var (label, mutate) in Ladder()) 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); mutate(cfg);
rows.Add(RunOne(cfg, batchRoot, skipRaw)); rows.Add(RunOne(cfg, batchRoot, skipRaw));
} }
@ -92,7 +100,7 @@ namespace IslaApocalypse.Tools
GD.Print("\n--- SEED BATCH (full pass-1) ---"); GD.Print("\n--- SEED BATCH (full pass-1) ---");
foreach (int seed in seeds) 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)); rows.Add(RunOne(cfg, batchRoot, skipRaw));
} }

View file

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

View file

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