islaApocalypse-v2/Tools
beezm ea301f7a8c rivers/02: river promotion — unified ranking over both termini, and the count gate
Choose the river count against the terrain that actually exists. The M3 count of
3 was tuned on topography the southern stretch (D-065) and coastal fragmentation
(D-063) have since replaced, and TrunkCount/GiantCount/EndorheicMaxCount = 3 are
LEAN REPORTING CAPS, not a statement about this island.

UNIFIED RANKING. The reference promoted from two lists with two quotas — N sea
trunks, N endorheic giants — which encodes the assumption that reaching the sea
is what makes a drainage a river. This terrain does not satisfy it: 54-78% of
land drains inland across the gallery, 106-121 terminal basins per seed. So
selection is unified — every major drainage ranked by contributing-cell count in
ONE list, top N promoted, and the sea/endorheic split FALLS OUT. A quota would
have promoted small coastal drainages over far larger inland ones purely because
of where they end. Deliberate departure from the reference's structure (D-050
noted, approved in chat). Only SELECTION is unified: the per-terminus tag is
retained per river because rivers/03's routing branches on it, and Trunk/Giant
are untouched.

  The metric is the same unit on both sides, and that is now ASSERTED rather
  than argued: sea Acc and endorheic BasinInflow are both counts of contributing
  land cells on the same D8 field, and every land cell has exactly one
  destination, so Σ sea Acc + Σ BasinInflow + unrouted == LandCells exactly. The
  tool refuses to rank unless it holds, per seed. It held on all 8.

DrainageAnalysis.cs is NOT modified. The complete candidate set is re-derived
from state it already exposes (Dir/Acc/BasinId/BasinInflow/FullFilled); the
three internal per-basin values that are not exposed are exactly reconstructible
because terminal basins are reverted to the real surface while FullFilled keeps
the fill. Only REPORTING caps were raised, and only so the analysis's own
TraceStem produces a real upland course per promotable candidate.
EndorheicMinDepthM/MinAreaPx were left alone — they decide which depressions
BECOME terminal basins, i.e. they define the routing surface itself, and a count
chosen on a moved surface would be a count for terrain that does not exist.

TWO FINDINGS.

  1. THE TERRAIN HAS NO NATURAL COUNT. The knee (largest ratio between
     consecutive ranks) lands at rank 19/7/15/5/4/12/3/8 across the eight
     gallery seeds at ratios of 1.33-1.57x, with one uncorroborated 2.37x. A
     break that wanders across the whole plausible range at that strength is not
     a break — on a log axis the candidates fall on a near-straight line.
     N is a DESIGN choice, and the diagnostic says so instead of inventing one.

  2. THE ISLAND'S MAJOR RIVERS ARE INLAND RIVERS. Six of eight seeds have ZERO
     sea-reaching drainages in their top 8; the median at N=16 is one; one seed
     has none at 16. On the primary seed the largest sea drainage ranks 11th at
     736,088 px against 2,329,573 px for the largest endorheic — 3.2x. (That
     outlet is exactly batch 12's recorded top trunk, to the cell.)

A concern I raised and then CLOSED by measuring: MinOutletSeparationPx is a
plain Euclidean test with no notion of which landmass a coastline belongs to, so
on a fragmented archipelago it could suppress an island's only river. Measured:
0-6 cross-landmass suppressions per seed, and — decisively — the largest
suppressed above-floor outlet anywhere is 209,611 px against a smallest ladder
cutoff of 439,592 px, so ZERO suppressed outlets clear any rung on any seed. The
rule provably cannot have altered the ladder. Real but harmless; flagged for
rivers/03, not a blocker. The rule was NOT changed — it belongs to the analysis,
and moving it would move the candidate set the developer is being asked to judge.

Also found: the params are ABSOLUTE pixel counts, so this analysis is only valid
at the size they were tuned for. At 1024 nothing qualifies as endorheic (the
whole endorheic half cannot be exercised) and separation suppresses 15,043 of
15,048 outlets. The tool now refuses loudly to have a non-8192 run read as a
count decision. Flagged for rivers/03: these want to become scale-free fractions
as MinLandComponentFrac already is.

Giant.ProvisionalRoute is never rendered — the "comb" is rivers/03's to replace,
and drawing it would make a count judgment look like a finished network. The
plates draw the real erosion-carved upland stems. Endorheic rivers are marked at
where their stem POOLS, not at the basin's deepest cell: the analysis
distinguishes these deliberately and they sit up to 395 px apart, which drew
every stem detached from its own endpoint until it was fixed.

TASTE GATE: 8/12/16 are presented and nothing is decided. No count is chosen, no
default is set, and neither TerrainGenConfig nor DrainageAnalysis.Params changed.

-> XX_Human/output/rivers/02_promotion.report.md
-> batches/rivers/02_promotion/INDEX.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
2026-08-23 06:13:03 -04:00
..
batches rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat 2026-08-23 04:45:10 -04:00
Scenes rivers/02: river promotion — unified ranking over both termini, and the count gate 2026-08-23 06:13:03 -04:00
Scripts rivers/02: river promotion — unified ranking over both termini, and the count gate 2026-08-23 06:13:03 -04:00
README.md rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat 2026-08-23 04:45:10 -04:00

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.

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).

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.

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 scalary/MapSize plus a ±0.1 low-frequency wobble
  2. island falloff/mask — squircle + ellipse, 50/50, then Pow(·, 2.5f)
  3. edge roughnessnoise(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 spineSmoothAbs 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/0506 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.

xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 <scene>

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:

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/<chat>/<task>_<descriptor>/<seed>_<variant>/
batches/<chat>/<task>_<descriptor>/INDEX.md
batches/<chat>/<task>_<descriptor>/scratch/   ← persistent; never cleaned

⚠ <chat> is required (rivers/01): task numbers restart per chat, so a flat batches/
  collided across chat1 and chat2. Set by ToolingPaths.ConfigureChat(), overridable
  with ISLA_CHAT. Historical anchor READS carry the prefix in their own source string
  (e.g. "chat1/02_pass1_port"), because they compose against BatchesRoot, not BatchRoot.
  → Tools/batches/README.md

⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER.

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_smoke05_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_fooREFUSED, 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.