# Tools — the offline generator **Holds:** the world generator and its diagnostics. Everything that produces a blueprint, and nothing that consumes one at play time. **Boundary — the tools wall:** > ### ⚠⚠ TOOLS MAY NOT REFERENCE `Client/`. THE DEPENDENCY RUNS ONE WAY, OR THE WALL IS NOT A WALL. > > No UI, no player controllers, no shaders, no rendering code. `Tools/` may use `Core/`. > > **Two reasons, both from `Design - Tooling - Offline Generation.md`:** a lighter shipped client, > and — less obviously — **not handing players the world-generation logic.** Shipping the generator > lets it be reverse-engineered into an unfair map preview, with the whole island's layout, every > settlement and every route known before anyone sets foot on the beach. > > **Tools emit data; they never write to a live save.** A generator that could touch live server > state is a corruption risk for no benefit. ## What is here ### The generator — pass 1 (Phase 1) **Ported from the reference's `MapGenerator.GenerateTopography` pass-1 loop** at tag `pre-rewrite-reference` (`ab78883`). This is the crown-jewel port: **working code and its tuned constants, carried over verbatim — not re-derived from a design summary** (→ D-050, `Design - Rewrite - Extraction Manifest.md`). | File | What it is | |---|---| | `Scripts/TerrainNoise.cs` | ⭐ The FastNoiseLite config, **every fractal property pinned explicitly** | | `Scripts/Topography.cs` | ⭐⭐ Pass 1 — the six elements, in the reference's execution order | | `Scripts/IslandFalloff.cs` | The whole reference file now: `SmoothAbs`, the **coast shelf**, the **offshore islets** + the reshape helpers | | `Scripts/OffshorePass.cs` | ⭐ **Pass 1b** — shelf + the **organic islet layer** (the one island mechanism) + the slop guards, over the finished pass-1 arrays, then `HMaxSeed` is retaken (chat2/05, retuned chat2/06) | | `Scripts/OffshoreSettings.cs` | Every islet dial in one object; `Faithful()` (the reference) and `Organic()` (the reshape, tuned: **density** + **south weight** + guards). ⚠ No floor / stamps / count guarantee — chat2/05's `Hybrid()` was reverted out in chat2/06 (git history has it) | | `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, the separation-guard geometry, **the hemisphere convention** | | `Scripts/OffshoreDiagnosis.cs` | The per-hemisphere **measurement** — valid-zone area, binding gate, noise peaks over threshold (chat2/06 §2) | | `Scripts/TagOverlayRenderer.cs` | The island-tag / hemisphere debug overlay | | `Scripts/RegionPass.cs` | ⭐ **Pass 1c** (chat2/07) — `Core.RegionLabeling` over the classify field, the **origin-blind speck revert** (lower-only + component-only asserted, mainland never), the island tag **by construction** | | `Scripts/RegionOverlayRenderer.cs` | The labeled-regions overlay: mainland one tint, each island its own colour, reverted specks dark red | | `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument | | `Scripts/SouthernStretch.cs` | ⭐ **The southern stretch** (chat2/08, exploration) — the one deliberate sea-identity relaxation, inside a fixed feathered latitude band: `y' = yB + (y − yB)/(1 + stretch·ramp)` for the mask geometry (and the sinker), texture untouched; north of the band bit-locked by construction | | `Scripts/SouthernStretchTool.cs` + `Scenes/SouthernStretchTool.tscn` | The chat2/08 batch — the diagnostic (`ISLA_DIAG_ONLY`) and the 5-level × 2-seed fragmentation ladder with the hemisphere-split instrument | | `Scripts/CoastalFragment.cs` | ⭐ **Coastal fragmentation** (chat2/09, exploration) — a perimeter-wide, band-limited, zero-mean noise on the pre-power falloff inside the coastal window (≈ 0.66 ± 0.18); thin necks flip first; interior bit-identical by construction | | `Scripts/CoastalFragmentTool.cs` + `Scenes/CoastalFragmentTool.tscn` | The chat2/09 batch — the frequency × amplitude probe (`ISLA_PROBE`) and the 4-amplitude × 2-seed ladder at a fixed stretch | | `Scripts/FragGalleryTool.cs` + `Scenes/FragGalleryTool.tscn` | The chat2/10 gallery — render-only: 09's `frag_4` frozen across 2 anchors + 6 fresh seeds at 8192, with the count/size table | | `Scripts/ErosionPass.cs` | ⭐ **Pass 2b** (chat2/11) — the erosion caller: render field only (copied if aliased), governors clamped as the reference's ConfigManager did, the crater exclusion passed through INERT, and the **flood guard** (render water pixels before/after; any change throws) | | `Scripts/ErosionTool.cs` + `Scenes/ErosionTool.tscn` | The chat2/11 batch — 4 gallery seeds × erosion off/on at 8192, the mid-slope crop, the erosion stats table; also `TerrainShapeV1` — the locked shape's values pinned once | | `Scripts/DrainageRenderer.cs` | The drainage maps (chat2/12): log-scaled accumulation; the promoted-candidates overlay (trunks cyan, endorheic giants orange, lean terminals red) | | `Scripts/DrainageTool.cs` + `Scenes/DrainageTool.tscn` | The chat2/12 batch — `DrainageAnalysis` on the eroded 8192 fields of 4 task-11 seeds, analysis-only oracle (terrain bit-identical, no water, fill invariants, determinism, ocean from the region layer) | | `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) | | `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** | | `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles | | `Scripts/HeightField.cs` | Raw `.f32` save/load — **the generation/presentation seam** | | `Scripts/TerrainGenTool.cs` | Batch entry point (ladder + seed batch + `INDEX.md`) | | `Scenes/TerrainGenTool.tscn` | Run this | ### The generator — pass 2a: the redistribution curve (Phase 2, chat2/01) **Ported from `REFERENCE:Tools/Scripts/HeightCurve.cs` + `TerrainDetailPass.cs`** at the same tag. The curve itself lives in `Core/` (engine-free, a named C++-candidate seam); `Tools/` carries the wiring, the measuring instrument and the batch. | File | What it is | |---|---| | `Scripts/Shaping.cs` | ⭐⭐ Pass 2a — curve + detail per column, producing **the two height fields** | | `Scripts/Pass2Result.cs` | The render field, the classify field, and `FieldsAreAliased` | | `Scripts/LandHistogram.cs` | ⭐ The land CDF — **the calibration engine AND the diagnostic** | | `Scripts/HistogramRenderer.cs` | The labelled distribution plot (clipped axis, marked) | | `Scripts/ShapingOracle.cs` | ⭐⭐ The four automatic correctness checks | | `Scripts/CurveBaselineTool.cs` | The chat2/01 batch: calibrate → variants → histograms → oracle | | `Scenes/CurveBaselineTool.tscn` | Run this | > ### ⭐⭐ THE TWO-FIELD SPLIT (D-046) STARTS HERE > > `heightClassify` is **raw, uncurved, un-detailed** — bit-for-bit the pass-1 field. > `height` is **curved and detailed**, and is what gets drawn and (later) eroded and carved. > > Everything that CLASSIFIES the world (biomes, water bodies, the ocean fill) will read the classify > field; everything that DRAWS it reads the render field. Nothing consumes the classify field yet — > it is established at the curve because **the curve is where the second field is born**, and > retrofitting a classify path after three passes have run on one array is how the two silently > diverge. `Pass2Result.FieldsAreAliased` is true when the curve is off (both point at one array, as > the reference did) — a later pass that writes one while reading the other must read both into > locals first. > ### ⭐ THE KNOTS ARE MEASURED, NOT CHOSEN > > The curve's six input knots are **P60/73/83/88/96/99 of the pooled land height distribution**, > which is what makes the band shares 60/13/10/5/8/3/1 exact by construction. The reference shipped > the six resulting literals and threw the instrument away — so its knots could never be re-derived, > only trusted. `LandHistogram` is that instrument, rebuilt. > > ⚠ **A knot set is only valid for the distribution it was measured on.** Re-run > `CurveBaselineTool` after any change to pass 1. ```bash xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CurveBaselineTool.tscn ``` `ISLA_MAPSIZE` (default 2048) · `ISLA_SEEDS` (6 pinned) · `ISLA_SHOWPIECE_SIZE` (8192) · `ISLA_SHOWPIECE=0` · `ISLA_VARIANTS=0` (calibration-only probe) · `ISLA_PHASE1_SOURCE` · `ISLA_SKIP_RAW=1` **The oracle runs before anything is looked at**, and the tool exits non-zero if any check fails: | | Check | |---|---| | `a` | curve OFF is bit-identical to Phase 1's pass-1 output | | `a′` | pass 1 is bit-identical to Phase 1's `.f32` dump | | `b` | the classify field is bit-identical to the raw pre-curve field, curve on or off | | `c` | the effective per-seed curve is strictly monotonic (24-corner sweep, throws otherwise) | | `d` | realized land shares match 60/13/10/5/8/3/1 | ### The map renders — presentation (Phase 1, the look) **Presentation only** (→ `Design - Rendering - Roughness Is Presentation.md`): it changes how height is *shown*, never how it is *made*. It **cannot** change the terrain — it is handed a height field loaded from a `.f32` dump and has no way to produce one. | File | What it is | |---|---| | `Scripts/GrayscaleRenderer.cs` | ⭐ Plain grayscale — **for judging the noise itself** | | `Scripts/ReliefPalette.cs` | The hypsometric palettes, stops placed on measured percentiles | | `Scripts/ReliefRenderer.cs` | The tint (+ optional hillshade) blend | | `Scripts/Hillshade.cs` | Horn 3×3 shaded relief; the `ZExaggeration` slope table | | `Scripts/LegendRenderer.cs` | ⭐ The elevation legend strip | | `Scripts/TinyFont.cs` | A 5×7 bitmap font, so a legend needs no viewport | | `Scripts/LookConfig.cs` | The look dials and the named variants | | `Scripts/ReviewBatchTool.cs` | ⭐ The review batch — grayscale + hero gradient + labelled relief | | `Scripts/ReliefRenderTool.cs` | Taste-gate batch runner | | `Scenes/ReviewBatchTool.tscn`, `Scenes/ReliefRenderTool.tscn` | Run these | > ### ⭐ The pretty map is the WIDE GRADIENT, rendered FLAT. Relief is not the hero. > > A reference relief map gets its look from **wide colour + subtle relief + COHERENT TERRAIN.** We > have the first. The third is Phase 2's curve and erosion. On raw pass-1 noise a hillshade has > nothing coherent to shade, so it renders fine fractal bumpiness as **visual fuzz that actively > hides the elevation the colour is showing.** So: > > | Look | Role | > |---|---| > | **`gradient_flat`** | ⭐ **the hero.** Wide Costa-Rica gradient, `HillshadeStrength = 0`, no relief at all | > | `subtle_relief` | gentle relief (zex 18, strength 0.30) for comparison — **still fuzzes on raw noise** | > | ⚠ `diagnostic_relief` | **A DEV VIEW, NEVER THE PRETTY MAP.** Strong exaggeration to make slope artifacts jump out. Its bumpiness is exaggerated *slope*, not extra terrain | > | `atlas`, `dusk` | the task-03 plates, kept reachable | > > Relief comes into its own once erosion carves ridges and valleys worth lighting. **Plain grayscale** (`GrayscaleRenderer`) normalizes a field to its own `[min,max]`. ⚠ This is the one place per-image normalization is *correct* — everywhere else anchors are fixed so images compare. Here the point is to see one field at full contrast, so the range used is printed and written into the `INDEX.md`, and a shade reads back to a height. **You cannot judge noise through a palette:** a ramp bends the value distribution and a hillshade adds shape the data does not have. **The legend** (`LegendRenderer`) draws a colour bar with ticks from the palette's own ramp, so it cannot drift from the map beside it. ⚠ **Ticks are RELATIVE height, not metres** — the metres conversion is Phase 2's elevation profile, and labelling the bar "m" now would invent a fact. It says so on the image. Text comes from `TinyFont`, a 5×7 bitmap font, specifically so a legend does not drag in the SubViewport capture path (which awaits render frames and is why `--headless` hangs). ```bash Godot_v4.7.2-stable_mono_linux.x86_64 --headless \ --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/ReliefRenderTool.tscn ``` `ISLA_LOOKS=atlas,relief,dusk` · `ISLA_SOURCE` (batch to read `.f32` from) · `ISLA_MAPSIZE` · `ISLA_SEEDS` · `ISLA_BATCH` · `ISLA_DUMP_RAW=1` **Three looks, deliberately** — a subjective gate drowns in a wall of near-duplicates, and each pair isolates one question: `atlas` vs `relief` asks how strong the relief should be (same palette and light); `atlas` vs `dusk` asks about palette and sun angle. > ### ⚠ Vertical exaggeration is required, and it is a LOOK dial — not a physical claim. > > Height is in raw noise units over a 1 m grid, so the true per-pixel gradient is tiny: measured > median land slope is **0.22°**. Un-exaggerated, the whole island shades as a flat plane. The raw > units are **not metres** — the metres-per-unit conversion is Phase 2's, not this renderer's. **The blend, and why it is not a multiply.** Hillshade on flat ground is `sin(altitude)` ≈ 0.71, so a plain `tint × shade` darkens the entire map by 29% before any slope is involved and the hypsometric tints are never actually seen. So the shade is normalized by its flat-ground value first — flat terrain keeps its true tint, and only *slope* moves the colour — then shadows **multiply** while highlights **screen** toward white. That asymmetry is the difference between a colour ramp and a map you would frame. **⚠ Relief fades out with depth below sea.** Not only taste: the deep floor is dominated by the Trench, a synthetic additive wall whose gradient is ~1.5× the p99 *land* gradient. Shading it faithfully draws a bright rim around the whole map and lights up the abyss with base-noise mottle — relief on terrain nobody is meant to look at. The fade puts relief where the bathymetry is real (the near-shore shelf) and lets the abyss lie flat, which is the cartographic convention anyway. ```bash Godot_v4.7.2-stable_mono_linux.x86_64 --headless \ --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn ``` `ISLA_MAPSIZE` (default 2048) · `ISLA_SEEDS` (comma-separated, positive) · `ISLA_BATCH` · `ISLA_SKIP_RAW=1` · `ISLA_LADDER=0` **The six ported elements, in order** — the order is load-bearing, not an implementation detail: 1. **wobbled latitude scalar** — `y/MapSize` plus a ±0.1 low-frequency wobble 2. **island falloff/mask** — squircle + ellipse, 50/50, then `Pow(·, 2.5f)` 3. **edge roughness** — `noise(x·2.5, y·2.5)`, modulated by the squircle so it bites only at the rim 4. **southern sinker** — bottom 25%, **before** the power (its effect is superlinear) 5. **the Trench** — map-anchored, additive on both axes, **after** the power 6. **mountain spine** — `SmoothAbs` crest, cubic, southern fade, amplitude `0.6f` > ### ⚠ The latitude scalar is NOT climate temperature. > > The reference called it `temperature` and let biomes read the same array — a large part of how > climate and terrain got fused. Here it is a **stage-1-local latitude field that terrain geometry > reads**, and nothing else. Climate is **stage 3** and classifies finished shape (D-049 §2, D-056). > Do not alias, store, or rename this into a climate map. **Pass 1b — the coast shelf and the offshore islets (chat2/05).** The reference continued its pass-1 loop with the submarine **coast shelf** and the **offshore islet** layer (~:621-664); v2 runs them as a second sweep over the finished arrays — `OffshorePass` — with the same per-pixel arithmetic in the same order, then **retakes `HMaxSeed` after them**, as the reference did (chat2/00 Drift §2, closed). The islets exist in two presets: `OffshoreSettings.Faithful()` (the reference, verbatim — the control) and `OffshoreSettings.Organic()` (the reshape: small / low / flat / crisp, corners allowed — **the organic noise-field layer is the only island mechanism**, tuned for coverage by **density** and a **south weight**, with speck / separation / blob guards; chat2/06). ⚠ **No forced count.** chat2/05's seeded floor (`Hybrid()`: stamps guaranteeing ≥2 N / ≥4 S) was tried and **reverted out on look** in chat2/06; the per-hemisphere counts are a statistical outcome of the tuning, read off the batch's count table. > ### ⚠ Both default OFF — deliberately, and that is a decision to revisit. > > Every oracle in this phase holds pass 1 against Phase 1's `.f32` dumps. The shelf changes every > below-sea cell and the islets ADD LAND, so defaulting either ON stales every regression anchor at > once. Batch tools turn them on explicitly. **Flipping the defaults is the act that retires the > Phase-1 dumps — do it in a task that re-baselines the oracles.** → `TerrainGenConfig`. **Pass 1c — region labeling + the speck revert + the island tag (chat2/07).** `RegionPass` runs `Core.RegionLabeling` over the finished classify field — **land 8-connected, mainland = the component containing the map centre, every other component an island** — then the config-gated **speck revert** (`TerrainGenConfig.SpeckRevert` / `MinLandComponentFrac`): every non-mainland component below the threshold is lowered to its ring's seabed. Origin-blind (a natural nub goes like an offshore dot), **lower-only and component-only, asserted**, mainland never a candidate. Then the island tag, by construction. ⚠ `SpeckRevert` defaults OFF in the bare config for the same reason as the shelf/islets (it would move the calibration pool and every regression dump); the batch turns it on. > ### ⭐ The island tag — data, set BY THE REGION LAYER, read by nothing yet. > > `Pass1Result.IsIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell of every > non-mainland land component — natural detached masses and offshore-pass islands alike — with the > COMPONENT's hemisphere (by centroid). **NORTH = rows `[0, MapSize/2)`, SOUTH = `[MapSize/2, > MapSize)`** — y runs south. `Pass1Result.Regions` carries the full per-component table. A biome / > fertility / placement pass reads these and never re-derives island-land from geometry. Null when > region labeling is off. (chat2/05–06 tagged only what the offshore pass raised — fixed in 07.) > ### ⚠ The one honest coupling: islets TURN WATER INTO LAND. > > Unlike the curve, which moves heights but not the waterline, the islet layer adds above-sea cells — > new classification downstream. That is why it runs in the base shape before anything classifies: > change an island dial, regenerate, and classification re-runs consistently. A known property, not a > surprise. **Not here at all:** erosion, rivers, water bodies, the crater carve, biomes, roads, the mesher. ### ⚠ The render writes a `Godot.Image` directly — not the capture path The reference captured maps through a SubViewport + a `TextureRect._Draw`, because it composited **vector overlays** (roads, town markers, river polylines) onto the raster. That machinery awaits render frames — which is exactly why unattended runs need `xvfb-run` and why `--headless` hangs on it. **Phase 1 has no overlays.** A heightmap is a raster, so it is written per-pixel into an `Image` and saved: no viewport, no frame awaits, no display. **Do not reintroduce the capture path by habit** — it becomes correct the moment something vector needs compositing, and not before. ### Diagnostics - `Scenes/UserDirProbe.tscn` — confirms this project's `user://` is its own, not the old prototype's. Exits non-zero on a clash. - `Scenes/NoiseDefaultsProbe.tscn` — prints this engine's `FastNoiseLite` constructor defaults and checks them against the recorded baseline. **Run it after any Godot upgrade.** It cannot change the terrain (`TerrainNoise` pins every value); it makes a drifting default *visible* instead of silent. --- ## ⭐ The conventions Phase 1's generator inherits These were **earned, not assumed** — each one came out of the prototype's terrain arc. → `Design - Tooling - Iteration and Batching.md`. ### 1. `xvfb-run` for unattended generation — NOT `--headless` > ### ⚠ `--headless` HANGS on a real generation run. > > The snapshot capture **awaits render frames, and there is no frame loop without a display.** > **Measured, not assumed.** A run left on `--headless` does not fail loudly; it sits there. ```bash xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 ``` A windowed run on the desktop also invites being closed mid-run by whoever is using the machine. **The one exception is a job that awaits no frames** — like `UserDirProbe`, which is why that one documents `--headless` explicitly: ```bash Godot_v4.7.2-stable_mono_linux.x86_64 --headless \ --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn ``` **If a job awaits a frame, it needs `xvfb-run`. When unsure, use `xvfb-run`** — it is correct in both cases and costs nothing. ### 2. Environment overrides — safe BY CODE, not by care Every path a tool reads or writes resolves through `Core/Scripts/ToolingPaths.cs`, and each has an environment override, so a batch **cannot** read over or write to the developer's live files: | Variable | Overrides | Default | |---|---|---| | `ISLA_CONFIG_PATH` | the generation config file | `user://config.json` | | `ISLA_BLUEPRINT_PATH` | blueprints read/written | `user://blueprints` | | `ISLA_OUTPUT_DIR` | generation output (batches live under it) | `user://output` | > ⭐ **The point is that it is enforced by CODE, not by care.** A rule that depends on an executor > remembering it will eventually meet an executor who does not. There is no second way to obtain these paths. Do not add one. ### 3. ⚠ File safety — permanent rules Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises. 1. **No deletion in the runtime root or anywhere under `batches/`.** 2. **Intermediates persist** in a `scratch/` subfolder that is **never cleaned.** 3. **The developer's placed files are never touched.** 4. **Any deletion is named explicitly in the run report.** > ⚠ **These came from a real incident:** an executor admitted it had been deleting the developer's > staged test blueprint. It was owned and fixed **in code.** That is why these are rules and not > guidance. ### 4. Batch layout ``` 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. > > `04_review` means *"the batch task 04 authored"*. It does **not** mean "the fifth batch". A task > that produces six batches produces six `04_*` folders — not `04_` through `09_`. > > **This already drifted once.** Tasks 02 and 03 used the prefix as a global running counter and > produced `00_smoke` … `05_trophy_10240` across two tasks, so nothing in a folder name said which > task made what. > > **Enforced in code, not remembered:** the task number is an explicit argument to > `ToolingPaths.BatchRoot(taskNumber, descriptor)`, which composes the prefix itself and **refuses a > descriptor that carries its own** (`ISLA_BATCH=05_foo` → `REFUSED`, exit 2). Tools take it as > `ISLA_TASK`. **A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not browsable.** The `INDEX.md` is what makes a batch readable a week later. ⚠ **Batches are OUTPUT.** They live under `ISLA_OUTPUT_DIR` (i.e. under `user://`), **not in this repo.** The `batches/` folder here carries the convention, not the data. No batches are run this phase. ### 5. Iteration levers, for when the generator exists - **Skip the road pass for all terrain and water iteration.** In the prototype this cut a run from ~26 min to ~80 s — a **19× cut**, and the difference between iterating on terrain and not iterating on terrain. Blueprints from such a run carry present-but-empty road sections, which is **legitimate, not corrupt.** - **⭐ Build the oracle before the taste-iteration, not after.** Because the classify path was pinned to *uncurved* height, every shaping iteration had a hard automatic correctness check (biome and water maps md5-identical). Five rounds of taste-iteration were safe **because correctness was not being judged by eye.** *Where a future phase has a subjective gate, ask first what the automatic invariant is.* - **Config-gate every shaping change**, with the legacy behaviour surviving on the other side. It keeps changes comparable as an A/B pair, and it means a rejected change costs a flipped default rather than a reverted commit. - **The lever is variants per batch, not speed per pass.** Two Phase B tasks ran ~2 hours each, which looked like the pipeline getting slower. It was not — a single pass stayed at ~2 minutes. The cost was A/B/ablation multiplication. **When a batch is genuinely wide, say so up front.** ### 6. Scaling discipline **Nothing uses a raw pixel number.** Every distance, radius, threshold and noise frequency comes from `Core/Scripts/GenerationScale.cs`. Read its header before adding any constant — including the tightening on **normalized additive noise offsets**, which is a forward-guard for the Phase 1 noise port. → `Design - Tooling - Scaling Discipline.md`.