feat: TunnelNetwork stage A — the SDF spine, wrapping BuildChunkCache

The last archetype is ~1080 lines with 13 detail modifiers, a two-region
cache and a per-room op override. Porting it whole before anything can be
verified is ~600 unverified lines on top of ~200 — the pattern this
refactor has dodged six times. So: three stages.

Stage A = vertical scale, base rock, cave warp, room graph (+ pits and
chimneys), carve, worms, structural post. 6 ops. It is verifiable NOW
because every detail modifier is amplitude-gated and FStrateGenerationParams
already defaults them all to zero — zeroing SurfaceRoughness sends the
ORIGINAL down exactly the path stage A ported.

TunnelNetwork stays OFF in UsesOperatorStackForChunk until stage C.

The decision that matters: FRoomGraphSource CALLS BuildChunkCache and
EvaluateSDFCached rather than transcribing them. That is where §8.4's
two-region window-invariance discipline lives; a transcription would fork
it, and the fork would be "validated" by a test comparing it to the
original. Only the ~60 lines of glue are transcribed.

FRAME ops are retired. All three candidates are now ported and none needed
one: CaveWarp's scope is exactly one operator (pits/chimneys read unwarped
coords), VerticalScale is a one-line pure function, and the island warp was
already local. Not missing infrastructure — one idea seen three times from
a distance.

