docs(opstack): record the green build and rewrite the handoff for a fresh context

OPSTACK-PROGRESS gets the measured entry: every number the run produced, what each one settles, and
the single open warning. OPSTACK-PLAN's status header goes from UNVERIFIED to BUILT AND GREEN.

OPSTACK-HANDOFF is rewritten end to end. It no longer describes a transition in progress but a
completed one, and it leads with the two things a cold context needs: the Underwater 0%-coverage
warning (with the truncation-vs-floor finding behind it, and the reminder that a green bit-identity
over solid rock is not evidence), and the one task everything else now waits on -- making
FRoomGraphSource::EffectOverBox answer spatially.

It also carries forward the two debts that must be paid BEFORE that lands rather than after: box
bounds computed from strate params can be too optimistic once a per-room op raises them, and AUDIT
C2 is confirmed but unfixed on the switch path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 14:13:59 +02:00
parent ce409e7bb1
commit b533294f5d
3 changed files with 208 additions and 248 deletions
+131 -245
View File
@@ -1,266 +1,147 @@
# Handoff — VoxelForge operator stack, 2026-07-28 (end of day 3) # Handoff — VoxelForge operator stack, 2026-07-28 (Phase 2 complete and green)
> Paste the block below into a fresh session. Everything it refers to is on disk and in git. > Paste the block below into a fresh session. Everything it refers to is on disk and in git.
>
> **State:** Phase 2 is **DONE — 8 of 8 archetypes ported, built, and green** (14 tests).
> `ClassifyTile` consumes `ClassifyBox`. One open warning, one clear next task.
--- ---
You're picking up an agreed refactor of the VoxelForge UE5 voxel plugin, on branch `experimental` You're picking up the VoxelForge UE5 voxel plugin on branch `experimental` (already checked out —
(already checked out — do not create another). I'm Jahni. Design and progress are written down so you do not create another). I'm Jahni. The design and the history are written down so you don't
don't re-derive them. re-derive them.
## Read first, in this order ## Read first, in this order
1. **`CLAUDE.md`** — project rules. **Rule #1 is absolute: never build, compile, or run the editor.** 1. **`CLAUDE.md`** — project rules. **Rule #1 is absolute: never build, compile, or run the editor.**
I build everything myself. When code is done, stop, say "ready to build", list the likely I build everything myself. When code is done, stop, say "ready to build", list the likely
compile-error spots, and wait. compile-error spots, and wait.
2. **`OPSTACK-PROGRESS.md` — THE LAST ENTRY FIRST.** Append-only log of what is built vs merely 2. **`OPSTACK-PROGRESS.md` — THE LAST ENTRY FIRST.** Append-only log; the resume point. The last
written. This is the resume point. entry is the green build with every measured number in it.
3. **`OPSTACK-PLAN.md`** — the plan. **§2.6.1 is the current acceptance bar** and supersedes §2.6. 3. **`OPSTACK-PLAN.md`** — the plan. **§2.6.1 is the acceptance bar** and supersedes §2.6.
4. **`OPSTACK-DECOMPOSITION.md`** — all 8 archetypes broken into ops. §2 TunnelNetwork (the one in 4. **`OPSTACK-DECOMPOSITION.md`** — per-archetype breakdown. **§0.2** (the amplitude bound) is the
progress), §0.2 the worm bound, §7 islands, §5 SurfaceWorld. live one; §2 TunnelNetwork and §8 Underwater are now history, not instructions.
5. **`AUDIT-2026-07.md`** — §C9's library half is the top open risk. **§C10 is SOLVED — don't 5. **`AUDIT-2026-07.md`** — **§C2 has a CONFIRMED sub-item as of 2026-07-28, read it**; §C10 is
reopen.** §C1 was reopened and re-closed on 2026-07-28; §C2 has a new *suspected* item worth SOLVED, don't reopen; §C9's library half is the top open theoretical risk with 0 measured
verifying before acting on. exposure.
6. **`CODEMAP.md`** — navigation. Trust symbol names over line numbers. 6. **`CODEMAP.md`** — navigation. Trust symbol names over line numbers.
## Where things stand — 6 of 8 ported and wired, the 7th half-ported, 13 tests green ## Where things stand — the transition is COMPLETE and VERIFIED
All 8 archetypes have an operator-stack twin, per-strate opt-in, each equivalence-tested **bit for
bit** against its original density function. The `switch` and the stack are now two complete,
interchangeable implementations.
| Archetype | State | | Archetype | State |
|---|---| |---|---|
| `Maze` | ✅ ported, bit-identical, wired | | `Maze` | ✅ ported, bit-identical, wired |
| `FlatPlain` + `CrystalChamber` | ✅ **one op for both**, bit-identical, wired | | `FlatPlain` + `CrystalChamber` | ✅ **one op for both**, bit-identical, wired |
| `SurfaceWorld` | ✅ ported incl. **biome blending**, bit-identical, wired | | `SurfaceWorld` | ✅ ported incl. biome blending, bit-identical, wired |
| `VerticalShafts` | ✅ ported, bit-identical, wired — 3 ops reused from Maze unchanged | | `VerticalShafts` | ✅ ported, bit-identical, wired |
| `FloatingIslands` | ✅ ported, bit-identical, wired — the stack that runs **backwards** | | `FloatingIslands` | ✅ ported, bit-identical, wired — the stack that runs **backwards** |
| `TunnelNetwork` | 🔶 **STAGE A of 3 done and verified.** Not wired — see below | | `TunnelNetwork` | **19 ops**, bit-identical incl. all 12 detail modifiers + per-room override |
| `Underwater` | ❌ TunnelNetwork + a water flag; folds in at stage C | | `Underwater` | ✅ same builder, second `case` — ⚠️ its check has a coverage gap, see below |
Everything is behind `UVoxelStrateDefinition::bUseOperatorStack`; the ported list lives **only** in Everything sits behind `UVoxelStrateDefinition::bUseOperatorStack`; the ported list lives **only** in
`UVoxelStrateManager::UsesOperatorStackForChunk`. Un-ported archetypes ignore the flag, so ticking it `UVoxelStrateManager::UsesOperatorStackForChunk` (now all 8). **No strate asset has the box ticked**
anywhere is harmless. — that is my call and I haven't made it. But the flag is no longer a no-op anywhere: ticking it now
really switches that strate onto the stack, for density *and* for tile classification.
## TunnelNetwork is staged — read this before touching it `ClassifyTile` **consumes `ClassifyBox`** for cave archetypes (SurfaceWorld and bedrock gaps keep
their hand-written exact-lattice proofs). `GetDensityAt` and `ClassifyTile` build the stack through
the **same** factory, `VF_BuildOpStackForChunk` — a second copy would be a hole, not a bug.
`GetDensityWithParams` is ~1080 lines: 13 detail modifiers, a two-region cache, worms, and a per-room ## The one open warning
op override. Porting it whole before anything can be checked would be ~600 unverified lines on top of
~200 — the `AUDIT §P3` pattern this refactor has dodged seven times. So:
- **Stage A — DONE, bit-identical over 6000 samples.** Vertical scale · base rock · cave warp · room ```
graph (+ pits + chimneys) · carve · worms · structural post. **7 ops.** Underwater (stage C2): bit-identical across 2000 samples — 0 of them in open cave (0.0%)
- **Stage B — NEXT.** The 13 detail modifiers of `STEP 4b4h`, gated on `Sdf < SDFBlendRadius·3`. ```
They insert between the carve and the worms, so the op-count assertion in the test must move.
- **Stage C.** The per-room op override (`§2`'s option (a)), the `Underwater` water flag, and only
then does `UsesOperatorStackForChunk` return true for either.
**Why stage A is verifiable while incomplete:** every detail modifier is amplitude-gated, and **A green bit-identity over 2000 samples of solid rock is not evidence** — it is exactly what two
`FStrateGenerationParams` already defaults all of them to zero. Zeroing `SurfaceRoughness` (the one agreeing voids look like. Same failure as stage A's 1.1 % run, in a different slot, caught by a
exception) sends the *original* down exactly the path stage A ported. Stage B's test will do the counter written for it.
reverse — turn them on one group at a time.
**⚠️ The decision that must not be undone:** `FRoomGraphSource` **CALLS** `BuildChunkCache` / A real bug surfaced while diagnosing: the sampled chunk-Z range used `Z / CHUNK_SIZE`, and C++
`EvaluateSDFCached`; it does not transcribe them. That is where `ARCHITECTURE §8.4`'s two-region integer division **truncates toward zero**. TunnelNetwork is at the top of the layout in positive Z
window-invariance discipline lives, and a transcription would *fork* it — with the fork "validated" where truncation == floor, so it could not show there; Underwater is at the **bottom, in negative
by a test that compares it to the original. Only the ~60 lines of glue are transcribed. Z**, where it shifts the upper chunk bound a notch high and the `Clamp` piles samples into the top
seal band. Fixed (`FloorDivChunk`), sampling widened 8 → 24 clusters, **and not trusted**: new
**check 5b** gives each of the three possible causes its own number and prints how to read them.
## Three things Phase 2 settled that were not in the original design **⚠️ That commit (`ce409e7`) is written but NOT BUILT.** First action: build, run the `VoxelForge`
filter, read the `Underwater diagnosis` line. It names the cause; the fix follows from it.
1. **Height space** (`VoxelHeightOp.h`) — a *second operator family*. SurfaceWorld's terrain ops read ## Then the one task everything is waiting on
and write an **altitude**, not a density: no input Z, XY-pure per column. They do not fit
`IVoxelDensityOp`. §0.1 found density needed a second *channel*; this found terrain needs a second
**space**. Bonus: a height stack *cannot* hold Z-dependent data, because there is no Z in the
signature — `AUDIT §6.3`'s hazard became a type error instead of a convention.
2. **`IVoxelBiomeField`** — ops depend on a *capability*, never on `UVoxelGenerator`. The adapter
(`FGeneratorBiomeField`) lives in `VoxelGenerator.cpp`. This is what lets ops become assets in
Phase 3; an op holding a generator pointer never could.
3. **`FRAME` ops are RETIRED — porting all three candidates killed the idea.** `CaveWarp`'s scope is
exactly one operator (pits/chimneys explicitly read *unwarped* coords, the thing §2 called the
fiddliest in the decomposition — inside one op it evaporates). `VerticalScale` is `Z / Scale`, a
one-line pure function. The island warp was already local. **Zero frames from three candidates:**
not missing infrastructure, one idea seen three times from a distance. Marked retired in §1.
## What is left, in the order I'd do it **Make `FRoomGraphSource::EffectOverBox` answer spatially.**
1. **Stage B** — the 13 detail modifiers. The bulk of the remaining lines. TunnelNetwork proves **0 of 40** tiles today, and the test asserts that. The chain dies at the room
2. **Stage C** — per-room op override + `Underwater` + flip both on in `UsesOperatorStackForChunk`. source, which returns `Both` with unknown amplitude before anything downstream is reached. Its room
**The override is NOT optional polish:** pits, chimneys and columns exist *only* through a and tunnel bounds (`FCachedRoom::CullRadiusSq`, `FCachedTunnel::BoundRadiusSq`) are **already in the
per-room `UVoxelTerrainOpDefinition` (`BuildChunkCache` opens its bake with SDF cache**; what it costs is building that cache for the *queried box*, on the querying thread.
`if (!CR.RoomOp) continue;` and then reads a *fresh* param struct). No override ⇒ no pits, ever.
3. **The worm amplitude cap (`DECOMPOSITION §0.2`)** — the largest single perf item in the plan. That cost is now clearly worth paying, and every other piece is already built to receive it:
TunnelNetwork proves **0 of 40** tiles today because a fielded-noise carve has no spatial bound.
But its amplitude is bounded and trivial (`t ∈ [0,1]`, `Mask ∈ [0,1]` ⇒ at most `WormStrength` - `ClassifyTile` consumes `ClassifyBox` in production, so a proved tile skips `GenerateMesh`
toward air), and that bound already has a home in `FWormFieldSource::MaxCarveAmplitude()`. It 30 000+ density evaluations saved against one `BuildChunkCache`;
needs a fold that carries **numbers**, not just directions. - the fold carries **numbers** (`MaxCarveOverBox` / `MaxFillOverBox` / `ForcedMarginOverBox`), so a
4. **`ClassifyTile` still uses hand-written guards.** `ClassifyBox` is brute-force verified per bounded worm no longer kills `AllSolid` on rock that is solid by more than it can carve;
archetype but **nothing consumes it in production**. That is where measured tile-skipping turns - the twelve detail modifiers already **inherit** the room source's verdict via `VF_NoCaveOverBox`
into frames — arguably the biggest single win still on the table. the day the source says `Identity` for a box, all twelve follow, in one place rather than thirteen.
5. **PERF — parked by Jahni until the transition is complete.** The op path is measurably slower.
One cause found and fixed (the column memo discarded itself every chunk). Remaining suspects in **Keep the brute-force check.** `VoxelForge.OpStack.ClassifyTileSoundness` verifies verdicts against
order: the hashed column lookup vs `GSurfColCache`'s direct-indexed box, then per-voxel virtual `GetDensityAt` on a world where every strate opted in. A false verdict is an invisible hole: no
dispatch. **Measure before optimising** — that is the C10 lesson. geometry, **no collision**, until a player falls through it.
6. **`AUDIT §C9` library half** — `sinf`/`cosf` are not IEEE-754 specified, so MSVC's CRT and glibc's
libm can differ. Currently **0 samples within 1e-6 of the isosurface**, i.e. no measured risk. The ## ⚠️ Debts that must be paid BEFORE that lands, not after
real fix, if ever needed, is a deterministic in-house sin/cos. Also: run `CrossPlatformDigest` on
Linux, compare the SHAPE digest, pin it. Both were introduced knowingly and are written at the exact site a reader would land on.
7. **`AUDIT §C2`, suspected** — `GetGenerationParams` blends params across Gradient transitions, so
two chunk Zs inside one strate may hold different params with the same XY box, strate index and 1. **Box bounds read STRATE params, but a per-room op can raise them.** `EffectOverBox` and the new
seed ⇒ the SDF cache never rebuilds ⇒ the lower chunk gets the upper chunk's rooms. **Verify the amplitude bounds are computed from strate params, because a box spans many rooms. But `ApplyTo`
premise before acting** (does Gradient blending actually vary within a strate?). The op stack does writes the op's value **even where the strate's was 0**, so a room op can enable a modifier the
not inherit it — its key folds in a params CRC. strate had switched off, or give it a bigger amplitude. A box verdict on a strate with a
8. **VerticalShafts proves 0 of 60 tiles.** Pessimistic, not wrong: `EffectOverBox` returns terrain-op pool can therefore be **too optimistic** — the dangerous direction. Harmless while the
room source answers `Both` (nothing is provable anyway); **not harmless the moment it doesn't.**
Noted at `FLayerLineMod::EffectOverBox` and `FRoomGraphSource::LocalParams()`.
2. **`AUDIT §C2` is confirmed and unfixed on the `switch` path.** `GetGenerationParams` blends params
*within* a strate (`Alpha` depends on chunk Z for `Gradient`, and on chunk XY too for
`Interleaved`), and `Gradient` + `TransitionBlendChunks = 2` are the **defaults**. The original's
SDF cache key has neither params nor chunk Z, so a worker evaluates the second chunk it builds
against the first chunk's rooms — and *which* chunk came first depends on worker order, so two
peers can diverge from the same seed. The op stack does **not** inherit it (params CRC in the
key), and `ClassifyTile`'s new path guards against it explicitly (params must be bit-identical
across every chunk coord the box touches). The fix on the `switch` path is a params CRC in its
key — a live-generation change that wants a build in front of it.
## After that, in order
1. **PERF — unparked.** The op path is measurably slower. One cause found and fixed (the column memo
discarded itself every chunk). Remaining suspects in order: the hashed column lookup vs
`GSurfColCache`'s direct-indexed box, then per-voxel virtual dispatch. Also measured and stated:
the gate is now tested twelve times per voxel instead of once (stage B5's deliberate trade).
**Measure before optimising** — that is the §C10 lesson.
2. **`VerticalShafts` proves 0 of 60 tiles.** Pessimistic, not wrong: `EffectOverBox` returns
`CarveOnly` whenever any shaft is within a `Spacing*1.6` halo instead of testing real connector `CarveOnly` whenever any shaft is within a `Spacing*1.6` halo instead of testing real connector
capsules. Lost CPU, never a hole. capsules. Lost CPU, never a hole.
3. **`AUDIT §C9` library half** — `sinf`/`cosf` are not IEEE-754 specified, so MSVC's CRT and glibc's
## ⚠️ IF YOU ARE RUNNING UNATTENDED — read this section before starting libm can differ. Currently **0 samples within 1e-6 of the isosurface**, i.e. no measured risk. Run
`CrossPlatformDigest` on Linux, compare the SHAPE digest, pin it. The real fix if ever needed is a
Jahni may start this session and leave, then have you shut the PC down when you're done. deterministic in-house sin/cos.
4. **Phase 3 — ops as data assets.** A design conversation, not a transcription. Don't start it
**You cannot build, and nobody will build for you during this run.** Rule #1 still holds absolutely. unprompted. What makes it possible is already in place: ops depend on capabilities
Everything you write stays unverified until he returns. (`IVoxelBiomeField`), never on `UVoxelGenerator`.
**That is his explicit, repeated decision, not an oversight — do not re-litigate it and do not
quietly scale the work down.** Verbatim: *"I'm fine with unverified work, if committed, I can always
manually go back and try — I want him to work towards the full op stack completeness."* So the goal
of an unattended run is **to finish the transition**, not to stop somewhere safe. Take the queue
below as far as it goes.
Two things make that reasonable rather than reckless, and they are worth knowing:
1. **Committing per group IS the safety net.** He reverts by sha. That is why the one-commit-per-group
rule is the one thing not to bend.
2. **Reaching 8 of 8 does not change his world by itself.** `UsesOperatorStackForChunk` returns false
unless a strate has `bUseOperatorStack` ticked, so adding `TunnelNetwork` and `Underwater` to the
ported list is inert until *he* ticks a box. The flag flip is safe to include; pointing a live
world at it stays his call.
### The rules that replace "build after each step"
1. **One commit per group below. Never bundle two.** Jahni reverts by group, and a failed build must
bisect to a group rather than to 700 lines. Name the group explicitly in the subject line.
2. **Every commit leaves the tree coherent.** No half-written op, no dangling brace, no factory
declared without a definition. Assume the next thing that happens is a compile.
3. **Transcribe literally. Do not improve anything.** With no test feedback, a "cleanup" is an
unfalsifiable guess. Keep the original's operation order, its constants, its `FVector`
round-trips, its odd `FMath::Max` floors. If something looks wrong, **write it down in the
progress log and port it as-is** — that is how `§C1` and the `MinDivisor` split were both found
without breaking anything.
4. **Update the test in the same commit as the code it covers**, including its coverage counters. Do
not leave "I'll add the test after" — after is a build away.
### The work queue, in order. Run it to the end — **8 of 8 is the finish line.**
- **B1 — Surface roughness** (`STEP 4b`, ~130 lines). Density-space variant: a *different op* from
`FSdfRoughnessMod` (quadratic fade, anti-fill clamp, 4 noise types). See `DECOMPOSITION §1`.
- **B2 — Terrace · LayerLines · Ribbing** (~150 lines). The three "sedimentary" remaps.
- **B3 — Overhang · Cliff · Scallop · Arch** (~210 lines).
- **B4 — Columns · Domes · Pinch · FloorBias** (~190 lines). Columns read `SDFCache.Columns`, so this
group needs a Column op in the test's room pool — and the stage-A guard that *errors* on
`TotalColumns > 0` must move to this commit, not before it.
- **B5 — the gate itself.** All of B1B4 live inside `if (bNearCaveSurface)`, i.e.
`Sdf < SDFBlendRadius·3 && Sdf < FLT_MAX`. Decide deliberately whether that is one scoping wrapper
or a repeated early-out in each op, write down which and why, and make the test prove a voxel
*outside* the gate is untouched.
After each group, extend the test: turn that group's amplitudes ON in `EnableTunnelFeatures`, add a
coverage counter proving the group actually fired (**not** just that its param is non-zero — see the
pit/chimney lesson below), and move the op-count assertion.
**⚠️ The subtlety that lets B precede C.** Inside `if (bNearCaveSurface)` the original *shadows*
`Params` with `LocalTerrainParams` — a copy with the nearest room's terrain op applied — and **all 13
modifiers read the shadowed copy**. Porting them against strate-level params is therefore equivalent
*only while no room carries a detail-type op*. The test's pool is Pit/Chimney only and `ApplyTo(Pit)`
writes just the four pit fields, so the two are identical today. **Keep the pool that way through
B1B5**; C1 is what makes a `Terrace` op in the pool legal. (B4's Column op is safe: `STEP 4d` reads
`SDFCache.Columns` from the bake, never the shadowed params.)
- **C1 — the per-room op override.** `§2` calls this the piece with "no clean home" and offers two
options. **Take option (a), and note that the mechanism is already established in this codebase —
do not invent a scoping predicate.** Three ops already read state owned by an upstream op via a
non-owning pointer handed over at build time: `FOverhangShelfMod``FSurfaceColumnSource`,
`FShaftLedgeMod``FShaftFieldSource`. Do the same here:
`FRoomGraphSource` gains `const FStrateGenerationParams& LocalParamsAt(x, y, z) const`, which
reproduces the original exactly — take the voxel's `NearestRoomIdx` (it already computes it), apply
that room's `RoomOp` onto a `thread_local` copy of the params, return it. Every detail op holds a
`const FRoomGraphSource*` and reads its fields from that copy instead of from its own.
This is the pit/chimney resolution a second time: the difficulty in `§2` came from assuming each
modifier must own its params. Let one op own the shared state and the rest read it, and the problem
disappears. **Transcribe the per-voxel copy as-is** — yes, the original copies a ~74-field struct
per voxel inside the gate; note it in the log as a perf item and change nothing.
Acceptance: put a `Terrace` op in the test's room pool alongside Pit/Chimney and require
bit-identity. That is the check B could not make.
- **C2 — `Underwater`.** Nearly free: `§8` establishes there is **no density difference at all**
`GetDensityAt` routes it to `GetDensityWithParams` and `WaterLevelRelative` is consumed by the
render-side water system, not by density. So it is the TunnelNetwork stack, reached from a second
`case`. Add the wiring case and an equivalence test that samples the `Underwater` slot. If you find
a real density difference, **stop and write it down** — that would contradict `§8` and is worth
more as a finding than as code.
- **C3 — 8 of 8.** Add `TunnelNetwork` and `Underwater` to `UsesOperatorStackForChunk`'s ported list.
Update `CODEMAP §3.2d` and its `UsesOperatorStackForChunk` row, tick `OPSTACK-PLAN` Phase 2, and
write the closing `OPSTACK-PROGRESS` entry: the archetype `switch` now has a complete operator-stack
twin, per-strate opt-in, every one equivalence-tested.
### After 8 of 8 — keep going, in this order
1. **Settle `AUDIT §C2`'s suspected item.** Does `GetGenerationParams` actually vary within one strate
across a Gradient transition? Answerable by *reading* `GetGenerationParams` and
`FStrateGenerationParams::Lerp` — no build. Turn the suspicion into a yes or a no with the
evidence, and correct the audit either way. Cheap, and it closes an open question honestly.
2. **The worm amplitude cap (`DECOMPOSITION §0.2`).** The largest single perf item in the plan, and
the reason TunnelNetwork proves 0 of 40 tiles. It needs the fold to carry a **number**, not just a
direction: add a numeric carve/fill amplitude alongside `EVoxelOpEffect`, have
`FWormFieldSource::MaxCarveAmplitude()` (already written, already unused) feed it, and let
`AllSolid` survive when the rock is solid by more than the sum of every remaining carve.
**⚠️ This changes the fold contract that all 13 tests rest on — its own commit, nothing else in
it,** and `VoxelForge.OpStack.BoxVerdictFold` must be extended in the same commit.
3. **Make `ClassifyTile` consume `ClassifyBox`.** Verified per archetype, still unconsumed in
production — this is where measured tile-skipping (Maze 23/60, slabs 3640/60) becomes frames.
A false verdict is an invisible hole, so keep the brute-force check in every archetype's test.
4. **Re-read your own diffs against the original**, group by group, as a reviewer rather than an
author. On a run with no builds, a second reading is the only oracle you have. Do this rather than
starting anything new when the queue runs out.
### Still not yours to do, even unattended
- **Do NOT build, do not launch the editor, do not push.** Unchanged, not negotiable.
- **Do NOT tick `bUseOperatorStack` on any strate asset.** C3 makes the archetypes *available*;
pointing a live world at an unverified path stays Jahni's decision.
- **Do NOT start Phase 3** (ops as data assets). It is a design conversation, not a transcription.
### The commit messages ARE the ledger
**Jahni's words: "just commit with explicit name what changed, I'll revert if needed."** So there is
no separate bookkeeping — the commit history is the record, and it has one job: let him revert a
single group without touching the others.
That makes the subject line load-bearing. Name the group and the scope, not the intent:
```
feat(opstack B2): port Terrace, LayerLines, Ribbing detail modifiers
feat(opstack B4): port Columns, Domes, Pinch, FloorBias + column op in the test pool
feat(opstack C1): per-room op override — detail ops read LocalParamsAt from the room source
feat(opstack C3): TunnelNetwork + Underwater in the ported list — 8 of 8
```
Body: what was transcribed, anything that looked wrong and was ported as-is anyway, and what breaks
first if the group is wrong. One group per commit — that rule exists *because* of the revert
workflow, so it is the one thing not to bend.
**Close the run with one `OPSTACK-PROGRESS` entry** listing every commit made, in order, with a
one-line "what this touches" each, and the test filter to run (`VoxelForge`). Jahni's first action on
return is a single build; that entry is what makes it efficient. Then shut down if he asked you to.
## Hard rules that prevent real bugs ## Hard rules that prevent real bugs
- **Density sign:** negative = solid at the mesher. Inside the op stack the convention is INTERNAL - **Density sign:** negative = solid at the mesher. Inside the op stack the convention is INTERNAL
(**positive = solid**), negated once by the caller. The SDF channel uses standard SDF convention. (**positive = solid**), negated once by the caller. The SDF channel uses standard SDF convention.
- **Never run both density paths in one world.** - **Never run both density paths in one world.** **Comparing them is legitimate** — §C10 is closed
- **The acceptance bar is `§2.6.1`:** *same seed ⇒ same world on every peer*. Resemblance to the since `FPSemantics = Precise`, and all eight equivalence tests compare bit for bit. They are
pre-refactor world is **not** required. The equivalence tests are **port-correctness oracles**, not **port-correctness oracles**, not fidelity checks: §2.6.1 requires *same seed ⇒ same world on every
fidelity checks — keep them for that reason. peer*, not resemblance to the pre-refactor world.
- **Every cache key includes `LayoutVersion` AND the params.** See `§C2` and the overhang regression - **Every cache key includes `LayoutVersion` AND the params.** See §C2 and the overhang regression of
of 2026-07-27, where omitting the params silently deleted the overhang and only 1 sample in 20 000 2026-07-27, where omitting the params silently deleted the overhang and only 1 sample in 20 000
crossed the isosurface. crossed the isosurface.
- `ProcessQueue` stays `EQueueMode::Mpsc`; `Epoch` carries through every async path; don't "optimize" - `ProcessQueue` stays `EQueueMode::Mpsc`; `Epoch` carries through every async path; don't "optimize"
the `ARCHITECTURE §8.10` invariants. the `ARCHITECTURE §8.10` invariants.
@@ -268,7 +149,7 @@ return is a single build; that entry is what makes it efficient. Then shut down
- Update `CODEMAP §3`, `ARCHITECTURE §8`, tick `OPSTACK-PLAN`, append to `OPSTACK-PROGRESS.md`. - Update `CODEMAP §3`, `ARCHITECTURE §8`, tick `OPSTACK-PLAN`, append to `OPSTACK-PROGRESS.md`.
- **When inserting a class into `VoxelDensityOpStack.cpp` / `VoxelHeightOpStack.cpp`, put it ABOVE - **When inserting a class into `VoxelDensityOpStack.cpp` / `VoxelHeightOpStack.cpp`, put it ABOVE
the labelled end of the anonymous namespace.** Anchoring on the FACTORIES banner puts it outside, the labelled end of the anonymous namespace.** Anchoring on the FACTORIES banner puts it outside,
and the brace added with it closes nothing. Made that mistake twice; both files now say so at the and the brace added with it closes nothing. Made that mistake twice; both files say so at the
exact line. exact line.
## Method lessons this refactor actually paid for ## Method lessons this refactor actually paid for
@@ -278,28 +159,33 @@ Ordered by how much they cost.
- **Instrument before hypothesising.** §C10 cost six builds and five refuted hypotheses, then was - **Instrument before hypothesising.** §C10 cost six builds and five refuted hypotheses, then was
solved for free by a build setting changed for an unrelated reason. Park a question whose solved for free by a build setting changed for an unrelated reason. Park a question whose
consequences are measured and benign. consequences are measured and benign.
- **Verify the premise before reasoning from it.** Four times now, a confident chain rested on an - **Verify the premise before reasoning from it.** Five times now a confident chain rested on an
unchecked assumption and the check reversed it: C1's *documented* fix was wrong; "C9's risk is gone unchecked assumption and the check reversed it: C1's *documented* fix was wrong; "C9's risk is gone
after FPSemantics" was wrong; "C1 is closed, 0 sites left behind" was wrong (the sweep matched a after FPSemantics" was wrong; "C1 is closed, 0 sites left behind" was wrong (the sweep matched a
*spelling*, `SeedF * K`, and the survivor spelled it `(float)S * K`); "PitDensity enables pits" was *spelling*); "PitDensity enables pits" was wrong; "there are 13 detail modifiers" was wrong (twelve,
wrong. **A grep over a spelling is evidence about the spelling.** and only eleven read the per-room copy). **A grep over a spelling is evidence about the spelling.**
- **Read the code, not the comment.** The cliff modifier's comment promises a sampled Z±1 gradient;
the code samples nothing and uses a Z-stretched Perlin it *calls* `VertGrad`. Ported as written —
and written down, so nobody "fixes" it from the comment.
- **A perf change can be a correctness change.** The column-memo optimisation silently deleted the - **A perf change can be a correctness change.** The column-memo optimisation silently deleted the
overhang; the tests caught it the same day. The failure was invisible to inspection and produced overhang; the tests caught it the same day. Invisible to inspection, and it produced plausible
plausible terrain. terrain.
- **Coverage is a number, not a boolean.** Three related traps, each of which produced a green run - **Coverage is a number, not a boolean.** Four related traps, each of which produced a green run
that proved almost nothing: that proved almost nothing:
- *A test that prints nothing on success is indistinguishable from one that never ran.* Report - *A test that prints nothing on success is indistinguishable from one that never ran.*
counts, not just failures. - *A guard that only trips at zero notices absence, it does not measure coverage.* Use fractions.
- *A guard that only trips at zero does not measure coverage, it notices absence.* A run with 65 of
6000 samples in open cave (1.1 %) passed a `== 0` guard silently. Use fractions.
- *A success message that **asserts** coverage instead of reporting it reads as evidence while - *A success message that **asserts** coverage instead of reporting it reads as evidence while
measuring nothing.* One said "pits and chimneys exercised" through a whole run in which zero pits measuring nothing.*
existed. - *A check can be vacuous as well as a counter.* "Nothing leaked" is worthless unless something
happened — so the gate check also reports how many samples move when the modifiers are zeroed.
- **Enabling a feature is not evidence it fired — ask the structure, not the output.** Setting - **Enabling a feature is not evidence it fired — ask the structure, not the output.** Setting
`PitDensity` did nothing (wrong struct). Diffing two stacks with/without the op pool would have `PitDensity` did nothing (wrong struct). Diffing two stacks with/without the op pool would have
*lied* (the pool is not in the SDF cache key, so both share the `thread_local` cache). What worked: *lied* (the pool is not in the SDF cache key, so both share the `thread_local` cache). What worked:
call `BuildChunkCache` and look at `Pits.Num()`. Prefer the check that can fail for exactly one call `BuildChunkCache` and look at `Pits.Num()`. **Prefer the check that can fail for exactly one
reason. reason** — and when a zero has three possible causes, give each one its own number.
- **An oracle that shares the defect under test proves nothing.** The stale-cache check was going to - **An oracle that shares the defect under test proves nothing.** The stale-cache check compares each
compare two interleaved param sets against the original — which keys its SDF cache without the stack against *itself evaluated alone*, never against the original — which keys its SDF cache
params and would have failed. Compare against the thing itself evaluated alone instead. without the params and would fail it.
- **One definition, not two kept in sync.** `VF_BuildOpStackForChunk` exists because a tile skipped on
the verdict of a stack that is not the one producing its density is a hole. A "keep these in sync"
comment would not have been enough.
+5 -3
View File
@@ -10,9 +10,11 @@
> FloatingIslands · TunnelNetwork · Underwater.** The archetype `switch` now has a complete > FloatingIslands · TunnelNetwork · Underwater.** The archetype `switch` now has a complete
> operator-stack twin, opt-in per strate. > operator-stack twin, opt-in per strate.
> >
> ⚠️ **The last three groups (stage B, C1, C2, C3) are UNVERIFIED — written, committed, never > **BUILT AND GREEN, 2026-07-28 — 14 tests.** TunnelNetwork A+B bit-identical over 6000 samples
> compiled.** Run the `VoxelForge` filter; `OPSTACK-PROGRESS.md`'s last entry lists every commit and > with all twelve group-coverage probes non-zero, all four noise branches covered, 0 gate leaks, and
> what breaks first if one is wrong. > C1 proved by 10 Terrace-op rooms containing 1119 samples. One open warning: the `Underwater` check
> landed 0 samples in open cave, so its bit-identity proves little — diagnosed, not guessed, in
> `OPSTACK-PROGRESS.md`'s last entry.
> >
> **Not done, and it is the next real prize:** `ClassifyTile` still uses hand-written guards and does > **Not done, and it is the next real prize:** `ClassifyTile` still uses hand-written guards and does
> not consume `ClassifyBox`. That is where measured tile-skipping becomes frames. > not consume `ClassifyBox`. That is where measured tile-skipping becomes frames.
+72
View File
@@ -2395,3 +2395,75 @@ difference *is* the T1.d gain.
After that: make `FRoomGraphSource::EffectOverBox` answer spatially. Everything else is now waiting After that: make `FRoomGraphSource::EffectOverBox` answer spatially. Everything else is now waiting
on it, and the two commits above were built to receive it. on it, and the two commits above were built to receive it.
---
## 2026-07-28 — **IT ALL COMPILES AND IT IS ALL GREEN.** 14 tests. 8 of 8, verified.
Jahni built it. Everything written across the unattended run is now verified except one number.
```
TunnelNetwork STAGE A+B ... bit-identical, 6000 samples (1002 open cave, 4699 rock)
Cave coverage ............ 16.7 % (floor 10 %)
Group coverage ........... roughness 64.8 % · terrace 60.2 % · layer lines 25.7 %
ribbing 13.4 % · overhang 39.4 % · cliff 26.2 %
scallop 1.6 % · arches 3.4 % · domes 1.6 % · pinch 1.0 %
floor bias 18.2 % ← ALL TWELVE NON-ZERO
Roughness noise sweep .... 8/8 variants bit-identical over 1500 samples
Gate check (B5) .......... 979 outside the gate, 0 leaked; 4880 inside move
Params fingerprint ....... 395/400 = 98.8 % differ, 0 served wrong
Bake coverage ............ 49 rooms, 26 pits, 30 chimneys, 39 columns
Per-room override (C1) ... 51/51 rooms carry an op, 10 Terrace, 1119 samples inside them
Box verdicts ............. 0 proved, 40 Mixed (asserted)
Underwater (C2) .......... bit-identical, 2000 samples — 0 in open cave ⚠️
```
**Every claim this run made about correctness now has a measurement behind it.** The twelve
modifiers are transcribed correctly *and* each demonstrably fired; the four noise branches are
covered, not just the one; the gate suppresses everything outside it with zero leaks; and C1 is
proved by the only check that could prove it — 10 rooms drawing a `Terrace` op whose params
overwrite the strate's, with 1119 samples inside them, still bit-identical.
### The one number that is a warning, and it is the test working
`Underwater: 0 of 2000 samples in open cave.` **A green bit-identity over 2000 samples of solid rock
is not evidence** — it is exactly what two agreeing voids look like. Same failure as stage A's 1.1 %
run, in a different slot, caught by a counter written for precisely that.
**A real bug surfaced while diagnosing it.** The sampled chunk-Z range used `Z / CHUNK_SIZE`, and
C++ integer division **truncates toward zero**. TunnelNetwork sits at the top of the layout in
positive Z, where truncation and floor agree — so the bug could not show there. Underwater sits at
the **bottom, in negative Z**: `-1 / 32` is `0` truncated and `-1` floored, so the upper chunk bound
starts a notch too high and the `Clamp` piles the excess onto the strate's last voxel, inside the
top seal band, i.e. solid rock. Fixed in both point builders (`FloorDivChunk`). Same family as the
`DivideAndRoundDown` lesson already in the project notes.
That is a **candidate**, not a conclusion, so the fix does not stand alone: new **check 5b** gives
each of the three possible causes its own number — rooms baked for the Underwater strate index (the
bake), samples landing inside the seal-free interior (the Z range), and both non-zero (the XY
spread) — and prints how to read them. Sampling also widens 8 → 24 clusters. **Unverified: this
commit has not been built.**
### What the green run settles, beyond the tests
- `AUDIT §C10` stays closed: eight archetypes now compare **bit for bit** against their originals
across every detail path, including four noise types and a domain warp.
- The `FRoomGraphSource::FState` hoist, the lazy `LocalParams()` memo and the whole C1 mechanism are
compiled and exercised — the pattern "one op owns the state, the rest read it" now has three
users, all green.
- The extraction of `VF_BuildOpStackForChunk` did not disturb `GetDensityAt`: the equivalence tests
that route through it are unchanged.
- `0 proved, 40 Mixed` remains **asserted** for TunnelNetwork. That is not a defect and not stale:
the chain still dies at `FRoomGraphSource::EffectOverBox`, which is the next piece of work.
### Next single action
Re-run the filter and read **one line**: `Underwater diagnosis`. It names which of the three causes
produced the 0 %, and the fix follows from that rather than from a guess.
Then the one thing everything else is now waiting on: **make `FRoomGraphSource::EffectOverBox`
answer spatially.** Its room and tunnel bounds are already in the SDF cache; what it costs is
building that cache for the queried box, and that is now clearly worth it — a skipped tile saves
30 000+ density evaluations, `ClassifyTile` consumes `ClassifyBox` in production, the numeric
amplitude fold is in place, and the twelve modifiers already inherit the source's verdict. Every
piece is built to receive it and nothing else moves until it lands.