diff --git a/Core/Scripts/ToolingPaths.cs b/Core/Scripts/ToolingPaths.cs index 63ccb63..9909a38 100644 --- a/Core/Scripts/ToolingPaths.cs +++ b/Core/Scripts/ToolingPaths.cs @@ -26,6 +26,7 @@ namespace IslaApocalypse.Core /// ISLA_CONFIG_PATH the generation config file default: user://config.json /// ISLA_BLUEPRINT_PATH the blueprint read/written default: user://blueprints /// ISLA_OUTPUT_DIR generation output (maps, batches) default: user://output + /// ISLA_CHAT the batch chat namespace default: the tool's authoring chat /// /// ⚠ user:// RESOLUTION. Defaults sit under the project's own user data directory, which this /// project pins away from the old prototype's — see project.godot's user:// isolation block. @@ -60,6 +61,61 @@ namespace IslaApocalypse.Core /// Whether has been called. Tooling should assert this before a run. public static bool IsConfigured => _userDataDir != null; + // ═══ ⭐⭐ THE CHAT NAMESPACE (rivers/01) ═══════════════════════════════════════════════════ + // + // ═══ WHY BATCHES ARE NAMESPACED BY CHAT ═══ + // + // The batch prefix is the AUTHORING TASK NUMBER (see ), and task + // numbers restart at 00 in every new build chat. So a flat batches/ directory COLLIDES the + // moment a second chat exists: chat 1's `02_pass1_port` and chat 2's `02_curve_continuous` + // are both "batch 02", and nothing in either name says which chat made it. Measured on the + // real pile at rivers/01: 25 batches, FOUR colliding prefixes (02, 03, 04, 06), 13 folders + // belonging to chat 1 and 12 to chat 2 — separable only by SLUG, never by number. + // + // > ### ⚠ The slug is WHO IS RUNNING, not who authored. + // > A tool carries its authoring chat as its default so that re-running it reproduces its own + // > batch in place. A different chat re-running it for its own purposes sets ISLA_CHAT and + // > writes under its own namespace — which is also what stops an acceptance run from + // > OVERWRITING THE VERY ANCHOR IT IS CHECKING AGAINST. + // + // ⚠ REQUIRED, exactly like : with no slug set, + // throws rather than quietly writing to the un-namespaced root and re-creating the collision + // this exists to end. + + public const string ChatVar = "ISLA_CHAT"; + + private static string _chatSlug; + + /// + /// Set the chat namespace batches are written under. Called once at startup by every batch + /// tool, with its authoring chat as the fallback: ConfigureChat(EnvStr(ChatVar, "chat2")). + /// + /// + /// A short domain slug — `chat1`, `chat2`, `rivers`. ⚠ It becomes a single path SEGMENT, so + /// separators are refused rather than silently creating a nested tree nobody asked for. + /// + public static void ConfigureChat(string slug) + { + if (string.IsNullOrWhiteSpace(slug)) + throw new ArgumentException("A chat slug is required — batches are namespaced by chat.", nameof(slug)); + string t = slug.Trim(); + if (t.IndexOf('/') >= 0 || t.IndexOf('\\') >= 0 || t.IndexOf(Path.DirectorySeparatorChar) >= 0 + || t == "." || t == "..") + throw new ArgumentException( + $"Chat slug '{t}' is not a single path segment. The slug is ONE folder under batches/ — " + + "pass \"rivers\", not \"a/b\" or \"..\".", nameof(slug)); + _chatSlug = t; + } + + /// The chat namespace. ⚠ Throws if has not been called. + public static string ChatSlug => _chatSlug ?? throw new InvalidOperationException( + "CHAT SLUG NOT SET. Batches are namespaced by chat (batches//NN_slug/); a tool must call " + + "ToolingPaths.ConfigureChat(...) before composing a batch path. Writing to the un-namespaced root " + + $"is what collided task numbers across chats in the first place. (Override with {ChatVar}.) — rivers/01."); + + /// Whether has been called. + public static bool IsChatConfigured => _chatSlug != null; + /// The generation config file. Override: ISLA_CONFIG_PATH. public static string ConfigPath => Override(ConfigPathVar) ?? Path.Combine(UserDataDir, "config.json"); @@ -78,6 +134,13 @@ namespace IslaApocalypse.Core /// INDEX.md and a persistent scratch/. A/B comparisons are browsed by a human, and a flat /// directory of same-named PNGs is not browsable. /// + /// ⚠⚠ THIS IS THE ROOT, NOT A BATCH, AND THE DISTINCTION IS LOAD-BEARING. Batch WRITES go + /// through , which inserts the segment. Anchor + /// READS compose against THIS, so an anchor's source string must carry its own explicit + /// `chatN/` prefix (e.g. `"chat1/02_pass1_port"`). Changing only `BatchRoot` would namespace + /// every write and silently orphan every historical read — the exact trap rivers/01 had to + /// walk through, and why ShapingOracle.LoadAnchor now throws on a missing anchor. + /// /// ⚠ PROTECTED FROM DELETION. → . /// public static string BatchesRoot => Path.Combine(OutputDir, "batches"); @@ -90,7 +153,7 @@ namespace IslaApocalypse.Core public static string BatchScratch(string batchDir) => Path.Combine(batchDir, "scratch"); /// - /// ⭐ A BATCH ROOT: batches/<task>_<descriptor>/. + /// ⭐ A BATCH ROOT: batches/<chat>/<task>_<descriptor>/. /// /// ═══ ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER. ═══ /// @@ -107,6 +170,14 @@ namespace IslaApocalypse.Core /// /// The descriptor must NOT carry its own numeric prefix; that is the mistake this method /// exists to prevent, so it is refused rather than silently accepted. + /// + /// ═══ ⭐ THE <chat> SEGMENT (rivers/01) ═══ + /// + /// Prepended from , because the task-number prefix restarts at 00 in + /// every chat — see the note on . It is a SEPARATE segment and is + /// never folded into the descriptor: the prefix guard below fires on a descriptor starting + /// with digits, so passing `"chat2/12_drainage"` as a descriptor would be a different kind + /// of wrong. /// public static string BatchRoot(int taskNumber, string descriptor) { @@ -127,7 +198,7 @@ namespace IslaApocalypse.Core $"taskNumber and the descriptor WITHOUT one (e.g. \"review\", not \"04_review\") — " + "the prefix is composed here so it cannot drift.", nameof(descriptor)); - return Path.Combine(BatchesRoot, $"{taskNumber:D2}_{d}"); + return Path.Combine(BatchesRoot, ChatSlug, $"{taskNumber:D2}_{d}"); } /// @@ -137,6 +208,13 @@ namespace IslaApocalypse.Core public static string BatchDir(int taskNumber, string descriptor, long seed, string variant) => Path.Combine(BatchRoot(taskNumber, descriptor), $"{seed}_{variant}"); + /// + /// Resolve a HISTORICAL batch by its namespaced name, e.g. "chat1/02_pass1_port" — the + /// form every `ISLA_*_SOURCE` anchor default takes since rivers/01. Kept beside + /// so a READ and a WRITE are visibly two different operations. + /// + public static string BatchSource(string namespacedName) => Path.Combine(BatchesRoot, namespacedName); + private static string Override(string variable) { string v = Environment.GetEnvironmentVariable(variable); @@ -149,7 +227,8 @@ namespace IslaApocalypse.Core $"config : {ConfigPath}{Marker(ConfigPathVar)}\n" + $"blueprints: {BlueprintPath}{Marker(BlueprintPathVar)}\n" + $"output : {OutputDir}{Marker(OutputDirVar)}\n" + - $"batches : {BatchesRoot}"; + $"batches : {BatchesRoot}\n" + + $"chat : {(IsChatConfigured ? ChatSlug : "⚠ NOT SET")} → writes land under batches/{(IsChatConfigured ? ChatSlug : "")}/NN_slug/"; private static string Marker(string variable) => Override(variable) != null ? $" [{variable}]" : ""; } diff --git a/Tools/README.md b/Tools/README.md index ae92b7a..b962d3d 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -343,9 +343,15 @@ Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises. ### 4. Batch layout ``` -batches/_/_/ -batches/_/INDEX.md -batches/_/scratch/ ← persistent; never cleaned +batches//_/_/ +batches//_/INDEX.md +batches//_/scratch/ ← persistent; never cleaned + +⚠ is required (rivers/01): task numbers restart per chat, so a flat batches/ + collided across chat1 and chat2. Set by ToolingPaths.ConfigureChat(), overridable + with ISLA_CHAT. Historical anchor READS carry the prefix in their own source string + (e.g. "chat1/02_pass1_port"), because they compose against BatchesRoot, not BatchRoot. + → Tools/batches/README.md ``` > ### ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER. diff --git a/Tools/Scripts/CoastalFragmentTool.cs b/Tools/Scripts/CoastalFragmentTool.cs index 8517962..36bc2c3 100644 --- a/Tools/Scripts/CoastalFragmentTool.cs +++ b/Tools/Scripts/CoastalFragmentTool.cs @@ -38,7 +38,6 @@ namespace IslaApocalypse.Tools /// ISLA_SPECK_FRAC the speck-revert threshold, fraction of map area (default 2.5e-7 ≈ 4 cells at 4096) /// ISLA_PROBE=1 · ISLA_PROBE_FREQS · ISLA_PROBE_AMPS the probe sweep /// ISLA_FRAG_BITES=1 bites-only noise ([0,1]) instead of zero-mean ([-1,1]) - /// ISLA_SKIP_8K=1 (no 8192 check this batch — the 08 dump at 4096 is the baseline) /// public partial class CoastalFragmentTool : Node { @@ -86,6 +85,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 9); string descr = EnvStr("ISLA_BATCH", "coastal_fragment"); @@ -101,9 +104,7 @@ namespace IslaApocalypse.Tools float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs); float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore"); - string t08Level = EnvStr("ISLA_T08_LEVEL", "stretch_3"); // the 08 rung with stretch 2 + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -132,16 +133,26 @@ namespace IslaApocalypse.Tools TerrainGenConfig Cfg(int size, int seed, string label, float amp, float fq, float st, bool revert) { - return new TerrainGenConfig + // ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT: + // it was authored and judged before the shape family existed, and its regression checks + // hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the + // bare defaults family-ON, so without this pin every config here would silently acquire + // stretch + fragmentation and every anchor check would fail for a configuration reason. + // → TerrainGenConfig.WithFamilyOff(). + var c = new TerrainGenConfig { MapSize = size, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, - CoastShelf = false, Offshore = new OffshoreSettings(), - RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = speckFrac, - SouthStretch = st, - FragmentAmp = amp, FragmentFreqPerMapWidth = fq, FragmentBitesOnly = bitesOnly, - }; + RegionLabeling = true, + }.WithFamilyOff(); + // …then this tool's swept axes, AFTER the pin. (These were already explicit before + // rivers/01; the pin makes the tool's independence from the defaults total rather than + // field-by-field, so a future default can never leak in through a field nobody listed.) + c.SpeckRevert = revert; c.MinLandComponentFrac = speckFrac; + c.SouthStretch = st; + c.FragmentAmp = amp; c.FragmentFreqPerMapWidth = fq; c.FragmentBitesOnly = bitesOnly; + return c; } // ═══ PROBE ═══ @@ -186,21 +197,17 @@ namespace IslaApocalypse.Tools var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false); Pass1Result p1 = Topography.Generate(offCfg); var curveOff = offCfg.Clone(); curveOff.Curve = false; + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{seeds[0]}_full", "height.f32"); - hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); + hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", + Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump)); - // ⭐ a8 — frag OFF at the fixed stretch, revert OFF == the task-08 stretch-2 field (its dump at the plate size). - foreach (int seed in seeds) - { - string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_{t08Level}", "height.f32"); - if (File.Exists(t08Dump) && mapSize == 4096) - { - var c8 = Cfg(mapSize, seed, "t08", 0f, freq, stretch, false); - Pass2Result q8 = Shaping.Shape(Topography.Generate(c8), c8); - hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch {stretch:G3}, revert OFF == task-08 {t08Level} dump (the baseline) [{seed}]", q8.Height, HeightField.Load(t08Dump, mapSize), mapSize, t08Dump)); - } - else GD.Print($" a8 [{seed}]: ⚠ skipped — {(mapSize != 4096 ? "map size is not 4096" : $"no 08 dump at {t08Dump}")}"); - } + // ⚑ RETIRED at rivers/01 — a8, the frag-OFF baseline == `08_southern_stretch_explore`. + // An EXPLORATION ladder, and off-shape: the 08 batch's dump is at stretch 3, the locked + // shape is stretch 2. Nothing should be pinned to a rung of a ladder that was climbed to + // find a value, then superseded by the value it found. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. foreach (var c in hard) GD.Print(" " + c); } @@ -311,7 +318,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -324,11 +331,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); diff --git a/Tools/Scripts/CurveBaselineTool.cs b/Tools/Scripts/CurveBaselineTool.cs index 54c1835..e016df0 100644 --- a/Tools/Scripts/CurveBaselineTool.cs +++ b/Tools/Scripts/CurveBaselineTool.cs @@ -47,7 +47,7 @@ namespace IslaApocalypse.Tools /// ISLA_SHOWPIECE_SIZE larger confirmation profile (default 8192) /// ISLA_SHOWPIECE "0" to skip the big render /// ISLA_VARIANTS "0" to skip the per-seed variants (calibration-only probe) - /// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port") + /// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "chat1/02_pass1_port") /// ISLA_SKIP_RAW "1" to skip the .f32 dumps /// public partial class CurveBaselineTool : Node @@ -86,6 +86,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 1); string descr = EnvStr("ISLA_BATCH", "curve_baseline"); @@ -94,7 +98,7 @@ namespace IslaApocalypse.Tools int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; bool variants = EnvStr("ISLA_VARIANTS", "1") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; // ⚠ Composed by BatchRoot, never free-form — it refuses a descriptor carrying its own @@ -131,7 +135,7 @@ namespace IslaApocalypse.Tools foreach (int seed in seeds) { - var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed }; + var cfg = TerrainGenConfig.CalibrationPool(mapSize, seed); Pass1Result p1 = Topography.Generate(cfg); pass1[seed] = p1; rawPool.Accumulate(p1.Height, mapSize); @@ -289,8 +293,16 @@ namespace IslaApocalypse.Tools // ---- configs -------------------------------------------------------- + /// + /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored + /// and judged before the shape family existed, on the family-off distribution the knots are + /// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what + /// keeps this tool measuring the thing it was written to measure. + /// → . + /// private static TerrainGenConfig OffConfig(int mapSize, int seed) => - new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "curve_off", Curve = false, ShelfDetail = false }; + new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "curve_off", Curve = false, ShelfDetail = false } + .WithFamilyOff(); private static TerrainGenConfig OnConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a) => new TerrainGenConfig @@ -301,7 +313,7 @@ namespace IslaApocalypse.Tools // bit-for-bit. chat2/02 moved the config DEFAULT to Continuous for its exploration; // a control batch must not move with a default. (chat2/02.) CurveMode = CurveModeKind.Staircase, - }; + }.WithFamilyOff(); // ⭐ rivers/01 — see OffConfig: the staircase control is pre-family too /// /// Fraction of land below the CostaRica palette's third stop (0.310 raw). A blunt diff --git a/Tools/Scripts/CurveContinuousTool.cs b/Tools/Scripts/CurveContinuousTool.cs index 3fd1b31..9c44867 100644 --- a/Tools/Scripts/CurveContinuousTool.cs +++ b/Tools/Scripts/CurveContinuousTool.cs @@ -48,8 +48,7 @@ namespace IslaApocalypse.Tools /// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below) /// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192) /// ISLA_SHOWPIECE "0" to skip it - /// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port") - /// ISLA_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline") + /// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "chat1/02_pass1_port") /// ISLA_SKIP_RAW "1" to skip the .f32 dumps /// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30) /// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4) @@ -91,6 +90,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 2); string descr = EnvStr("ISLA_BATCH", "curve_continuous"); @@ -98,8 +101,7 @@ namespace IslaApocalypse.Tools int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form @@ -128,7 +130,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int seed in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed)); pass1[seed] = p1; rawPool.Accumulate(p1.Height, mapSize); GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms"); @@ -187,15 +189,20 @@ namespace IslaApocalypse.Tools { Pass1Result pp1 = pass1[primary]; - // (a1) curve off == Phase 1's own dump. + // (a1) curve off == Phase 1's own dump. ⭐ KEPT at rivers/01: the family-off pass-1 guard, + // the last link between today's generator and the Phase-1 port. The config is pinned + // family-off so it still means what it says. ⚠ A missing dump now THROWS. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump", - offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump)); + offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump)); - // (a2) staircase == task 01's own dump — the control is the control. - string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32"); - hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump", - results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump)); + // ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control. + // The staircase curve is SUPERSEDED by the continuous grade (→ D-062). A control that + // reproduces a curve nothing ships is scaffolding, and holding it green cost a + // 4-variant batch run to prove a mode no design doc describes any more. + // The dump is NOT deleted (file-safety; it is regenerable and it is the record of what + // was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can + // pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4. // (b) classify == raw, every seed × every variant. long bFail = 0; @@ -340,7 +347,11 @@ namespace IslaApocalypse.Tools LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f), ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f), SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f), - }; + } + // ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/02 is CURVE development, measured on + // the family-off distribution the knots are percentiles of; the re-baseline flipped the bare + // defaults family-ON. → TerrainGenConfig.WithFamilyOff(). + .WithFamilyOff(); // ---- output ----------------------------------------------------------- diff --git a/Tools/Scripts/DrainageTool.cs b/Tools/Scripts/DrainageTool.cs index fceb5d9..1d97c10 100644 --- a/Tools/Scripts/DrainageTool.cs +++ b/Tools/Scripts/DrainageTool.cs @@ -54,6 +54,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 12); string descr = EnvStr("ISLA_BATCH", "drainage_analysis"); @@ -62,7 +66,7 @@ namespace IslaApocalypse.Tools int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipT11 = EnvStr("ISLA_SKIP_T11_CHECK", "0") == "1"; - string t11Source = EnvStr("ISLA_T11_SOURCE", "11_erosion"); + string t11Source = EnvStr("ISLA_T11_SOURCE", "chat2/11_erosion"); string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -77,7 +81,11 @@ namespace IslaApocalypse.Tools GD.Print("=================================================================="); GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}"); GD.Print($"seeds : {string.Join(", ", seeds)}"); - GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON (faithful tune) — the task-11 erosion_on field"); + // ⚠ rivers/01: the shape AND erosion now come from the defaults, so both are asserted before + // anything generates. A drift here would silently re-baseline every river measurement. + TerrainShapeV1.Assert("DrainageTool"); + TerrainShapeV1.AssertErosionDefaultOn("DrainageTool"); + GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON by default (faithful tune) — the task-11 erosion_on field"); GD.Print($"params : endorheic depth ≥ {dp.EndorheicMinDepthM} m, area ≥ {dp.EndorheicMinAreaPx}, inflow ≥ {dp.EndorheicMinInflowPx}, max {dp.EndorheicMaxCount} · trunks {dp.TrunkCount} sep {dp.MinOutletSeparationPx} px · giants {dp.GiantCount} · stem ≥ {dp.StemMinAccPx} · tributary ≥ {dp.TributaryMinAccPx} (max {dp.TributaryMaxPerTrunk}) · exit grade {dp.ExitGradeMin} m/px over {dp.ExitWindowPx} px"); GD.Print($"batch : {batchRoot}"); GD.Print("=================================================================="); @@ -88,14 +96,15 @@ namespace IslaApocalypse.Tools TerrainGenConfig Cfg(int size, int seed) { + // ⭐ rivers/01: THE SHAPE AND EROSION COME FROM THE BARE DEFAULTS. `TerrainShapeV1.Apply(c)` + // and `c.Erosion = true` used to sit here; both are now what `new TerrainGenConfig()` + // carries. Only the CURVE (measured this run) is set. var c = new TerrainGenConfig { MapSize = size, Seed = seed, VariantLabel = "drainage", Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, }; - TerrainShapeV1.Apply(c); - c.Erosion = true; // the faithful tune — the defaults return c; } @@ -114,15 +123,16 @@ namespace IslaApocalypse.Tools Pass2Result p2 = ero.Shaped; GD.Print($" terrain ready ({p1.ElapsedMs} ms pass 1, erosion {ero.Ms / 1000.0:F1} s)"); + // ⭐ a11 — THE EROSION ACCEPTANCE ANCHOR (rivers/01 keeps this one). Since the re-baseline + // the whole chain — shape AND erosion — comes from the bare defaults, so this is the + // standing proof that the terrain every river measurement rests on has not moved. + // ⚠ A missing dump now THROWS (ShapingOracle.LoadAnchor) instead of skipping silently. if (!skipT11) { string dump = Path.Combine(ToolingPaths.BatchesRoot, t11Source, $"{seed}_erosion_on", "height.f32"); - if (File.Exists(dump) && mapSize == 8192) - { - var a11 = ShapingOracle.DumpRegression("a11", $"the eroded render field == the task-11 erosion_on dump (the terrain the developer saw) [{seed}]", p2.Height, HeightField.Load(dump, mapSize), mapSize, dump); - hard.Add(a11); GD.Print(" " + a11); - } - else GD.Print($" a11 [{seed}]: ⚠ skipped — {(mapSize != 8192 ? "map size is not the 11 batch's 8192" : $"no dump at {dump}")}"); + var a11 = ShapingOracle.DumpRegression("a11", $"the eroded render field from the BARE DEFAULTS == the task-11 erosion_on dump (the terrain the developer saw) [{seed}]", + p2.Height, ShapingOracle.LoadAnchor("a11", "ISLA_T11_SOURCE", dump, mapSize), mapSize, dump); + hard.Add(a11); GD.Print(" " + a11); } // ⭐ THE OCEAN IDENTITY — from the region layer, on the CLASSIFY field. @@ -284,7 +294,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -297,11 +307,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); diff --git a/Tools/Scripts/ErosionTool.cs b/Tools/Scripts/ErosionTool.cs index 88507f2..d19babf 100644 --- a/Tools/Scripts/ErosionTool.cs +++ b/Tools/Scripts/ErosionTool.cs @@ -7,33 +7,6 @@ using IslaApocalypse.Core; namespace IslaApocalypse.Tools { - /// - /// ⭐ THE LOCKED SHAPE — `terrain-shape-v1` (chat2/10 gallery-confirmed): the continuous curve + the - /// frag_4 organic islands. Every later pass (erosion, rivers, …) starts from exactly these values, - /// pinned here once so no tool re-types them. - /// - public static class TerrainShapeV1 - { - public const float FragmentAmp = 0.5f, FragmentFreq = 12f, BandCentre = 0.66f, BandHalfWidth = 0.18f; - public const bool BitesOnly = false; - public const float Stretch = 2f, BandStart = 0.70f, BandFeather = 0.05f; - public const bool StretchSinker = true; - public const float SpeckFrac = 2.5e-7f; - - /// Apply the locked shape to a config (curve settings are the caller's — they come from the calibration). - public static void Apply(TerrainGenConfig c) - { - c.CoastShelf = false; c.Offshore = new OffshoreSettings(); - c.RegionLabeling = true; c.SpeckRevert = true; c.MinLandComponentFrac = SpeckFrac; - c.SouthStretch = Stretch; c.SouthBandStartFrac = BandStart; c.SouthBandFeatherFrac = BandFeather; c.StretchSinker = StretchSinker; - c.FragmentAmp = FragmentAmp; c.FragmentFreqPerMapWidth = FragmentFreq; - c.FragmentBandCentre = BandCentre; c.FragmentBandHalfWidth = BandHalfWidth; c.FragmentBitesOnly = BitesOnly; - } - - public static string Describe() => - $"terrain-shape-v1: frag amp {FragmentAmp} freq {FragmentFreq} window {BandCentre}±{BandHalfWidth} · stretch {Stretch} (band {BandStart}/{BandFeather}, sinker stretched) · speck revert {SpeckFrac:G2} · offshore OFF · shelf OFF · labeling ON"; - } - /// /// ⭐ THE EROSION BATCH (chat2/11) — the faithful droplet erosion on the locked shape, judged across /// seeds, erosion OFF vs ON. 4 seeds from the task-10 gallery × {off, on} = 8 fields at showpiece @@ -85,6 +58,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 11); string descr = EnvStr("ISLA_BATCH", "erosion"); @@ -93,7 +70,7 @@ namespace IslaApocalypse.Tools int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipTag = EnvStr("ISLA_SKIP_TAG_CHECK", "0") == "1"; - string t10Source = EnvStr("ISLA_T10_SOURCE", "10_frag4_seed_gallery"); + string t10Source = EnvStr("ISLA_T10_SOURCE", "chat2/10_frag4_seed_gallery"); string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -107,6 +84,7 @@ namespace IslaApocalypse.Tools GD.Print("=================================================================="); GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}"); GD.Print($"seeds : {string.Join(", ", seeds)}"); + TerrainShapeV1.Assert("ErosionTool"); // ⚠ rivers/01: refuse to render if the defaults drifted off the locked shape GD.Print($"shape : {TerrainShapeV1.Describe()}"); GD.Print($"batch : {batchRoot}"); GD.Print("=================================================================="); @@ -117,13 +95,17 @@ namespace IslaApocalypse.Tools TerrainGenConfig Cfg(int size, int seed, string label, bool erosion) { + // ⭐ rivers/01: THE SHAPE COMES FROM THE BARE DEFAULTS. `TerrainShapeV1.Apply(c)` used to + // sit here; the locked shape is now what `new TerrainGenConfig()` produces, so stamping + // a preset on top would MASK a default drift instead of catching it. Only the CURVE + // (measured this run) and the per-variant erosion flag are set. + // → TerrainShapeV1.Assert(), called before any generation below. var c = new TerrainGenConfig { MapSize = size, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, }; - TerrainShapeV1.Apply(c); c.Erosion = erosion; c.ErosionDropletCount = EnvInt("ISLA_ERO_COUNT", c.ErosionDropletCount); c.ErosionDropletLifetime = EnvInt("ISLA_ERO_LIFETIME", c.ErosionDropletLifetime); @@ -156,12 +138,15 @@ namespace IslaApocalypse.Tools var cOff = Cfg(mapSize, seed, "erosion_off", false); Pass1Result p1Off = Topography.Generate(cOff); Pass2Result p2Off = Shaping.Shape(p1Off, cOff); + // ⭐ a10 — THE SHAPE ACCEPTANCE ANCHOR (rivers/01 keeps this one). Since the re-baseline + // `p2Off` is generated from the BARE DEFAULTS, so this check is now the standing proof + // that the defaults still reproduce `terrain-shape-v1`. + // ⚠ A missing dump now THROWS (ShapingOracle.LoadAnchor) instead of skipping silently. if (!skipTag) { string dump = Path.Combine(ToolingPaths.BatchesRoot, t10Source, $"{seed}", "height.f32"); - if (File.Exists(dump) && mapSize == 8192) - hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF == terrain-shape-v1 (the task-10 gallery dump) [{seed}]", p2Off.Height, HeightField.Load(dump, mapSize), mapSize, dump)); - else GD.Print($" a10 [{seed}]: ⚠ skipped — {(mapSize != 8192 ? "map size is not the gallery's 8192" : $"no gallery dump at {dump}")}"); + hard.Add(ShapingOracle.DumpRegression("a10", $"erosion OFF from the BARE DEFAULTS == terrain-shape-v1 (the task-10 gallery dump) [{seed}]", + p2Off.Height, ShapingOracle.LoadAnchor("a10", "ISLA_T10_SOURCE", dump, mapSize), mapSize, dump)); } Image reliefOff = ReliefRenderer.Render(p2Off.Height, mapSize, look); Image shadeOff = ShadeRenderer.Render(p2Off.Height, mapSize, sea, ShadeZ, look.LightAzimuth, look.LightAltitude); @@ -281,7 +266,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -294,11 +279,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); diff --git a/Tools/Scripts/FragGalleryTool.cs b/Tools/Scripts/FragGalleryTool.cs index 143d6ce..6a68237 100644 --- a/Tools/Scripts/FragGalleryTool.cs +++ b/Tools/Scripts/FragGalleryTool.cs @@ -22,7 +22,7 @@ namespace IslaApocalypse.Tools /// ISLA_MAPSIZE gallery size (default 8192) /// ISLA_CALIB_SIZE curve calibration size (default 2048) /// ISLA_SEEDS the gallery seeds (default: the 2 anchors + 6 fresh below) - /// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 bit-identity check against the 09 frag_4 dumps + /// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 interior-locked invariant check /// public partial class FragGalleryTool : Node { @@ -35,17 +35,22 @@ namespace IslaApocalypse.Tools /// ⚠ Task 01's pool, verbatim — the curve's identity. private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; - // ═══ THE FROZEN frag_4 SETTING — every value pinned explicitly (chat2/09 batch, level 4) ═══ - private const float FrozenFragmentAmp = 0.5f; - private const float FrozenFragmentFreq = 12f; - private const float FrozenBandCentre = 0.66f; - private const float FrozenBandHalfWidth = 0.18f; - private const bool FrozenBitesOnly = false; - private const float FrozenStretch = 2f; - private const float FrozenBandStart = 0.70f; - private const float FrozenBandFeather = 0.05f; - private const bool FrozenStretchSinker = true; - private const float FrozenSpeckFrac = 2.5e-7f; // 09's low speck revert (≈ 4 cells at 4096, ≈ 17 at 8192) + // ═══ THE FROZEN frag_4 SETTING (chat2/09 batch, level 4) ═══ + // + // ⭐ rivers/01: these were the ONLY home of the locked values. They now ALIAS + // `TerrainShapeV1`, which is itself the assertion target for `TerrainGenConfig`'s defaults — + // so the chain is: bare defaults → asserted against TerrainShapeV1 → printed here. One value, + // one place, and a throw if the generator ever stops agreeing with it. + private const float FrozenFragmentAmp = TerrainShapeV1.FragmentAmp; + private const float FrozenFragmentFreq = TerrainShapeV1.FragmentFreq; + private const float FrozenBandCentre = TerrainShapeV1.BandCentre; + private const float FrozenBandHalfWidth = TerrainShapeV1.BandHalfWidth; + private const bool FrozenBitesOnly = TerrainShapeV1.BitesOnly; + private const float FrozenStretch = TerrainShapeV1.Stretch; + private const float FrozenBandStart = TerrainShapeV1.BandStart; + private const float FrozenBandFeather = TerrainShapeV1.BandFeather; + private const bool FrozenStretchSinker = TerrainShapeV1.StretchSinker; + private const float FrozenSpeckFrac = TerrainShapeV1.SpeckFrac; // ≈ 4 cells at 4096, ≈ 17 at 8192 private const int DefaultMapSize = 8192; private const int DefaultCalibSize = 2048; @@ -79,6 +84,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 10); string descr = EnvStr("ISLA_BATCH", "frag4_seed_gallery"); @@ -87,9 +96,7 @@ namespace IslaApocalypse.Tools int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool skipAnchor = EnvStr("ISLA_SKIP_ANCHOR_CHECK", "0") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore"); - string t09Source = EnvStr("ISLA_T09_SOURCE", "09_coastal_fragment"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); var seeds = new List(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); } var anchorSet = new HashSet(AnchorSeeds); @@ -118,17 +125,40 @@ namespace IslaApocalypse.Tools var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors); GD.Print($" {knots}"); - TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true) => new TerrainGenConfig + // ⭐⭐ rivers/01: THE FROZEN SETTING IS NOW THE BARE DEFAULT. + // + // Every `Frozen*` constant above was re-homed into `TerrainGenConfig`'s defaults by the + // re-baseline, so this helper no longer SETS the shape — it only ABLATES it, for the + // family-off halves of the regression checks. That is the whole point: this batch is the + // `terrain-shape-v1` acceptance, and it can only prove the defaults reproduce the locked + // shape if it reads them instead of re-stating them. + // + // ⚠ The `frag` / `revert` / `stretch` flags are ABLATIONS ONLY. All three true = the bare + // default = the locked shape; `TerrainShapeV1.Assert` below is what keeps that claim + // honest if a default ever drifts. + TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true) { - MapSize = size, Seed = seed, VariantLabel = label, - Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, - Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, - CoastShelf = false, Offshore = new OffshoreSettings(), - RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = FrozenSpeckFrac, - SouthStretch = stretch ? FrozenStretch : 0f, SouthBandStartFrac = FrozenBandStart, SouthBandFeatherFrac = FrozenBandFeather, StretchSinker = FrozenStretchSinker, - FragmentAmp = frag ? FrozenFragmentAmp : 0f, FragmentFreqPerMapWidth = FrozenFragmentFreq, - FragmentBandCentre = FrozenBandCentre, FragmentBandHalfWidth = FrozenBandHalfWidth, FragmentBitesOnly = FrozenBitesOnly, - }; + var c = new TerrainGenConfig + { + MapSize = size, Seed = seed, VariantLabel = label, + Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, + Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, + }; + if (!stretch) c.SouthStretch = 0f; + if (!frag) c.FragmentAmp = 0f; + if (!revert) c.SpeckRevert = false; + // This batch is render-only shape: erosion is a later pass and never ran here. + c.Erosion = false; + return c; + } + + // ⚠⚠ THE DEFAULT-DRIFT GUARD (rivers/01). The gallery above stopped STATING the locked shape + // and started READING it. If a default ever moves, every render silently moves with it and + // the batch still "passes" — so the claim is asserted, loudly, before a pixel is drawn. + // The `Frozen*` constants below are unchanged in value; their ROLE flipped from source to + // assertion target. → Tools/Scripts/TerrainShapeV1.cs + TerrainShapeV1.Assert("FragGallery"); + GD.Print($" defaults : ✅ {TerrainShapeV1.Describe()}"); // ═══ 1. THE ORACLE — no code change, same setting ═══ GD.Print($"\n--- 1. ORACLE: the setting is the 09 frag_4 setting, and nothing upstream moved ---"); @@ -137,25 +167,32 @@ namespace IslaApocalypse.Tools var offCfg = Frozen(calibSize, AnchorSeeds[0], "off", frag: false, revert: false, stretch: false); Pass1Result p1 = Topography.Generate(offCfg); var curveOff = offCfg.Clone(); curveOff.Curve = false; + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{AnchorSeeds[0]}_full", "height.f32"); - hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); + hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", + Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump)); + + // ⚑ RETIRED at rivers/01 — a8 (`08_southern_stretch_explore`) and a9 (`09_coastal_fragment`). + // Both are EXPLORATION ladders this gallery was built to CONCLUDE: chat2/09 climbed the + // fragmentation ladder, chat2/10 froze rung 4 across 8 seeds, and the developer tagged the + // result `terrain-shape-v1`. Since rivers/01 that frozen setting IS the bare default, and + // `TerrainShapeV1.Assert` + a10 assert it directly — asserting it a third time through the + // rungs it was chosen from is circular, and 08's dump is at stretch 3 (off-shape) besides. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. if (!skipAnchor) { foreach (int seed in AnchorSeeds) { - // ⭐ a9 — the frozen setting at the 09 batch's size reproduces the 09 frag_4 field bit for bit. - string t09Dump = Path.Combine(ToolingPaths.BatchesRoot, t09Source, $"{seed}_frag_4", "height.f32"); + // ⭐ r KEPT and SELF-ANCHORED — the interior-locked invariant is coastal fragmentation's + // load-bearing claim (it must touch the coastal window and NOTHING else), and it needs + // no external dump: both fields are generated here, from the bare defaults and from the + // same defaults with frag ablated off. That is what let 08 and 09 retire intact. var c9 = Frozen(AnchorCheckSize, seed, "frag_4"); Pass1Result q9p = Topography.Generate(c9); - Pass2Result q9 = Shaping.Shape(q9p, c9); - hard.Add(ShapingOracle.DumpRegression("a9", $"frozen frag_4 at {AnchorCheckSize} == task-09 frag_4 dump [{seed}] (no code change, same setting)", q9.Height, HeightField.Load(t09Dump, AnchorCheckSize), AnchorCheckSize, t09Dump)); - - // a8 — the stretch-2, frag-off baseline still equals the 08 field, and the interior is still locked against it. - string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_stretch_3", "height.f32"); var c8 = Frozen(AnchorCheckSize, seed, "t08", frag: false, revert: false); Pass1Result q8p = Topography.Generate(c8); - hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch 2 at {AnchorCheckSize} == task-08 stretch_3 dump [{seed}]", Shaping.Shape(q8p, c8).Height, HeightField.Load(t08Dump, AnchorCheckSize), AnchorCheckSize, t08Dump)); var r = ShapingOracle.InteriorLocked(q8p, q9p, FrozenBandCentre, FrozenBandHalfWidth); r.Name += $" [{seed}, {AnchorCheckSize}]"; hard.Add(r); } // determinism at the check size @@ -203,7 +240,7 @@ namespace IslaApocalypse.Tools foreach (var c in perSeed) if (!c.Passed) GD.PrintErr(" " + c); WriteTable(batchRoot, mapSize, rows, big, speckCells); - WriteIndex(batchRoot, mapSize, calibSize, rows, big, speckCells, hard, perSeed, allOk); + WriteIndex(batchRoot, task, mapSize, calibSize, rows, big, speckCells, hard, perSeed, allOk); GD.Print("\n=================================================================="); GD.Print($" DONE — {batchRoot}"); @@ -262,7 +299,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -275,11 +312,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); @@ -356,11 +396,11 @@ namespace IslaApocalypse.Tools WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString()); } - private static void WriteIndex(string batchRoot, int mapSize, int calibSize, List rows, long big, long speckCells, + private static void WriteIndex(string batchRoot, int task, int mapSize, int calibSize, List rows, long big, long speckCells, List hard, List perSeed, bool allOk) { var sb = new StringBuilder(); - sb.AppendLine($"# Batch 10 — frag_4 seed gallery: does the look generalize? (render-only, {mapSize})"); + sb.AppendLine($"# Batch {task:D2} — frag_4 seed gallery: does the look generalize? (render-only, {mapSize})"); sb.AppendLine(); sb.AppendLine("**A contact sheet, not a tune.** Every plate is the SAME setting — chat2/09's `frag_4`, frozen — across the two"); sb.AppendLine("seeds it was judged on (⭐ anchors) and six fresh seeds chosen before any render. The question: does a coherent"); @@ -370,7 +410,13 @@ namespace IslaApocalypse.Tools sb.AppendLine(); sb.AppendLine($"`FragmentAmp {FrozenFragmentAmp}` · `FragmentFreqPerMapWidth {FrozenFragmentFreq}` · window `{FrozenBandCentre} ± {FrozenBandHalfWidth}` · bites-only `{FrozenBitesOnly}` · " + $"`SouthStretch {FrozenStretch}` (band `{FrozenBandStart}` / feather `{FrozenBandFeather}`, sinker stretched `{FrozenStretchSinker}`) · speck revert < {speckCells} cells (`{FrozenSpeckFrac:G2}` of the map) · " + - "offshore OFF · shelf OFF · region labeling ON · the tagged curve (calibrated on task 01's pool at " + calibSize + "). Pinned explicitly in `FragGalleryTool` — nothing is left to a default."); + "offshore OFF · shelf OFF · region labeling ON · the tagged curve (calibrated on task 01's pool at " + calibSize + ")."); + sb.AppendLine(); + sb.AppendLine("> ### ⭐ Since rivers/01, this setting IS the bare `TerrainGenConfig` default — it is not stated here, it is READ."); + sb.AppendLine("> That is what makes this batch the acceptance for the re-baseline rather than a restatement of it: if a default"); + sb.AppendLine("> ever drifts, `TerrainShapeV1.Assert` refuses the run instead of rendering a gallery that would look right and"); + sb.AppendLine("> mean nothing. The curve calibration pool is pinned FAMILY-OFF (`TerrainGenConfig.WithFamilyOff`), which is what"); + sb.AppendLine("> keeps the knots — and therefore these renders — bit-identical across the flip."); sb.AppendLine(); sb.AppendLine("## ⭐ The contact sheet"); sb.AppendLine(); diff --git a/Tools/Scripts/MountainRestoreTool.cs b/Tools/Scripts/MountainRestoreTool.cs index afb4d56..af5e70c 100644 --- a/Tools/Scripts/MountainRestoreTool.cs +++ b/Tools/Scripts/MountainRestoreTool.cs @@ -44,7 +44,7 @@ namespace IslaApocalypse.Tools /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn /// /// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE - /// ISLA_PHASE1_SOURCE (default "02_pass1_port") · ISLA_T01_SOURCE (default "01_curve_baseline") + /// ISLA_PHASE1_SOURCE (default "chat1/02_pass1_port") /// ISLA_SKIP_RAW /// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35) /// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5) @@ -77,6 +77,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 3); string descr = EnvStr("ISLA_BATCH", "mountain_restore"); @@ -84,8 +88,7 @@ namespace IslaApocalypse.Tools int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f); float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f); @@ -114,7 +117,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int seed in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed)); pass1[seed] = p1; rawPool.Accumulate(p1.Height, mapSize); } @@ -219,12 +222,17 @@ namespace IslaApocalypse.Tools var hard = new List(); var soft = new List(); + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). + // ⚠ A missing dump now THROWS instead of skipping silently. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32"); - string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump", - offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump)); - hard.Add(ShapingOracle.DumpRegression("a2", "staircase == task-01 curve_on .f32 dump", - results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump)); + offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump)); + + // ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control. + // Superseded by the continuous grade (→ D-062) — see CurveContinuousTool for the full note. + // The dump is NOT deleted (file-safety; it is regenerable and it is the record of what + // was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can + // pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4. long bFail = 0; foreach (int seed in seeds) @@ -335,13 +343,20 @@ namespace IslaApocalypse.Tools GetTree().Quit(hardOk ? 0 : 3); } + /// + /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored + /// and judged before the shape family existed, on the family-off distribution the knots are + /// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what + /// keeps this tool measuring the thing it was written to measure. + /// → . + /// private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label) => new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, Knots = k, Anchors = a, LowlandCeilingM = 30f, - }; + }.WithFamilyOff(); // ---- output --------------------------------------------------------- diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs index 5417d50..62bd0a0 100644 --- a/Tools/Scripts/OffshoreIslandsTool.cs +++ b/Tools/Scripts/OffshoreIslandsTool.cs @@ -57,8 +57,7 @@ namespace IslaApocalypse.Tools /// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides) /// ISLA_OFF_FREQ / ISLA_OFF_CORE / ISLA_OFF_SHARP / ISLA_OFF_CREST the shape (probe overrides) /// ISLA_TABLE_ONLY=1 probe: diagnosis + count table only (no regressions, no plates) - /// ISLA_SKIP_8K=1 skip the 8192 regression against the 04 gallery dump (a4) - /// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches + /// ISLA_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port") /// public partial class OffshoreIslandsTool : Node { @@ -77,7 +76,6 @@ namespace IslaApocalypse.Tools private const int DefaultMapSize = 4096; private const int DefaultCalibSize = 2048; - private const int GallerySize = 8192; // the 04 gallery's render size /// The consistency targets the table is read against: "a couple north, a few south". private const int TargetNorth = 2, TargetSouth = 3; @@ -112,6 +110,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 6); string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune"); @@ -121,12 +123,9 @@ namespace IslaApocalypse.Tools int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0); - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); - string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; - bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1"; string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -185,14 +184,17 @@ namespace IslaApocalypse.Tools var curveOff = offCfg.Clone(); curveOff.Curve = false; Pass2Result pOff = Shaping.Shape(p1, curveOff); + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump", - pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); + pOff.Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump)); - Pass2Result pRest = Shaping.Shape(p1, offCfg); - string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); - hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)", - pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump)); + // ⚑ RETIRED at rivers/01 — a3, `continuous_restored` == `03_mountain_restore`. + // A curve-development intermediate: it proved the chat2/03 climb restoration against + // chat2/02. Both are upstream of the locked shape, and `terrain-shape-v1` (a10) now + // asserts the whole chain end-to-end — subsuming it. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. // Shelf ON, islets OFF: land must be bit-identical (the shelf touches only sea). var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only"; @@ -205,22 +207,12 @@ namespace IslaApocalypse.Tools // ⭐ a4 — offshore OFF at the 04 gallery's size == the terrain-curve-v1 tag's OWN output. // The literal "offshore-off is bit-identical to terrain-curve-v1", at full size. - if (!skip8k) - { - string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); - if (File.Exists(t04Dump)) - { - GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …"); - var gCfg = BaseConfig(GallerySize, plateSeed, knots, anchors, calibration, "off"); - Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg); - var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump", - pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump); - hard.Add(a4); - GD.Print(" " + a4); - } - else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}"); - } - else GD.Print(" a4: skipped (ISLA_SKIP_8K)"); + // ⚑ RETIRED at rivers/01 — a4, offshore-OFF at 8192 == `04_seed_gallery` (`terrain-curve-v1`). + // The PRE-FAMILY committed curve. The locked shape is family-ON, so this dump is a + // baseline the generator is deliberately no longer on; `a10` replaced it as the 8192 + // acceptance. (It also cost an 8192 generation on every run of this tool.) + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. } // ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══ @@ -402,7 +394,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF by default + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -416,11 +408,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); @@ -436,14 +431,21 @@ namespace IslaApocalypse.Tools return (knots, cal, pass1); } + /// + /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT + /// (chat2/05–06): authored and judged before the shape family existed, and its regressions hold + /// pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the bare defaults + /// family-ON, so without every config here would + /// silently acquire stretch + fragmentation and the anchor checks would fail for a configuration + /// reason, not a regression. The variants re-enable offshore explicitly, after the pin. + /// private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, ClimbCalibration cal, string label) => new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f, - CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on - }; + }.WithFamilyOff(); // ⭐ shelf / islets / speck / stretch / frag / erosion all OFF — pinned // ---- output ----------------------------------------------------------- diff --git a/Tools/Scripts/RegionLabelingTool.cs b/Tools/Scripts/RegionLabelingTool.cs index 2c44186..5687ed1 100644 --- a/Tools/Scripts/RegionLabelingTool.cs +++ b/Tools/Scripts/RegionLabelingTool.cs @@ -42,8 +42,7 @@ namespace IslaApocalypse.Tools /// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands) /// ISLA_THR_LOW / ISLA_THR_MID / ISLA_THR_HIGH thresholds, fraction of map area (probe overrides) /// ISLA_TABLE_ONLY=1 probe: table only (no regressions, no plates) - /// ISLA_SKIP_8K=1 skip the 8192 regression (a4) - /// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches + /// ISLA_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port") /// public partial class RegionLabelingTool : Node { @@ -57,7 +56,6 @@ namespace IslaApocalypse.Tools private const int DefaultMapSize = 4096; private const int DefaultCalibSize = 2048; - private const int GallerySize = 8192; public override void _Ready() { @@ -89,6 +87,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 7); string descr = EnvStr("ISLA_BATCH", "region_labeling"); @@ -98,13 +100,9 @@ namespace IslaApocalypse.Tools int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); int secondEnv = EnvInt("ISLA_SECOND_SEED", 0); - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); - string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery"); - string t06Source = EnvStr("ISLA_T06_SOURCE", "06_offshore_organic_tune"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; - bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1"; var levels = new List { @@ -141,7 +139,14 @@ namespace IslaApocalypse.Tools TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac) { - var c = BaseConfig(size, seed, knots, anchors, calibration, label); + // ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT: + // it was authored and judged before the shape family existed, and its regression checks + // hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the + // bare defaults family-ON, so without this pin every config here would silently acquire + // stretch + fragmentation and every anchor check would fail for a configuration reason. + // → TerrainGenConfig.WithFamilyOff(). + var c = BaseConfig(size, seed, knots, anchors, calibration, label).WithFamilyOff(); + // …then this tool's own axes, AFTER the pin (the pin would otherwise clear them). if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); } c.RegionLabeling = true; c.SpeckRevert = revertOn; @@ -159,26 +164,17 @@ namespace IslaApocalypse.Tools var curveOff = offCfg.Clone(); curveOff.Curve = false; Pass2Result pOff = Shaping.Shape(p1, curveOff); + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF, revert OFF (labeling on) == Phase-1 .f32 dump", - pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); + pOff.Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump)); - Pass2Result pRest = Shaping.Shape(p1, offCfg); - string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); - float[,] t03 = HeightField.Load(t03Dump, calibSize); - hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump", - pRest.Height, t03, calibSize, t03Dump)); - - // Informational: offshore OFF, revert ON — how many NATURAL speck cells the revert removes - // from the bare field. Allowed to differ (the revert may change terrain); reported, not asserted. - var revCfg = Cfg(calibSize, plateSeed, "off_revert", offshoreOn: false, revertOn: true, mid.Frac); - Pass1Result p1Rev = Topography.Generate(revCfg); - Pass2Result pRev = Shaping.Shape(p1Rev, revCfg); - var info = ShapingOracle.DumpRegression("a3r", "(informational) offshore OFF, revert ON at threshold_mid vs task-03 dump — the natural specks removed", pRev.Height, t03, calibSize, t03Dump); - info.Detail = (info.Passed ? "no natural speck below the threshold on this seed — " : "") + info.Detail + - $" · reverted {p1Rev.RegionLedger.RevertedComponents} natural components / {p1Rev.RegionLedger.RevertedCells:N0} cells"; - info.Passed = true; - hard.Add(info); + // ⚑ RETIRED at rivers/01 — a3 and a3r, both against `03_mountain_restore`. + // A curve-development intermediate, subsumed by the `terrain-shape-v1` acceptance (a10). + // a3r was informational only, and its subject — how many natural specks the revert takes — + // is now reported by the region ledger every run, with the revert ON by default. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only"; Pass1Result p1Shelf = Topography.Generate(shelfCfg); @@ -188,34 +184,13 @@ namespace IslaApocalypse.Tools hard.Add(ShapingOracle.CentreIsLand(p1)); foreach (var c in hard) GD.Print(" " + c); - if (!skip8k) - { - string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); - if (File.Exists(t04Dump)) - { - GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …"); - var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac); - Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg); - var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF, revert OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump", - pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump); - hard.Add(a4); GD.Print(" " + a4); - } - else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}"); - } - else GD.Print(" a4: skipped (ISLA_SKIP_8K)"); - - // ⭐ a6 — labeling ON, revert OFF, on the chat2/06 preset: bit-identical to the 06 batch's - // render field. Labeling is pure analysis; only the revert may change terrain. - string t06Dump = Path.Combine(ToolingPaths.BatchesRoot, t06Source, $"{plateSeed}_density_mid", "height.f32"); - if (File.Exists(t06Dump) && mapSize == 4096) - { - var c6 = Cfg(mapSize, plateSeed, "density_mid", offshoreOn: true, revertOn: false, mid.Frac); - Pass2Result p6 = Shaping.Shape(Topography.Generate(c6), c6); - var a6 = ShapingOracle.DumpRegression("a6", "offshore density_mid ON, labeling ON, revert OFF == task-06 .f32 dump (labeling is pure analysis)", - p6.Height, HeightField.Load(t06Dump, mapSize), mapSize, t06Dump); - hard.Add(a6); GD.Print(" " + a6); - } - else GD.Print($" a6: ⚠ skipped — {(mapSize != 4096 ? "map size is not the 06 batch's 4096" : $"no 06 dump at {t06Dump}")}"); + // ⚑ RETIRED at rivers/01 — a4 (`04_seed_gallery`) and a6 (`06_offshore_organic_tune`). + // a4 held the PRE-FAMILY committed curve; the locked shape is family-ON, and a10 is the + // 8192 acceptance now. a6 held the ORGANIC ISLET layer — the DROPPED mechanism (→ D-063): + // islands are organic-only, made by the stretch + fragmentation and identified here, never + // placed. An oracle pinning islet output is an oracle defending a design that was reversed. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. } // ═══ 2. DETERMINISM ═══ @@ -354,7 +329,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF, revert OFF by default + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -368,11 +343,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); diff --git a/Tools/Scripts/ReliefRenderTool.cs b/Tools/Scripts/ReliefRenderTool.cs index 955866b..ad77ab7 100644 --- a/Tools/Scripts/ReliefRenderTool.cs +++ b/Tools/Scripts/ReliefRenderTool.cs @@ -26,7 +26,7 @@ namespace IslaApocalypse.Tools /// ISLA_LOOKS comma-separated look names (default: atlas,relief,dusk) /// ISLA_TASK authoring task number (default 3) /// ISLA_BATCH descriptor, NO prefix (default "relief_taste") - /// ISLA_SOURCE batch to read .f32 from (default 02_pass1_port) + /// ISLA_SOURCE batch to read .f32 from (default "chat1/02_pass1_port") /// ISLA_DUMP_RAW "1" to also dump .f32 when a field had to be generated /// public partial class ReliefRenderTool : Node @@ -36,6 +36,10 @@ namespace IslaApocalypse.Tools public override void _Ready() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1")); int mapSize = EnvInt("ISLA_MAPSIZE", 2048); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); @@ -43,8 +47,8 @@ namespace IslaApocalypse.Tools string batch = EnvStr("ISLA_BATCH", "relief_taste"); // ⚠ A FULL batch folder name, prefix included — it names an EXISTING folder rather than // composing a new one, so it is not run through BatchRoot. Tracks TerrainGenTool's - // default output: BatchRoot(task 2, "pass1_port") = 02_pass1_port. - string source = EnvStr("ISLA_SOURCE", "02_pass1_port"); + // default output: BatchRoot(task 2, "pass1_port") = /02_pass1_port (rivers/01: chat-namespaced). + string source = EnvStr("ISLA_SOURCE", "chat1/02_pass1_port"); bool dumpRaw = EnvStr("ISLA_DUMP_RAW", "0") == "1"; LookConfig[] looks = SelectLooks(EnvStr("ISLA_LOOKS", null)); @@ -83,7 +87,9 @@ namespace IslaApocalypse.Tools } else { - var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }; + // ⭐ rivers/01: family-off PINNED — this regenerates a PHASE-1 field to stand in for a + // missing `02_pass1_port` dump, so it must reproduce that dump, not the new default. + var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }.WithFamilyOff(); Pass1Result r = Topography.Generate(cfg); height = r.Height; origin = "generated"; diff --git a/Tools/Scripts/ReviewBatchTool.cs b/Tools/Scripts/ReviewBatchTool.cs index 0b218cc..7111eed 100644 --- a/Tools/Scripts/ReviewBatchTool.cs +++ b/Tools/Scripts/ReviewBatchTool.cs @@ -25,7 +25,7 @@ namespace IslaApocalypse.Tools /// /// ISLA_TASK authoring task number (default 4) /// ISLA_BATCH descriptor, NO prefix (default "review") - /// ISLA_SOURCE batch holding the .f32 (default 02_pass1_port) + /// ISLA_SOURCE batch holding the .f32 (default "chat1/02_pass1_port") /// ISLA_MAPSIZE side in columns (default 2048) /// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds) /// @@ -59,16 +59,21 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1")); int task = EnvInt("ISLA_TASK", 4); string descr = EnvStr("ISLA_BATCH", "review"); // ⚠ A FULL batch folder name, prefix included — this NAMES AN EXISTING FOLDER rather than // composing a new one, so it is not run through BatchRoot. The default tracks where - // TerrainGenTool writes by default: BatchRoot(task 2, "pass1_port") = 02_pass1_port. + // TerrainGenTool writes by default: BatchRoot(task 2, "pass1_port") = chat1/02_pass1_port + // (rivers/01 namespaced batches by chat; the READ side carries the chatN/ prefix explicitly). // It was "01_pass1_port" until chat1/05 renamed the folder to its authoring-task number; // a stale default here does not fail loudly, it just silently regenerates instead of // loading — which is exactly the kind of quiet cost worth pinning to the real name. - string source = EnvStr("ISLA_SOURCE", "02_pass1_port"); + string source = EnvStr("ISLA_SOURCE", "chat1/02_pass1_port"); int mapSize = EnvInt("ISLA_MAPSIZE", 2048); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); @@ -103,7 +108,7 @@ namespace IslaApocalypse.Tools if (h == null) { GD.PrintErr($" ⚠ no full dump for {seed} at {mapSize} — generating deterministically."); - h = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed }).Height; + h = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed)).Height; } notes.Add(Gray(h, mapSize, batchRoot, "2_height_grayscale", seed, diff --git a/Tools/Scripts/SeedGalleryTool.cs b/Tools/Scripts/SeedGalleryTool.cs index 30dddb8..d5df989 100644 --- a/Tools/Scripts/SeedGalleryTool.cs +++ b/Tools/Scripts/SeedGalleryTool.cs @@ -126,6 +126,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 4); string descr = EnvStr("ISLA_BATCH", "seed_gallery"); @@ -180,7 +184,7 @@ namespace IslaApocalypse.Tools var poolPass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); poolPass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -288,13 +292,18 @@ namespace IslaApocalypse.Tools public ulong ElapsedMs; } + /// + /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/04 is the PRE-FAMILY committed-curve + /// gallery (`terrain-curve-v1`); the re-baseline flipped the bare defaults family-ON, and this + /// tool must keep producing the curve gallery it was judged as. → TerrainGenConfig.WithFamilyOff(). + /// private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label) => new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, Knots = k, Anchors = a, LowlandCeilingM = LowlandCeilingM, - }; + }.WithFamilyOff(); private static SeedMetrics WriteSeed(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw) diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs index 3d9842f..c252e20 100644 --- a/Tools/Scripts/ShapingOracle.cs +++ b/Tools/Scripts/ShapingOracle.cs @@ -675,6 +675,82 @@ namespace IslaApocalypse.Tools return c; } + // ═══ ⭐⭐ ANCHOR RESOLUTION — A MISSING ANCHOR IS LOUD (rivers/01) ═══════════════════════════ + // + // ═══ THE FAILURE MODE THIS CLOSES ═══ + // + // Every anchor check used to be written as: + // + // if (File.Exists(dump) && mapSize == 8192) hard.Add(DumpRegression(...)); + // else GD.Print(" ⚠ skipped — no dump at …"); + // + // so a moved, renamed or deleted anchor did not make the oracle FAIL. It made the oracle + // NOT RUN — and a batch with a silently-skipped check prints an all-PASS table and reads + // exactly like a clean one. The `INDEX.md` is then evidence for a claim nothing checked. + // + // > ### ⚠ This is the INVERSE of the hazard the re-baseline guards against. + // > The re-baseline stops an oracle PASSING FOR THE WRONG REASON. This stops one + // > DISAPPEARING FOR NO REASON. Both end with a green table and an unproven claim, and the + // > rivers/01 batch-root migration is exactly the event that would have triggered the second + // > one — nine anchors moved under `/` in a single commit. + // + // The two cases the old code conflated are now separated, in ONE place (`LoadAnchor`) so every + // anchor site behaves identically: + // MISSING FILE the anchor moved, was renamed, or was never written. THROWS — the check + // cannot run, and dropping it quietly is the failure this exists to close. + // WRONG SIZE the anchor exists, but only at the size it was captured. A legitimate case + // (a probe run at another size); reported loudly and recorded INCONCLUSIVE, + // which is a FAIL in the table. ⚠ Deliberately NOT a throw: a guard that fires + // on ordinary small-map probe work is a guard people learn to route around. + + /// + /// ⭐ Load a regression anchor's `.f32`, separating the two failures the old code conflated. + /// + /// FILE ABSENT → THROWS. The anchor moved, was renamed, or was never written. + /// This is the migration hazard, and it is not survivable: the + /// check cannot run and must not be quietly dropped. + /// PRESENT, WRONG SIZE → returns null, LOUDLY. A legitimate case — an anchor exists only + /// at the size it was captured, and a batch run at another size + /// genuinely cannot check against it. The caller's + /// records it INCONCLUSIVE, which is + /// a FAIL in the table, never a pass. + /// + /// ⚠ The distinction matters because only ONE of them means something is broken. Throwing on a + /// size mismatch would make every small-map probe run refuse, and a guard that fires on ordinary + /// work is a guard people route around. + /// + /// The oracle check this anchor feeds, e.g. "a10" — named in the message. + /// The env override that can re-point it, e.g. "ISLA_T10_SOURCE". + /// The resolved absolute path. + /// The field size to read. + public static float[,] LoadAnchor(string checkId, string envVar, string dumpPath, int mapSize) + { + if (!System.IO.File.Exists(dumpPath)) + throw new InvalidOperationException( + $"[Oracle] MISSING ANCHOR for check '{checkId}' — nothing at:\n" + + $" {dumpPath}\n" + + "An oracle whose anchor is absent does not fail, it does not RUN — and a batch with a " + + "silently-skipped check prints an all-PASS table that reads exactly like a clean one. " + + "Refusing to render evidence for a claim nothing checked.\n" + + $"→ Re-point it with {envVar}, or regenerate the anchor. If the anchor is genuinely " + + "retired, DELETE THE CHECK — never leave one aimed at nothing. (rivers/01.)"); + + long expected = (long)mapSize * mapSize * 4; + long actual = new System.IO.FileInfo(dumpPath).Length; + if (actual != expected) + { + int anchorSize = (int)System.Math.Round(System.Math.Sqrt(actual / 4.0)); + Godot.GD.PrintErr( + $" ⚠⚠ {checkId}: NOT CHECKED — the anchor exists but was captured at {anchorSize}, " + + $"and this run is at {mapSize} ({actual:N0} bytes, expected {expected:N0}). This is a size " + + $"mismatch, NOT a missing file: the batch is simply not proven against it at this size. " + + $"Run at {anchorSize} to check it. The oracle records INCONCLUSIVE, which is a FAIL — never a pass."); + return null; + } + + return HeightField.Load(dumpPath, mapSize); + } + /// Render the whole oracle as a markdown table for the INDEX and the report. public static string ToMarkdownTable(IEnumerable checks) { diff --git a/Tools/Scripts/SouthernStretchTool.cs b/Tools/Scripts/SouthernStretchTool.cs index cd0304c..934768e 100644 --- a/Tools/Scripts/SouthernStretchTool.cs +++ b/Tools/Scripts/SouthernStretchTool.cs @@ -41,7 +41,6 @@ namespace IslaApocalypse.Tools /// ISLA_BAND_START / ISLA_BAND_FEATHER the fixed band (fractions of the map; constants for the batch) /// ISLA_STRETCH_SINKER 1 = the sinker rides the stretched distance (default), 0 = real y /// ISLA_DIAG_ONLY=1 diagnostic only - /// ISLA_SKIP_8K=1 skip the 8192 band regression (a4b) /// public partial class SouthernStretchTool : Node { @@ -64,7 +63,6 @@ namespace IslaApocalypse.Tools private const int DefaultMapSize = 4096; private const int DefaultCalibSize = 2048; - private const int GallerySize = 8192; public override void _Ready() { @@ -91,6 +89,10 @@ namespace IslaApocalypse.Tools private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 8); string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore"); @@ -103,11 +105,8 @@ namespace IslaApocalypse.Tools float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac); bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1"; bool diagOnly = EnvStr("ISLA_DIAG_ONLY", "0") == "1"; - bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1"; bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; - string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); - string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); - string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery"); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); @@ -136,16 +135,26 @@ namespace IslaApocalypse.Tools TerrainGenConfig Cfg(int size, int seed, string label, float stretch, bool sinkerStretched, bool sinkerOn = true, bool edgeOn = true) { + // ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT: + // it was authored and judged before the shape family existed, and its regression checks + // hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the + // bare defaults family-ON, so without this pin every config here would silently acquire + // stretch + fragmentation and every anchor check would fail for a configuration reason. + // → TerrainGenConfig.WithFamilyOff(). var c = new TerrainGenConfig { MapSize = size, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, - CoastShelf = false, Offshore = new OffshoreSettings(), - RegionLabeling = true, SpeckRevert = false, - SouthStretch = stretch, SouthBandStartFrac = bandStart, SouthBandFeatherFrac = bandFeather, StretchSinker = sinkerStretched, + RegionLabeling = true, SouthernSinker = sinkerOn, EdgeNoise = edgeOn, - }; + }.WithFamilyOff(); + // …then the swept axis, AFTER the pin. Coastal fragmentation stays OFF here: chat2/08 + // predates it, and this tool's ladder measures the stretch ALONE. + c.SouthStretch = stretch; + c.SouthBandStartFrac = bandStart; + c.SouthBandFeatherFrac = bandFeather; + c.StretchSinker = sinkerStretched; return c; } @@ -255,34 +264,31 @@ namespace IslaApocalypse.Tools var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker); Pass1Result p1 = Topography.Generate(offCfg); var curveOff = offCfg.Clone(); curveOff.Curve = false; + // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plate}_full", "height.f32"); - hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); - string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plate}_continuous_restored", "height.f32"); - float[,] t03 = HeightField.Load(t03Dump, calibSize); - hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, stretch OFF == task-03 .f32 dump", Shaping.Shape(p1, offCfg).Height, t03, calibSize, t03Dump)); + hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump", + Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump)); - // ⭐ a3b — stretch ON at the ladder's TOP: north of the band bit-identical to the tag's own dump. + // ⭐ a3c KEPT and RE-POINTED — the north-locked invariant is the load-bearing claim of the + // southern stretch (→ D-065) and it does NOT need an external anchor: the unstretched field + // is generated right here. Re-pointing it off `03_mountain_restore` is what let that dump + // retire without losing the guarantee. var topCfg = Cfg(calibSize, plate, "top", maxStretch, stretchSinker); Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg); - if (t03 != null) - hard.Add(ShapingOracle.NorthLocked("a3b", $"stretch {maxStretch:G3} ON: north of the band bit-identical to task-03 dump (terrain-curve-v1); changes only in/below the band", pTop.Height, t03, calibSize, bandRowC)); + Pass2Result pUnstretched = Shaping.Shape(p1, offCfg); + hard.Add(ShapingOracle.NorthLocked("a3c", + $"stretch {maxStretch:G3} ON: north of the band bit-identical to the SAME-RUN unstretched field; changes only in/below the band", + pTop.Height, pUnstretched.Height, calibSize, bandRowC)); foreach (var c in hard) GD.Print(" " + c); - if (!skip8k) - { - string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plate}", "height.f32"); - if (File.Exists(t04Dump)) - { - GD.Print($" a4b: generating {plate} at {GallerySize}, stretch {maxStretch:G3} …"); - var g = Cfg(GallerySize, plate, "top", maxStretch, stretchSinker); - Pass2Result pG = Shaping.Shape(Topography.Generate(g), g); - var a4b = ShapingOracle.NorthLocked("a4b", $"stretch {maxStretch:G3} ON at {GallerySize}: north of the band bit-identical to terrain-curve-v1's 04 gallery dump", - pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, (int)(bandStart * GallerySize)); - hard.Add(a4b); GD.Print(" " + a4b); - } - else GD.Print($" a4b: ⚠ skipped — no 04 gallery dump at {t04Dump}"); - } - else GD.Print(" a4b: skipped (ISLA_SKIP_8K)"); + // ⚑ RETIRED at rivers/01 — a3 (`03_mountain_restore`) and a4b (`04_seed_gallery`). + // a3 was a curve-development intermediate, subsumed by the terrain-shape-v1 acceptance. + // a4b asserted the north-lock against the PRE-FAMILY 8192 gallery — but the north-lock is + // now proven scale-free against a same-run field (a3c above), so the external anchor bought + // nothing except an 8192 generation on every run. + // The dump is NOT deleted (file-safety; regenerable, and the record of what was judged); + // its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4. + } // ═══ 4. THE LADDER — 5 levels × 2 seeds ═══ @@ -420,7 +426,7 @@ namespace IslaApocalypse.Tools var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { - var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // bare default: offshore / revert / stretch OFF + var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } @@ -433,11 +439,14 @@ namespace IslaApocalypse.Tools var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { + // ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and + // `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole + // calibration is family-off" is a total claim rather than a field-by-field one.) var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", - }; + }.WithFamilyOff(); Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs index c6dfe99..419c597 100644 --- a/Tools/Scripts/TerrainGenConfig.cs +++ b/Tools/Scripts/TerrainGenConfig.cs @@ -242,14 +242,20 @@ namespace IslaApocalypse.Tools // ---- PASS 1b — the coast shelf + offshore islets (chat2/05) ---------- // - // ⚠⚠ BOTH DEFAULT OFF, DELIBERATELY — and that is a decision to revisit, not an oversight. + // ⚠⚠ BOTH STAY OFF AFTER THE rivers/01 RE-BASELINE — and each for its own reason. // - // Every oracle in this phase holds pass 1 against Phase 1's `.f32` dumps (curve-off == - // `02_pass1_port`), and the curve tools hold it against task 01/03's. The shelf changes every - // below-sea cell and the islets ADD LAND, so the moment either defaults ON, every one of - // those regression anchors goes stale at once. The batch tools that want them turn them on - // explicitly. FLIPPING THESE DEFAULTS IS THE ACT THAT RETIRES THE PHASE-1 REGRESSION DUMPS — - // do it deliberately, in a task that re-baselines the oracles, not as a side effect here. + // SHELF OFF because the LOCKED SHAPE has no shelf. `terrain-shape-v1` (a59e52f) was + // judged with `CoastShelf = false`, so turning it on here would produce terrain + // the developer never approved and would break the bit-identity this default set + // exists to guarantee. The shelf is below-sea only and invisible until water + // renders — EVALUATING IT IS ITS OWN LATER TASK (→ D-041), once water renders. + // (The rivers kickoff's "set shelf ON" was a mis-statement; corrected by the + // developer via master before rivers/01 ran.) + // ISLETS OFF because it is the DROPPED mechanism (→ D-063): islands are ORGANIC-ONLY, + // produced by the southern stretch + coastal fragmentation and then IDENTIFIED by + // the region layer — never placed. The pass is kept, not deleted, because + // `OffshoreSettings.Faithful` is a live port-fidelity control and `Pass1Result` + // carries the offshore seam. ⚠ NEVER RE-ENABLE IT IN THE BARE DEFAULT. /// /// The submarine coast shelf (IslandFalloff.CoastShelf). Below-sea only, @@ -289,16 +295,27 @@ namespace IslaApocalypse.Tools /// of the map to seabed. Origin-blind; lower-only and /// component-only, asserted; mainland never a candidate. /// - /// ⚠ DEFAULT OFF IN THE BARE CONFIG, for exactly the reason the shelf and the islets are: the - /// raw field has small natural nubs, so with this ON the calibration pool's land histogram, the - /// curve knots and every Phase-1 / task-03 / task-04 regression dump would move at once. - /// The region batch turns it on explicitly (its preset is ON); flipping the bare default is - /// the act that re-baselines the oracles — own task, not a side effect. + /// ⭐ ON BY DEFAULT since rivers/01 — it is part of the LOCKED SHAPE (`terrain-shape-v1`). + /// + /// ⚠ It was default-OFF through chat 2 because turning it on moves the calibration pool's land + /// histogram, the curve knots and every pre-family regression dump at once. rivers/01 is the + /// task that owned that flip: the calibration pool is now pinned FAMILY-OFF + /// (), so the knots are unmoved and the flip is terrain-only. /// - public bool SpeckRevert = false; + public bool SpeckRevert = true; - /// The revert threshold, as a fraction of the map's AREA (scale-free). → . - public float MinLandComponentFrac = RegionPass.ThresholdMidFrac; + /// + /// The revert threshold, as a fraction of the map's AREA (scale-free). + /// + /// ⚠⚠ THE LOCKED-SHAPE VALUE, PINNED AS A LITERAL — 2.5e-7 ≈ 4 cells at 4096, ≈ 17 at 8192. + /// It is deliberately NOT (3e-5, ~2,013 cells at + /// 8192), which was the pre-re-baseline default and is 120× larger: at that threshold the + /// revert eats real islands rather than specks. The three named `RegionPass.Threshold*Frac` + /// values are the chat2/07 exploration ladder; this is the value chat2/09–10 froze and the + /// developer judged. Changing it changes `terrain-shape-v1`. (rivers/01, from + /// `FragGalleryTool.FrozenSpeckFrac`.) + /// + public float MinLandComponentFrac = 2.5e-7f; // ---- PASS 1 — THE SOUTHERN STRETCH (chat2/08, exploration) ---------------- // @@ -311,14 +328,30 @@ namespace IslaApocalypse.Tools // organically. Cells north of the band take the UNTOUCHED code path, so the classify field // there is bit-identical by construction (asserted). Nothing is stamped. - /// ⭐ THE SWEPT AXIS. 0 = off (bit-identical to the unstretched field everywhere). Stretch factor inside the band: 1 ⇒ the southward distance is halved, 3 ⇒ quartered. - public float SouthStretch = 0f; + /// + /// ⭐ THE SWEPT AXIS. 0 = off (bit-identical to the unstretched field everywhere). Stretch + /// factor inside the band: 1 ⇒ the southward distance is halved, 3 ⇒ quartered. + /// + /// ⭐ 2 IS THE LOCKED-SHAPE VALUE since rivers/01 (→ D-065). With coastal fragmentation it is + /// one of the two mechanisms that MAKE the islands (→ D-063). + /// + public float SouthStretch = 2f; - /// The band's FIXED latitude line, fraction of the map (y runs south). Sea identity is hard above it. A constant for a whole batch. - public float SouthBandStartFrac = SouthernStretch.DefaultBandStartFrac; + /// + /// The band's FIXED latitude line, fraction of the map (y runs south). Sea identity is hard + /// above it. A constant for a whole batch. + /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandStartFrac` + /// today). The literal is the pin: retuning that constant must not silently move + /// `terrain-shape-v1`. (rivers/01.) + /// + public float SouthBandStartFrac = 0.70f; - /// The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map. A constant for a whole batch. - public float SouthBandFeatherFrac = SouthernStretch.DefaultBandFeatherFrac; + /// + /// The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map. + /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandFeatherFrac` + /// today) — same reason as . (rivers/01.) + /// + public float SouthBandFeatherFrac = 0.05f; /// /// Does the SOUTHERN SINKER ride the stretched distance (true — it is part of the southern @@ -326,7 +359,8 @@ namespace IslaApocalypse.Tools /// it keeps pulling the extended mass down where it always did)? The chat2/08 diagnostic /// measured both; → . /// - public bool StretchSinker = SouthernStretch.DefaultStretchSinker; + /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultStretchSinker` today). rivers/01. + public bool StretchSinker = true; // ---- PASS 1 — COASTAL FRAGMENTATION (chat2/09, exploration) ------------------ // @@ -336,34 +370,59 @@ namespace IslaApocalypse.Tools // into islands while the interior — window weight exactly zero — is bit-identical by // construction. Nothing is detected, nothing is stamped. → CoastalFragment. - /// ⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the window's centre. - public float FragmentAmp = 0f; + /// + /// ⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the + /// window's centre. + /// + /// ⭐ 0.5 IS THE LOCKED-SHAPE VALUE since rivers/01 — chat2/09's `frag_4`, frozen by chat2/10 + /// across an 8-seed gallery and tagged `terrain-shape-v1`. With the southern stretch it is one + /// of the two mechanisms that MAKE the islands (→ D-063). + /// + public float FragmentAmp = 0.5f; - /// The fragmentation noise's frequency, periods per map width — the neck/lobe scale. The secondary dial (fixed this round). → . - public float FragmentFreqPerMapWidth = CoastalFragment.DefaultFreqPerMapWidth; + /// + /// The fragmentation noise's frequency, periods per map width — the neck/lobe scale. + /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultFreqPerMapWidth` + /// today); the literal is the pin. (rivers/01.) + /// + public float FragmentFreqPerMapWidth = 12f; - /// The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the centre, smooth to 0 at ± half-width; exactly 0 beyond. - public float FragmentBandCentre = CoastalFragment.DefaultBandCentre; - public float FragmentBandHalfWidth = CoastalFragment.DefaultBandHalfWidth; + /// + /// The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the + /// centre, smooth to 0 at ± half-width; exactly 0 beyond. + /// ⚠ Pinned as LITERALS at the locked-shape values (equal `CoastalFragment.DefaultBandCentre` / + /// `DefaultBandHalfWidth` today). (rivers/01.) + /// + public float FragmentBandCentre = 0.66f; + public float FragmentBandHalfWidth = 0.18f; /// /// false (default) ⇒ zero-mean noise: the margin is redrawn — bites AND builds (which can also /// bridge an island back onto the mainland). true ⇒ bites only ((noise+1)/2 ≥ 0): land can only /// recede, necks are cut, nothing is bridged, the coast net-recedes. → . /// - public bool FragmentBitesOnly = CoastalFragment.DefaultBitesOnly; + /// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultBitesOnly` today). rivers/01. + public bool FragmentBitesOnly = false; // ---- PASS 2b — HYDRAULIC EROSION (chat2/11) — RENDER MAP ONLY ------------------ // // The reference's droplet erosion, ported verbatim (Core.HydraulicErosion), run on the render // field AFTER shaping (after detail, before the crater carve — which does not exist yet). The // classify field never sees it (D-046); the caller's flood guard proves no waterline moved. - // ⚠ DEFAULT OFF in the bare config for the usual reason (regression anchors); the batch turns - // it on. The governors + physics are the reference ConfigManager's declared defaults, clamped - // as it clamped them (→ ErosionPass). + // ⭐ DEFAULT ON since rivers/01 — the locked baseline the rivers epic routes on is the ERODED + // render field. The governors + physics are the reference ConfigManager's declared defaults, + // clamped as it clamped them (→ ErosionPass). + // + // ⚠ This flag is INERT unless a caller explicitly runs `ErosionPass.Apply` — nothing in + // `Topography.Generate` or `Shaping.Shape` reads it. So flipping it moves no field on its own; + // it makes "erode by default" the answer for the callers that DO ask. - /// ⭐ Erosion on/off. Render only. Default OFF (see above). - public bool Erosion = false; + /// + /// ⭐ Erosion on/off. RENDER MAP ONLY — the classify field never sees it (→ D-046), proven by + /// `ErosionPass`'s flood guard. Default ON since rivers/01 (see above); the erosion A/B sets + /// it to false explicitly for its OFF half. + /// + public bool Erosion = true; /// Governor 1 — droplet count. Reference 250000, clamp [0, 50,000,000]. public int ErosionDropletCount = 250000; @@ -401,6 +460,75 @@ namespace IslaApocalypse.Tools /// The scale object every distance and frequency in the generator derives from. public GenerationScale Scale => new GenerationScale(MapSize); + // ═══ ⭐⭐ THE FAMILY-OFF PIN (rivers/01) ═══════════════════════════════════════════════════ + // + // ═══ WHY THIS EXISTS — the preserve mechanism for `terrain-shape-v1` ═══ + // + // The locked shape's curve knots are PERCENTILES OF THE FAMILY-OFF LAND DISTRIBUTION, measured + // by chat2/01 over a 6-seed pool at 2048 and applied to FAMILY-ON generation. That was not a + // choice at the time — it was simply what the bare defaults produced, because the shape family + // defaulted off. + // + // rivers/01 flipped those defaults ON. Left alone, every calibration pool would have moved + // with them (fragmentation removes coastal land, the stretch adds southern land — both change + // the land CDF), the six knots would have moved, and with them the render field of EVERY + // batch, including `10_frag4_seed_gallery` (= `terrain-shape-v1`) and `11_erosion`. The + // developer's ruling was to PRESERVE the locked terrain bit-identically, so the pool is pinned + // here instead of the knots being baked: CALIBRATION STAYS LIVE, its INPUT DISTRIBUTION is + // what is held still. + // + // ⚠ At the moment it was introduced this was a NO-OP BY CONSTRUCTION: it sets exactly the + // values the bare defaults carried the instant before the flip. That is what made the flip + // provably terrain-only — and what the rivers/01 acceptance confirmed byte-for-byte at 8192². + // + // > ### ⚑ THE JUDGED-AND-PARKED PROPERTY (recorded for the vault, rivers/01) + // > The curve knots are percentiles of the FAMILY-OFF land distribution, applied to FAMILY-ON + // > terrain. That is a real asymmetry and it is DELIBERATE, not an oversight: re-pooling on + // > family-on land would move the locked shape the developer judged. Same disposition as the + // > mid-slope feather — documented, revisit only at the final palette / in the mesher if it + // > ever visibly bothers. → `Vision - Threads - Open Questions.md`. + // + // ═══ WHAT IT IS FOR, AND WHAT IT IS NOT FOR ═══ + // + // USE IT for a config whose job is to REPRODUCE A PRE-FAMILY FIELD: a curve-calibration + // pool, or the "off" half of a regression check against a family-off `.f32` dump. + // DO NOT use it for generation — the locked shape IS the family, and the bare defaults now + // carry it. + + /// + /// ⭐ Pin the SHAPE FAMILY and erosion OFF on this config, independent of this class's + /// evolving defaults, and return it for chaining. → the block above for why. + /// + /// Sets: false · Off · + /// false · 0 · + /// 0 · false. + /// + /// ⚠ It deliberately does NOT touch or the band/window + /// shape dials: with the revert off and the amplitudes at zero those are unread, so pinning + /// them would assert an independence that does not exist. It also does not touch the CURVE + /// (knots, anchors, calibration, climb knobs) — the family and the curve are separate axes, + /// and a calibration pool is pass-1 only. + /// + public TerrainGenConfig WithFamilyOff() + { + CoastShelf = false; + Offshore = new OffshoreSettings(); // Mode = Off + SpeckRevert = false; + SouthStretch = 0f; + FragmentAmp = 0f; + Erosion = false; + return this; + } + + /// + /// ⭐ A bare pass-1 config with the shape family pinned OFF — THE CALIBRATION POOL'S CONFIG. + /// Every curve-calibration pool in `Tools/` builds its fields through this, so there is one + /// place where "what distribution were the knots measured on?" is answered. + /// → . + /// + public static TerrainGenConfig CalibrationPool(int mapSize, int seed) => + new TerrainGenConfig { MapSize = mapSize, Seed = seed }.WithFamilyOff(); + /// /// ⚠ DEEP on . MemberwiseClone is shallow, so two configs cloned /// from one parent would share a single mutable anchor object and an A/B that edited one @@ -421,6 +549,10 @@ namespace IslaApocalypse.Tools $"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " + $"trench={Trench} spine={MountainSpine}] " + $"[curve={Curve} detail={ShelfDetail} relief={ShelfReliefAmpM:F1}m edge={ShelfEdgeVariationM:F1}m " + - $"knots={(Knots == null ? "-" : Knots.Name)}]"; + $"knots={(Knots == null ? "-" : Knots.Name)}] " + + // rivers/01: the shape family is now a DEFAULT, so a run header must state it — otherwise + // "the defaults" stops being a readable claim the moment anyone asks which defaults. + $"[stretch={SouthStretch:G3} frag={FragmentAmp:G3} speck={(SpeckRevert ? $"{MinLandComponentFrac:G3}" : "off")} " + + $"shelf={CoastShelf} islets={Offshore?.Mode} erosion={Erosion}]"; } } diff --git a/Tools/Scripts/TerrainGenTool.cs b/Tools/Scripts/TerrainGenTool.cs index 799023e..f32057a 100644 --- a/Tools/Scripts/TerrainGenTool.cs +++ b/Tools/Scripts/TerrainGenTool.cs @@ -43,6 +43,10 @@ namespace IslaApocalypse.Tools public override void _Ready() { ToolingPaths.Configure(OS.GetUserDataDir()); + // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, + // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another + // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. + ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1")); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); @@ -81,7 +85,11 @@ namespace IslaApocalypse.Tools GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---"); foreach (var (label, mutate) in Ladder()) { - var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label }; + // ⭐⭐ rivers/01 — FAMILY-OFF PINNED. This tool AUTHORED `02_pass1_port`, the Phase-1 + // regression anchor that survived the re-baseline. If it picked up the family-on + // defaults it could no longer regenerate its own dump, and the last link between + // today's generator and the Phase-1 port would break silently. + var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label }.WithFamilyOff(); mutate(cfg); rows.Add(RunOne(cfg, batchRoot, skipRaw)); } @@ -92,7 +100,7 @@ namespace IslaApocalypse.Tools GD.Print("\n--- SEED BATCH (full pass-1) ---"); foreach (int seed in seeds) { - var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }; + var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }.WithFamilyOff(); // ⭐ see the ladder above rows.Add(RunOne(cfg, batchRoot, skipRaw)); } diff --git a/Tools/Scripts/TerrainShapeV1.cs b/Tools/Scripts/TerrainShapeV1.cs new file mode 100644 index 0000000..6e16eaa --- /dev/null +++ b/Tools/Scripts/TerrainShapeV1.cs @@ -0,0 +1,116 @@ +using System; +using IslaApocalypse.Core; + +namespace IslaApocalypse.Tools +{ + /// + /// ⭐⭐ THE LOCKED SHAPE — `terrain-shape-v1` (commit a59e52f), AS AN ASSERTION. + /// + /// ═══ ⚠⚠ THIS TYPE INVERTED AT rivers/01. READ THIS BEFORE USING IT. ═══ + /// + /// It used to be a PRESET: Apply(cfg) stamped the locked shape onto a bare config, because + /// the bare defaults did not reproduce the terrain the developer had judged. That was the single + /// most expensive fact in the codebase — *"run the default generator" ≠ "the terrain the developer + /// locked"* — and a fresh chat comparing bare-default output against the locked renders would see + /// differences that were CONFIGURATION, not regression. + /// + /// **rivers/01 re-baselined the defaults so that `new TerrainGenConfig()` IS the locked shape.** + /// So Apply is GONE — there is nothing left to apply, and re-stamping the values on top of + /// the defaults would mask a default drift instead of catching it. + /// + /// > ### What survives is the OPPOSITE job: these constants are now the ASSERTION TARGET. + /// > They are the values the developer judged, written down once, and holds + /// > the live defaults against them. If a default is ever edited, the batch tools that claim to + /// > render the locked shape REFUSE TO RUN rather than quietly rendering something else. + /// + /// ⚠ DO NOT "fix" a drift by editing these constants — they are the record of what was approved, + /// and `10_frag4_seed_gallery` / `11_erosion` are its pixels. A deliberate shape change moves the + /// defaults AND these constants AND re-runs the acceptance, in one task, as rivers/01 did. + /// + /// ⚠ THE SHELF IS OFF, DELIBERATELY. The locked shape has no coast shelf; evaluating it (→ D-041) + /// is its own later task, once water renders. ⚠ THE ISLETS ARE OFF, PERMANENTLY (→ D-063): islands + /// are organic-only, made by the stretch + fragmentation and identified by the region layer. + /// + /// ⚠ EROSION IS NOT PART OF THIS SHAPE. `terrain-shape-v1` is the pass-1/1c/2a field; erosion is + /// pass 2b, on top, render-only (`11_erosion` = `ea291ea`). It is asserted separately. + /// + public static class TerrainShapeV1 + { + /// The tagged commit this shape is defined by. ⚠ Reference the COMMIT — the tag is annotated and local-only until the developer pushes it. + public const string Commit = "a59e52f"; + + public const float FragmentAmp = 0.5f, FragmentFreq = 12f, BandCentre = 0.66f, BandHalfWidth = 0.18f; + public const bool BitesOnly = false; + public const float Stretch = 2f, BandStart = 0.70f, BandFeather = 0.05f; + public const bool StretchSinker = true; + public const float SpeckFrac = 2.5e-7f; + + /// + /// ⭐ THE DEFAULT-DRIFT GUARD. Throws unless a bare carries the + /// locked shape exactly. Every batch tool that renders or asserts `terrain-shape-v1` calls this + /// before it generates anything. + /// + /// ⚠ It is a THROW, not a warning, for the same reason is: a batch + /// that renders the wrong terrain still produces beautiful, browsable, wrong PNGs, and a human + /// gate cannot see a default from a picture. + /// + /// The calling tool, for the message. + public static void Assert(string who) + { + var d = new TerrainGenConfig(); + string bad = null; + void Want(string name, object got, object want) + { + if (!Equals(got, want)) bad = (bad == null ? "" : bad + "; ") + $"{name} = {got}, expected {want}"; + } + + Want(nameof(d.SouthStretch), d.SouthStretch, Stretch); + Want(nameof(d.SouthBandStartFrac), d.SouthBandStartFrac, BandStart); + Want(nameof(d.SouthBandFeatherFrac), d.SouthBandFeatherFrac, BandFeather); + Want(nameof(d.StretchSinker), d.StretchSinker, StretchSinker); + Want(nameof(d.FragmentAmp), d.FragmentAmp, FragmentAmp); + Want(nameof(d.FragmentFreqPerMapWidth), d.FragmentFreqPerMapWidth, FragmentFreq); + Want(nameof(d.FragmentBandCentre), d.FragmentBandCentre, BandCentre); + Want(nameof(d.FragmentBandHalfWidth), d.FragmentBandHalfWidth, BandHalfWidth); + Want(nameof(d.FragmentBitesOnly), d.FragmentBitesOnly, BitesOnly); + Want(nameof(d.SpeckRevert), d.SpeckRevert, true); + Want(nameof(d.MinLandComponentFrac), d.MinLandComponentFrac, SpeckFrac); + Want(nameof(d.CoastShelf), d.CoastShelf, false); + Want(nameof(d.RegionLabeling), d.RegionLabeling, true); + Want("Offshore.Mode", d.Offshore.Mode, OffshoreMode.Off); + + if (bad != null) + throw new InvalidOperationException( + $"[{who}] LOCKED-SHAPE DRIFT: the bare TerrainGenConfig no longer reproduces " + + $"terrain-shape-v1 ({Commit}) — {bad}. Refusing to run: this tool's output is only " + + "meaningful if the defaults ARE the locked shape. → Tools/Scripts/TerrainShapeV1.cs (rivers/01)."); + } + + /// + /// The same guard for the EROSION default, kept separate because erosion is pass 2b and is not + /// part of the shape. Tools that render the erosion A/B set the flag per variant and call this + /// only if they rely on the default. + /// + public static void AssertErosionDefaultOn(string who) + { + if (!new TerrainGenConfig().Erosion) + throw new InvalidOperationException( + $"[{who}] EROSION DEFAULT DRIFT: bare TerrainGenConfig.Erosion is false; rivers/01 " + + "made it true (the rivers baseline routes on the eroded render field). Refusing to run."); + } + + /// + /// One line for a run header, READ FROM THE LIVE DEFAULTS rather than from the constants — + /// so the header states what actually ran, and states whether that is + /// still the locked shape. + /// + public static string Describe() + { + var d = new TerrainGenConfig(); + return $"terrain-shape-v1 ({Commit}) from the BARE DEFAULTS: frag amp {d.FragmentAmp} freq {d.FragmentFreqPerMapWidth} " + + $"window {d.FragmentBandCentre}±{d.FragmentBandHalfWidth} · stretch {d.SouthStretch} " + + $"(band {d.SouthBandStartFrac}/{d.SouthBandFeatherFrac}, sinker {(d.StretchSinker ? "stretched" : "real-y")}) · " + + $"speck revert {d.MinLandComponentFrac:G2} · offshore {d.Offshore.Mode} · shelf {(d.CoastShelf ? "ON" : "OFF")} · labeling {(d.RegionLabeling ? "ON" : "OFF")}"; + } + } +} diff --git a/Tools/batches/README.md b/Tools/batches/README.md index 23e2826..58ecbb5 100644 --- a/Tools/batches/README.md +++ b/Tools/batches/README.md @@ -8,12 +8,33 @@ Real batch output is written under `ISLA_OUTPUT_DIR` (default `user://output/bat ## Layout ``` -batches/NN_/ NN = the task number that ran it +batches//NN_/ = the chat namespace · NN = the task number that ran it ├── INDEX.md what varied, what to look at, what was concluded ├── scratch/ intermediates — PERSISTENT, never cleaned └── _/ one directory per generated world ``` +### ⭐ The `` segment (rivers/01) + +**`NN` is the AUTHORING TASK NUMBER, and task numbers restart at 00 in every build chat** — so a flat +`batches/` collides the moment a second chat exists. It did: chat 1's `02_pass1_port` and chat 2's +`02_curve_continuous` are both "batch 02", and nothing in either name says which chat made it. On the +real pile there were **four colliding prefixes (02, 03, 04, 06)** across 25 batches, separable only by +SLUG. + +The slug is set by `ToolingPaths.ConfigureChat(...)` and is **required** — with none set, `BatchRoot` +throws rather than writing to the un-namespaced root. Each tool defaults to its own authoring chat, so +re-running it reproduces its batch in place; **`ISLA_CHAT` redirects a run to another namespace**, which +is what keeps an acceptance run from overwriting the very anchor it is checking against. + +> ### ⚠ Writes are namespaced; historical READS carry the prefix themselves. +> `BatchRoot(task, descriptor)` inserts ``. `BatchesRoot` is the plain root, and every +> `ISLA_*_SOURCE` anchor composes against it — so an anchor default is written out in full, e.g. +> `"chat1/02_pass1_port"`. Namespacing only the writes would silently orphan every historical read. +> **That is why a missing anchor now THROWS** (`ShapingOracle.LoadAnchor`): a moved anchor used to make +> its oracle not RUN, and a batch with a silently-skipped check prints an all-PASS table that reads +> exactly like a clean one. + `INDEX.md` is not optional. A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not browsable.