Also: check 3 was going to compare two interleaved param sets against the
original, which would have FAILED — the original's SDF cache key has no
params, so it serves B the rooms it built for A. Comparing there measures
its bug, not the port. Rewritten against each stack evaluated alone. The
same reasoning suggests a live production staleness across Gradient
transitions; filed in AUDIT §C2 as SUSPECTED with the check that would
confirm it, since it rests on a premise I have not verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 18:28:06 +02:00
parent 96e75abe57
commit ef5bda3d8a
7 changed files with 963 additions and 9 deletions
+89
View File
@@ -1903,3 +1903,92 @@ Then `Underwater` + `TunnelNetwork` (§8 / §2, **last**, with §8.4's window-in
Perf still parked by Jahni until the transition is complete.
---
## 2026-07-28 — 12 tests green. TunnelNetwork STAGE A (of three) written.
Jahni: *"everything's green."* FloatingIslands is bit-identical and wired; 6 of 8 ported.
**Worth asking for explicitly next run:** the three coverage numbers that test prints — samples
inside island rock, the AllAir verdict count, and the diff count. Green with zero samples inside
island rock would mean the equivalence proved that two empty voids agree.
### The last archetype, staged deliberately
`GetDensityWithParams` is ~1080 lines, 13 detail modifiers, a two-region cache and a per-room op
override. Porting all of it before anything can be verified would be ~600 unverified lines on top of
~200 unverified ones — the `AUDIT §P3` pattern this refactor has dodged six times.
**The way in:** every detail modifier is amplitude-gated, and `FStrateGenerationParams` already
leaves all of them at **zero** by default (`BuildParamsFromDefinition` stopped merging them globally
— they arrive as per-room ops). One exception, `SurfaceRoughness = 5`. So zeroing those amplitudes
sends the ORIGINAL down exactly the path stage A ported, and stage A is verifiable **today**, bit for
bit, against the real function. Same discipline as the height-stack test's "defaults, then all ops
ON", taken in the other direction.
- **Stage A (this commit):** vertical scale · base rock · cave warp · room graph (+ pits + chimneys)
· carve · worms · structural post. 6 ops.
- **Stage B:** the 13 detail modifiers, gated on `Sdf < SDFBlendRadius·3`.
- **Stage C:** the per-room op override (`§2`'s option (a)), the `Underwater` water flag, and only
then does `UsesOperatorStackForChunk` return true for either.
### ⚠️ The one decision that matters: the op CALLS `BuildChunkCache`, it does not transcribe it
Every other port is a literal transcription. This one must not be. `BuildChunkCache` carries the
two-region window-invariance discipline (`ARCHITECTURE §8.4`) and is the most delicate code in the
plugin; transcribing it would **fork** it — two copies of one invariant, drifting, with the copy
"validated" by a test that compares it to the original. What *is* transcribed is the ~60 lines of
glue around it (strate-index memo, search-box key, warp, pit/chimney loops).
### `FRAME` ops are retired, and porting is what retired them
`§2` described two nested frames (`VerticalScale`, `CaveWarp`) and `§7` a third (the island warp).
Porting all three dissolved all three:
- **`CaveWarp` wraps exactly ONE operator.** Pits and chimneys explicitly read *unwarped* coords.
A transform whose scope is one op is a local variable, not a frame.
- **`VerticalScale` is `Z / Scale`** — a pure function of a scalar and a param, one line wherever it
is needed. A frame would add a channel to avoid a division.
- **The island warp** was already kept local for the same reason.
**Zero frames out of three candidates.** It was never missing infrastructure; it was the same thing
seen three times from a distance. Recorded in the builder rather than left as a permanent TODO.
Also found: TunnelNetwork's carve divides by `Max(SDFBlendRadius·2, 1)` where Maze/Shafts/Islands
divide by `Blend·2`. The formulas diverge once `Blend·2 < 1`, so `FSdfConvertOp` gained an explicit
`MinDivisor` instead of letting the two look interchangeable. `Max(x, 0) == x` for positive Blend, so
the green ports are untouched.
### ⚠️ A test that compares against the original would have measured the ORIGINAL'S bug
Check 3 (two stacks, different params, evaluated A/B/A/B at the same point) was written comparing
both against `GetDensityWithParams`. **It would have failed** — and not because of the port. The
original's SDF cache key is `(XY box, strate, seed)` with **no params**, so under interleaving it
serves B the rooms it built for A. Comparing to it there measures its staleness, not my operator.
Rewritten to compare each stack against **itself evaluated alone**: an oracle that does not share the
defect under test.
That in turn raises a **suspicion, filed as a suspicion**: `GetGenerationParams` blends params across
Gradient transitions, so two chunk Zs inside one strate can hold different params with the same XY
box, strate index and seed ⇒ no rebuild ⇒ the lower chunk gets the upper chunk's rooms. In
production, with no live edit needed. Recorded in `AUDIT §C2` **with the check that would confirm
it**, because the whole chain rests on "Gradient blending actually varies within a strate", which I
have not verified. The port does not inherit it — the params fingerprint forces the rebuild.
### What stage A deliberately does NOT prove
Box verdicts: **zero proved, and the test asserts zero.** `FRoomGraphSource` answers `Both` (its
bounds are in the SDF cache, which it would have to build for the queried box — worth doing only
once `ClassifyTile` actually consumes `ClassifyBox`), and `FWormFieldSource` answers `CarveOnly`
*everywhere*, because a fielded-noise carve has no spatial bound. That is `§0.2`'s point, and the
amplitude cap that fixes it (`t ∈ [0,1]`, `Mask ∈ [0,1]` ⇒ at most `WormStrength` toward air) now has
a home in `FWormFieldSource::MaxCarveAmplitude()`, waiting for a fold that carries numbers.
**UNVERIFIED:** not compiled. Likely spots: `FStrateTerrainOpEntry` / `UVoxelStrateDefinition` newly
reachable from `VoxelDensityOpStack.cpp` (added the include); `MakeSdfCarve`'s new defaulted third
parameter (declared in the header, so the three existing call sites still compile); `FCrc` needing
`Misc/Crc.h` (it comes via `CoreMinimal.h`, and `FSurfaceColumnSource` already uses it in this file);
and the test's `GetGenerationParams` signature.
**Next single action:** build, run the filter — **13 tests**, the new one is
`VoxelForge.OpStack.TunnelNetworkSpineEquivalence`. Then stage B (the 13 modifiers).
---