Commit Graph
100 Commits
Author SHA1 Message Date
Fr0zkaandClaude Opus 5 0388c7dede docs: Sol's read-only audit, verbatim
The full 34 KB assessment that WORK-NEXT.md was built from, kept in the repo so any
future session has the reasoning rather than my summary of it. Read-only audit by
gpt-5.6-sol at xhigh; nothing was modified when it ran.

Its two-sentence verdict:
  'The operator abstraction leaked into the innermost loop, and streaming treats
   work submitted/applied as safe to stand on.'

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-12 03:42:31 +02:00
Fr0zkaandClaude Opus 5 081cc5e459 fix(streaming): collision-ready gate -- the pawn waits for a COOKED floor, not a submitted one
WORK-NEXT item 1, plus all three of item 6. Item 2 (the fused evaluator) untouched as
instructed.

⭐ RMC DOES EXPOSE WHAT WE NEEDED, so the gate rests on a real signal:
  OnCollisionBodyUpdated                              (delegate -- diagnostic)
  SetCollisionConfig() -> TFuture<ERealtimeMeshCollisionUpdateResult>  (AUTHORITATIVE)
RealtimeMeshComponent was READ ONLY, per the owner's grant. Nothing there was modified.

THE INVARIANT
Until now nothing guaranteed a tile the player occupies has collision. apply_s ended at
mesh SUBMISSION; RMC cooks asynchronously and we recorded no completion, so no
measurement this project has ever taken said when physics could actually collide.

  - per-tile CollisionReadyTiles state, with stale-callback protection
  - HandleTileCollisionCookComplete driven by RMC's cook future
  - pawn gated at BeginPlay and pre-physics on the FEET-SUPPORT tile -- deliberately not
    the actor-centre tile, because the floor that catches a falling pawn is what must be
    cooked -- plus the predicted movement path and solid/air traversal
  - movement-component state saved/restored, so a temporary stream wait cannot
    permanently change a character's movement mode
  - new trace fields: mesh_submit_to_cook_s, collision_request_to_cook_s,
    body_event_to_future_s -- three boundaries where we had one blurred number

ITEM 6, all three:
  - LoadedTiles is now recorded only after SUCCESSFUL mesh submission, and a failed
    mesh init retries. Previously a failed init left a tile marked loaded forever -- a
    permanent hole.
  - origin spawning fixed (the zero-position guard made a pawn at exactly
    FVector::ZeroVector indistinguishable from "no pawn" -- and the owner spawns at 0,0)
  - exact core-lattice validation for uniform whole-tile verdicts, which previously had
    no safety net while block verdicts did

⚠️ I FIXED TWO COMPILE ERRORS MYSELF rather than spend another Codex round on them:
`GetMovementMode()` / `GetCustomMovementMode()` do not exist on
UCharacterMovementComponent -- MovementMode and CustomMovementMode are public fields.
Two lines. Codex did not build this round, so it could not have caught them.

VERIFIED BY ME: build 0 errors after the patch; export 1.022 / 1.055 s (aa62971 was
1.034 / 1.089); geometry SHA-256 b3e5f4c3..9b377 UNCHANGED -- generation is untouched,
as it should be for a streaming-lifecycle change.

⚠️ UNVERIFIED AND IMPORTANT: the submission-to-cook gap is still unmeasured -- Codex had
no live process to measure it in. The instrumentation exists and will report it on the
next play session. And the GATE ITSELF IS UNTESTED IN MOTION: if a support tile never
becomes collision-ready, the pawn stays frozen rather than falling. That trade is
deliberate, but the owner should watch for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-12 03:38:58 +02:00
Fr0zkaandClaude Opus 5 58bdfb32de docs: WORK-NEXT.md -- the queue after Sol's read-only audit
Ordered by importance then size. Sol (read-only, xhigh) audited experimental @ aa62971
against main and delivered the two-sentence diagnosis this queue is built on:

  "The operator abstraction leaked into the innermost loop, and streaming treats
   'work submitted/applied' as 'safe to stand on.'"

Two corrections it made to us, both recorded in the doc:

- The fall-through is NOT a removed correctness barrier. main has no spawn gate
  either, marks tiles loaded before applying, and collides only LOD0. It was fast
  enough to hide the race. The owner's evidence was real; the inference was not.
- The loop inversion did not recover the 8x because it traded PER-SAMPLE
  short-circuiting (~70% of deep-solid samples skipped the cave suite) for per-block
  pruning. The fix is lowering the operator graph into a fused evaluator, not more
  dispatch removal.

It also found that RMC cooks collision ASYNCHRONOUSLY and we never record
cook-complete -- apply_s ends at submission, so nothing we have ever measured says
when physics could actually collide.

Reading Plugins/RealtimeMeshComponent is now permitted for item 1, READ ONLY, by the
owner's decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 22:01:47 +02:00
Fr0zkaandClaude Opus 5 aa62971047 tools(trace): Play-to-steady-state timeline -- startup is 83ms, the other 8.2s is tiles
Measurement only, no optimisation, per the instruction. First time anyone has looked at
the path BEFORE LoadTile, and first time any measurement has used the owner's REAL
assets instead of the commandlet's synthetic strates.

Trace: Saved/StartupTraceRealFinalTrace.startup.jsonl -- 873 JSONL records, 268 KB,
841 tile records, bounded at 4096 with zero suppressed. Structured, own file,
stream-parseable; the 13 GB log pile taught that lesson twice.

TIMELINE
  BeginPlay                    45.585 ms
  StrateManager.Initialize     29.018 ms
  GeneratePassages              6.628 ms   (5 passages)
  first BuildDesiredTiles       2.710 ms
  ------------------------------------------
  all startup stages           ~83 ms
  BeginPlay -> steady state     8.216 s
  process launch -> stop       20.164 s

⚠️ My hypothesis was wrong: I expected layout resolution or GeneratePassages to be a
hidden one-off cost. They are 83 ms combined. **The 8.2 seconds is tiles, all of it.**
A further ~2.05 s sits between BeginPlay finishing and the first desired-set build, and
the trace attributes that to engine/map/player startup, not to us.

REAL ASSET RESOLUTION -- two things worth the owner's attention
  settings                /Game/VoxelForge/DA_Settings, seed 0
  ⭐ five slots, ALL resolving to /Game/VoxelForge/DA_Strate3
  InterStrateGapChunks    2
  MaxClipLevel 4, ClipRadius 3, RenderDistanceChunks 0
  bFarSheetRing true, but NO sheet tiles exist because render distance is 0
  ⭐ bEnableDensityVolume FALSE -- already off, no volume object, no GPU upload

DA_Strate1 (the 10.7 KB surface strate) and DA_Strate2 (rooms + water) are not in play
at all. Every strate in the world is Strate3.

THE DESIRED SET -- 841 tiles, and the shape is the story
  LOD0  343 tiles    8 m each     56 x 56 x 56 m
  LOD1  218         16 m         112 x 112 x 80 m
  LOD2  120         32 m         224 x 224 x 96 m
  LOD3   80         64 m         448 x 448 x 128 m
  LOD4   80        128 m         896 x 896 x 256 m

⭐ LOD4 covers 896 x 896 x 256 m -- about 205 MILLION cubic metres of cave -- and its
verdicts are 0 AllAir / 0 AllSolid / 80 Mixed. Not one LOD4 tile can be skipped, and
each costs 127.5 ms p50 / 291.6 ms max to generate. That is roughly 12 s of CPU spent on
geometry nobody can see from inside a cave.

TILE DISTRIBUTIONS (p50 / p95 / max ms)
  LOD  tiles  verdicts A/S/M   req->ready        generation        mesh
   0    343   98/123/122    44.9/420.8/819.5    9.6/358.2/767.6   0/341.6/726.1
   1    218   49/83/86      31.1/148.9/188.6    8.7/134.3/164.2   0/117.8/151.1
   2    120   0/39/81       34.0/61.6/76.9     10.9/43.6/63.7   9.9/40.4/60.5
   3     80   0/2/78        46.4/85.9/96.3     38.5/80.7/87.8  31.0/67.5/70.7
   4     80   0/0/80       148.2/272.8/304.2  127.5/253.8/291.6 127.4/253.8/291.5
  totals: 147 AllAir, 247 AllSolid, 447 Mixed, 669,834 triangles
  apply never exceeds 0.43 ms at any level
  zero classifier-cache and region-cache hits on a fresh process, as expected

⇒ The two costs are LOD0's 122 meshed tiles (~360 ms p95 each) and LOD4's 80
unskippable tiles (~150 ms each). Apply, queue and startup are all noise beside them.

VERIFIED BY ME: build 0 errors; export 1.034 / 1.089 s (5ddd966 was 1.013-1.075, so the
instrumentation does not distort); geometry SHA-256 b3e5f4c3..9b377 UNCHANGED. Game peak
RSS 2.07 GB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 20:03:24 +02:00
Fr0zkaandClaude Opus 5 5ddd96664d perf(opstack): invert the loop + prune per block -- 1.45s to 1.02s, byte-identical
The architecture round. Loop inverted from per-sample-per-op to per-op-per-block,
operators pruned per block by interval bounds, profiler moved out of the hot loop.

  my build, 3 runs:  1.013 / 1.036 / 1.075 s   (adb34df was 1.418 / 1.445 / 1.458)
                     mean 1.041 vs 1.440  =  -28.5%, 1.39x
  geometry SHA-256   b3e5f4c3..9b377  IDENTICAL to adb34df

  pruning removed 20,999 / 31,320 operator-block visits = 67.1%
  surviving ops per block:  LOD0 6.63  LOD1 6.30  LOD2 5.23  LOD3 4.41  LOD4 3.89
                            (out of 22)

⚠️ I PREDICTED THE DISPATCH OVERHEAD WAS "THE 10x". IT WAS NOT.
864,688 virtual dispatches per chunk sounded decisive and is worth ~20-28%. Real, and
worth having, but the order of magnitude is elsewhere. That is my fourth wrong
performance prediction in this project, and the pattern is identical each time: I reason
from a call site's shape instead of measuring what the data actually costs.

⭐⭐ AND THE "5-20x GAME/EXPORT GAP" WAS MY ARITHMETIC ERROR. IT DOES NOT EXIST.
I told the owner the largest unexplained number in the project was that export does
22 ms/tile while the game does 115-500 ms. That 22 ms came from dividing 1.4 s of WALL
CLOCK by 64 tiles -- ignoring that 15 workers ran them in parallel. Per-job means on the
same build:

  nonshared block export   447 ms/job
  shared-grid export       261 ms/job
  game LOD0 meshing        253 ms/tile p50

They are the same. The game is not anomalously slow; generation simply costs ~250 ms per
LOD0 tile. The shared grid's benefit is real but modest, and a neighbourhood cache is NOT
justified by these numbers -- I would have had Codex build one on the strength of a
division error.

GAME PATH, final build (841 profiles, 447 applies)
  meshed p50/p95/max   LOD0 253/521/697 ms   LOD1 79/130/135   LOD2 19/42/51
                       LOD3 36/64/81         LOD4 93/238/271
  LOD0 request-to-ready  p50 47.7 ms  p95 389 ms  max 798 ms
                         worker queue p95 84 ms, generation p95 344 ms
  at 6 m/s: p95 = 2.3 m of travel, max = 4.8 m
  empty LOD0 tiles never enter the mesher; classifier p50 8.7 ms, p95 17.2 ms
  apply is not the bottleneck: p50 0.107 ms, p95 0.161 ms, max 0.296 ms

⭐ THE BINDING CONSTRAINT IS COMPUTE, NOT POLICY:
  logical cores 16, automatic core cap 14, effective generation cap 14, CAP HIT.
Apply limits never bind. We are saturating the machine, which means the remaining wins
must come from doing less work, not from scheduling it better.

NOT DONE, with reasons rather than excuses:
- Coarse-first LOD refinement: coarse tiles carry NO COLLISION, so streaming them first
  would let the player fall through. Needs a conservative collision proxy or an L0
  handoff first. Correct refusal.
- Heading prefetch: desired tiles are sorted by distance only (VoxelWorld.cpp:1748).
  Velocity-weighted prediction with a bounded budget and expiry is the shape, not built.

A failed experiment, reported rather than buried: eagerly filling every classifier leaf
with EvalBlock regressed export to 3.2-3.3 s because it defeated the classifier's early
exit. Removed.

Build 43 actions, 0 errors. Peak RSS under 2.1 GB. Sources: Epic's Tasks System and
Level Streaming docs, Lorensen-Cline.

Cumulative on this region: 342.2 s -> 1.02 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 18:09:12 +02:00
Fr0zkaandClaude Opus 5 adb34dffa9 feat(passages): passages are D-tunnels now -- legacy support writer fires ZERO
The owner was right and said so plainly: "the tunnels should be a flat D with then a
bit of noise added, as it's most basic shape -- this alone should make all tunnels not
over X steepness to be walkable." Codex had reached the same conclusion from the other
direction ("the native fix is to author the inter-strate passage floor profile during
passage construction"); it had measured the current position while he described the
destination. This is that refactor.

Walkable inter-strate passages now bake their floor profile and bounded relief during
GeneratePassages. Evaluation composes a D-shaped SmoothMax floor, with final MC
ownership after the generic writers -- the same ordering fix that made the tunnel floor
native in 2260c4d.

  native compositions      7,629
  legacy support fires         0
  floor-profile fallbacks      0

  player-fit cells    20,804 -> 20,830   (+26)
  walk-reachable      10,884 -> 10,909   (+25)
  climb-reachable              10,911
  connectivity        arrival -> departure, Connected
  largest component           10,909

⭐ Support-ON and support-OFF now produce IDENTICAL export geometry (hash 07C14005,
82,273 v / 153,346 t, mesh 2.981 s vs 2.960 s). The writer that previously cost
3,359 reachable cells when disabled is now inert -- the shape provides what it used to
patch. That is the ~17.43% of banded pixels from 4f8b4a9 addressed at the source.

The legacy writer stays only as a fail-closed fallback for a malformed/non-native
passage. It is no longer load-bearing.

PUSHBACK, and it is the right restraint: only passages carrying
`bWalkableTunnelContract` -- currently the authored `Straight` style -- receive the
guaranteed D-floor. Spiral, Cascading and Worm styles do NOT silently gain a
walkability promise without a separate gradient proof. Correct: a guarantee nobody
proved is worse than no guarantee.

ROUGHNESS -- the owner's report, measured properly this time and CLOSED
He confirmed twice that changing roughness type/frequency changed generation time from
startup. The earlier sweep was a single 128^3 export at LOD0 and could not have seen
it, so this one used the game path, fixed streaming centre, identical 850-tile workload:

  skip rate   LOD0 70.8%  LOD1 57.3%  LOD2 32.6% (Cellular 31.8%)  LOD3 2.5%  LOD4 0%
  gen p95 s   FBM .472/.369/.083/.087/.145   Ridged .473/.365/.084/.084/.143
              Mixed .474/.358/.084/.090/.149  Cellular .479/.360/.083/.089/.161
  FBM frequency 0.025-0.2: flat across every LOD

No classifier-bound collapse by type or frequency. The startup difference was
workload/streaming behaviour, not a type-specific bound defect. No unsafe bounds change
was made to chase it.

⚠️ Worth noting for later: the skip rate collapses with coarseness -- 70.8% at LOD0 to
0% at LOD4. That is expected (a LOD4 tile spans a huge volume and almost certainly
contains something) but it means the classifier does nothing for exactly the tiles that
cover the most ground.

VERIFIED BY ME: build 0 errors; export 1.418 / 1.445 / 1.458 s (4f8b4a9 was
1.353 / 1.430 / 1.432); 153,346 triangles. Peak game RSS ~2.05 GB.

Sources consulted: Epic's UE task-systems documentation for the worker/game-path
distinction, and Lorensen-Cline for the discrete isosurface context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 16:29:49 +02:00
Fr0zkaandClaude Opus 5 4f8b4a9a27 fix(gen): the phantom terracing is the PASSAGE support writer -- plus a region verdict cache
The owner played the build and reported four things. Three are answered with evidence,
one is disproven, and my own leading hypothesis was wrong.

⭐ 1. THE PHANTOM TERRACING IS NOT MARCHING-CUBES ALIASING
He reported stairs surviving with bTunnelFloorTerracingEnabled = false. I suspected MC
lattice aliasing on a near-planar floor; I was wrong. An A/B over the same mesher,
counted in pixels:

  graph-tunnel floor removed     2,813 / 147,456 px changed   1.91%
  passage support removed       25,695 / 147,456 px changed  17.43%

⇒ The bands are the INTER-STRATE PASSAGE SUPPORT WRITER, which is independent of the
tunnel terracing flag. That is exactly why turning terracing off did nothing: he was
disabling the wrong system. FCaveTerraceMod is inactive (TerraceStepHeight=0) and was
never involved.

⚠️ It cannot simply be switched off. With it disabled:
  fit 20,804 -> 20,511, walk-reachable 10,884 -> 7,525, components 260 -> 272.
Route still connected, but a third of reachable space gone. It stays as a correctness
backstop. The native fix is the same pattern we just applied to tunnels: author the
inter-strate passage floor profile during passage CONSTRUCTION. Codex judged that
refactor too large to risk this round and said so rather than attempting it.

MC aliasing remains theoretically possible at shallow angles (Lorensen & Cline 1987),
but the same-mesher A/B proves our writer dominates. Dual Contouring (Ju et al.) was
considered and rejected as a disproportionate mesher/topology change.

2. THE ROUGHNESS COST DEFECT DOES NOT EXIST -- my hypothesis, cleanly disproven
  FBM 2.91s / 66.92% skips · Ridged 2.85s / 66.53% · Mixed 2.92s / 66.82%
  Cellular 2.99s / 66.43% · FBM frequency 0.025-0.5: 2.88-2.99s, 66.80-67.04%
I predicted a shared bound across noise types collapsing the classifier. The code
already carries separate conservative bounds per type. No roughness code was changed.
⇒ Whatever the owner saw is not steady-state export cost. Most likely the in-editor
regeneration a parameter change triggers, which is the full-rebuild cost, not the
per-sample cost. Worth confirming with him.

3. ROOM MOUTH RISE -- his idea, implemented, off by default
  RoomMouthRiseBlendVoxels, default 8.0
  blend 8   fit 20,804  reach 10,884  40,429 v / 75,416 t   capability-NEUTRAL
  blend 24  fit 20,792  reach 10,874  40,337 v / 75,232 t   REJECTED, loses capability
Relief preserved at blend 8 (dihedral p50 2.5584, p90 33.7191). The chosen camera did
not see a mouth bench, so no visual claim is made.

4. ⭐ VERDICT CACHE WITH REGION REUSE -- answers "why re-prove what we proved?"
  130 classification calls -> 1 store, 129 hits, of which 128 ENCLOSING-REGION hits.
The region level is the one that matters: deep rock is empty thousands of chunks at a
time, and one proof over a large box now serves all of them.

Soundness, which is the whole risk (a stale "empty" silently deletes geometry):
512-entry bounded LRU; stores ONLY AllAir/AllSolid, never Mixed; key includes seed,
layout/lifetime id, params fingerprint, diff version, density/seal settings, support
mode, lattice step, origin and extent; dynamic season/composer/terrain-op/biome
configs FAIL CLOSED and bypass reuse. Deliberately session-resident, not on disk --
cross-process persistence cannot serialise all asset/layout/edit state safely yet.

VERIFIED BY ME: build 0 errors; export 1.353 / 1.430 / 1.432 s (2260c4d was
1.320 / 1.424 / 1.439); geometry SHA-256 b3e5f4c3..9b377 IDENTICAL to 2260c4d.
Peak RSS ~1.75 GB. Determinism repeat equal.

⚠️ My composer hypothesis was also wrong: I suspected
`bTunnelFloorTerracingEnabled = CategoryRng.FRand() < 0.75f` in the composer was
overriding his setting. It is not the cause here. The design question it raises is
still open and worth the owner's view: should a composer roll be allowed to overwrite
an explicitly authored value? Otherwise "I turned it off" never reliably means anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 13:13:28 +02:00
Fr0zkaandClaude Opus 5 2260c4d47a perf(gen): the floor is finally NATIVE -- clamp 29,634/29,634 -> 0, and the 551ms empty tile is dead
Two targets, both hit, and the second is the biggest in-game win since the interval work.

1. THE FLOOR IS NO LONGER PATCHED IN
The audit proved the backstop was doing the work: TunnelAuthoredFloorProtectionClamps
fired on EVERY authored floor sample while the named fallbacks fired zero. The cause,
in its own words, was that the stack composed the floor before the common
disturbance/passage writers, so every sample looked like a post-hoc clamp.

The floor is now composed ONCE, after those writers, at the final MC boundary. The
clamp inside the operator stack is deleted; the metadata stays on the sample for the
hand-off and for diagnostics but may no longer modify density.

  clamp fires        29,634 / 29,634  ->  0
  player-fit cells   20,804  (unchanged)
  walk reachable     10,884  (unchanged)
  connectivity       arrival -> departure, Connected
  geometry SHA-256   b3e5f4c3..9b377  IDENTICAL to 8e42d6d
  export, my 3 runs  1.320 / 1.424 / 1.439 s   (8e42d6d: 1.357 / 1.365 / 1.420)

⭐ Byte-identical output with the patch removed entirely. That is the proof the clamp
was never adding anything -- it was compensating for its own position in the pipeline.
Three days of "is the floor native yet" answered by moving where it is composed.

Remaining, and deliberately: the final support backstop still fires (6,509 export /
9,700 walk). Removing it is not capability-safe and it was not removed.

2. THE 551 ms EMPTY TILE IS GONE
An EMPTY LOD0 tile cost 551.7 ms: it returned verdict=Mixed, ran the full mesher, and
produced no triangles. Pure waste -- work done to discover there was nothing to do.

The cause is precise and worth recording: `AnyPassageModifierNearLattice` fed
`FPassageCarveOp::EffectOverBox` with a SPATIAL candidate test. Returning true killed
the AllSolid hypothesis even when every exact MC sample had a zero carve factor. A
"maybe something is near here" answer was being used where "does it actually change
anything at these samples" was required. It now takes the cheap domain test as a
reject, then consults the exact cached lattice carve factor the fold already computes.
Unknown or non-finite geometry stays conservative.

  empty Mixed fallbacks   86  ->  0      no empty tile enters the mesher at all
  empty tile p50      48.7 ms  ->  7.14 ms
  empty tile p95     451.3 ms  ->  11.54 ms
  empty tile max     551.7 ms  ->  27.86 ms

⚠️ Not fully at the "few ms" target: the remaining 27.86 ms tail is conservative
tunnel-tail proof work, not the old fallback. Honest, and a different problem.

Build 0 errors, three source files, no RMC or external paths touched. The shared
walkable-volume descriptor was not started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 05:55:43 +02:00
Fr0zkaandClaude Opus 5 5156084cac audit(gen): which post-hoc ops are tunnel-floors in disguise -- and the X+2A budget is unsound
The owner's framing for the night: "if it sees performance opportunity by unifying
some process that should be, or like the tunnels, have it be native more than patching
it after generation". This is the audit. No generator behaviour changed; the only code
here is request/generation timing instrumentation in VoxelWorld.

STRUCTURAL-POST AUDIT -- verdict per operator
  FBoundarySealOp    0.000011 us/sample  KEEP. A boundary condition, not a workaround.
  FXYEdgeSealOp      0.000054 us/sample  KEEP. Domain constraint; identity today with
                                         WorldRadiusVoxels=0.
  FOriginSpineOp     0.000588 us/sample  PARTLY family, but origin anchoring and
                                         ordering are special.
  FPassageCarveOp    0.019977 us/sample  Could share the volume/floor representation,
                                         but its boundary-crossing order and
                                         current-density carve are load-bearing.
  floor protection   0.061298 us/sample  ⭐ THIS IS STILL THE REAL PATCH.

  TunnelAuthoredFloorProtectionClamps fired 29,634 / 29,634 -- every authored floor
  sample -- while the named support/room fallbacks fired ZERO. So the backstop is not
  a rare safety net; it is doing the work, and the tunnel floor is not yet native.

⭐ AND IT NAMED THE PATH OUT, which I had not seen: the floor is authored at build time
  but LATER WRITERS can still refill or erase the clearance, so a clamp re-asserts it.
  Native means either composing the floor AFTER all writers, or making every later
  writer floor-aware with a proof it cannot violate the invariant. Removing the clamp
  today would be unsafe. It did not remove it.

PRIMITIVE FAMILY -- valid, but my proposal was too crude
  Algebraically sound: solid + floor via SmoothMax + bounded relief + smooth union.
  Rooms, swept tunnels, landing rooms and some passages can share a cached
  walkable-volume DESCRIPTOR (shape/profile, baked floor, relief params, mouth/room
  ownership, capability metadata).
  ⛔ But they should NOT become one undifferentiated operator: tunnels are swept
  multi-segment chains, passages cross structural boundaries, origin landings have
  special finite geometry, seals are domain conditions. Flattening those would WEAKEN
  interval proofs and operation ordering -- the opposite of what I hoped unification
  would buy. Renaming the passage carve would not help either; it Lerps against
  incoming density, so its global bound stays unbounded regardless.

X + 2A CLEARANCE BUDGET -- SETTLED, AND IT IS UNSOUND AS SPECIFIED
  A surface-displacement budget needs BOTH an upper bound on field perturbation AND a
  lower bound on the gradient near the zero surface. The second fails at flat or
  degenerate crossings, and the current-density Lerp/Min operators make a global bound
  unavailable. Perlin derivative bounds alone do not fix it.
  ⇒ Usable only inside separately proven primitive regions; elsewhere the validator and
  backstop must stay. The owner and I both assumed interval propagation already gave
  us this. It does not, and now we know why rather than just that.

ITEM ZERO -- the suspected in-game regression: NARROWED, NOT CONFIRMED
  Queue wait is real, but meshed latency is primarily GENERATION time, not queue. One
  EMPTY LOD0 generation took 551.7 ms, so it is not pure queue saturation either.
  RoomGraphSource measures 0.278939 us/sample, which cannot explain a 400-1000 ms mesh.
  Codex declined to call it a confirmed code regression on this data. Still open, and
  it now points at the known-unsolved LOD0 mixed-classifier fallback rather than at the
  floor work.

REFERENCE ARCHETYPE -- costs, and a correction to my arithmetic
  CorridorRadius 12.5 voxels is a 6.25 m DIAMETER (I said 3.1 m), and validation caps
  it at 16 voxels. The 15-20 m reference hall needs a separate gallery archetype or a
  larger cell-size budget; raising the value violates current lattice/wall assumptions.
  FCaveTerraceMod is a true no-op disabled (0.000531 us/sample); enabled it adds two
  cached SDF probes, estimated lower bound ~0.56 us for a qualifying sample -- estimated,
  not measured. Columns belong to slab generation; TunnelNetwork needs a baked column
  op. Water is presentation, not rock density, and null-RHI cannot price it.

Build 0 errors. No RMC files, links, historical checkouts or logs touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 05:05:47 +02:00
Fr0zkaandClaude Opus 5 8e42d6d807 feat(tunnels): floor authored at BUILD time, room clips tunnel, climb tier added
The round the owner asked for, with three corrections from Codex that are worth more
than the features.

WHAT LANDED
- The complete tunnel floor profile is authored into FCachedTunnel during
  BuildChunkCache, not derived per sample. This was his point: "it should be done at
  creation time, since it knows its destination."
- Room floor ownership blends/clips the tunnel floor at mouths -- his A > B boolean.
- The traversal validator gained a climb tier, structured exactly as agreed:
    bWalkOnlyCanReachDeparture / bClimbCanReachDeparture kept SEPARATE, with
    WalkOnly / Climb / MineRequired cell counts reported alongside.
  The comment in the header states the contract: "The primordial capability contract
  is walk-only... climb is reported as a separate tier and never makes the hard
  connectivity verdict pass." That is the distinction we wanted: reporting tiers, not
  permission.
- Floor controls exposed under Cave Morphology|Tunnels|Floor.

⚠️ CORRECTION 1 -- LARGE LEDGES ARE NOT CAPABILITY-SAFE YET, so they stay opt-in.
    default (preference 0)   fit 20,804   walk reach 10,884   0 ledges
    preference 2             fit 20,765   walk reach 10,712   360 ledges
  Ledges cost 172 walk-reachable cells. Codex kept preference 0 (the old staircase) as
  the default rather than shipping the owner's preferred look at the price of walkable
  space, and led its report with that. Correct call.

⚠️ CORRECTION 2 -- THE BACKSTOP STILL CANNOT GO. Support/room fallback counters were
  zero, but the structural protection clamp fired on 31,017 of 31,017 authored floor
  samples -- 100%. So the shape is still not correct by construction; the clamp is
  doing the work. Instrumenting it was the right move: we now have a number instead of
  an argument.

⚠️ CORRECTION 3 -- THE "X + 2A" CLEARANCE BUDGET IS NOT IMPLEMENTABLE AS I SPECIFIED.
  Interval propagation bounds FIELD VALUES, not SPATIAL DISPLACEMENT. Deriving A needs
  a proven Lipschitz/influence bound for every post-operation, which we do not have.
  Both the owner and I assumed the interval work already gave us this; it does not.
  The idea is still right, the prerequisite is a displacement bound.

MEASURED BY ME: build 0 errors; export 1.365 / 1.357 / 1.420 s (checkpoint was
1.321 / 1.357), flat within noise; 153,346 triangles.

ALSO MEASURED THIS ROUND
- Edit cost: 8 affected chunks, 195.5 ms total -- generation 195.471 ms, apply 0.059,
  diff 0.020, queue 0.001. It is full level-0 regeneration; a dirty-mesh path needs
  partial section support that is unavailable without modifying RMC.
- Streaming caps: effective generation cap 14, pending max 14, CAP HIT. Mesh applies
  per frame 4 (not hit), 2 ms apply budget (not hit), unloads 6 (not hit).
  ⇒ The generation core cap is what binds during startup fill.
- Peak export RSS 1.70 GB (was 19.5 GB before the memory work).
- LOD4 empty outliers gone. LOD0 not solved: 4,600 empty tiles at 3 us median, but 11
  mixed-classifier fallbacks over 100 ms, max 554 ms -- verdict=Mixed, then the full
  mesher, producing no triangles.

⚠️ UNVERIFIED AND CONCERNING: the reported in-game meshed times are far worse than
  412a7ad's -- p50 498.8 ms vs 115 ms, p95 1.227 s vs 410 ms, max 1.974 s vs 844 ms.
  The samples differ (9,336 ready events vs 841 requests, different traversal, possibly
  queue wait included), and the export path is flat, so I do not know whether this is a
  real regression or a different measurement. I have NOT reproduced it myself. It is
  the first thing the next round must settle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 04:25:45 +02:00
Fr0zkaandClaude Opus 5 e91f1c44bb wip(tunnels): checkpoint -- floor parameters exposed, round cut short by Codex credit limit
⚠️ THIS IS A CHECKPOINT, NOT A FINISHED CHANGE. The agent hit its usage limit
mid-round (locked out until 2026-09-15) after compacting its context, and never
filed a report or capability numbers. Committing so 1,344 lines are not left
uncommitted for five days. Verified by me only to the extent stated below.

WHAT LANDED
The tunnel floor controls are now real strate parameters under
"Cave Morphology|Tunnels|Floor", which is what the owner asked for -- hand-made
assets can tune them and the composer can roll them later:

  bTunnelFloorEnabled                true
  bTunnelFloorTerracingEnabled       true
  TunnelFloorTerraceStepHeight       1.71 voxels   (the existing 42.75 cm riser)
  TunnelFloorMaxLedgeHeight         12.0  voxels   (3 m -- climbable, not walkable)
  TunnelFloorGentleSlopeThreshold    tan(44 deg)
  TunnelFloorLedgeCountPreference    0             (0 = old per-segment staircase;
                                                    >0 = whole-chain ledge policy)
  TunnelFloorMaxLedges               4096

⭐ So the staircase-vs-ledges question the owner raised is now answerable IN THE
EDITOR without a rebuild: set LedgeCountPreference to 2-3 and the whole-chain
policy takes over from the per-segment one.

VERIFIED BY ME
  build            0 errors
  export, 2 runs   1.321 / 1.357 s   (ce53552 was 1.384 / 1.405 / 1.415)  ~-4%
  triangles        153,382           (was 153,338, +44)
  geometry hash    9815c9a2..ebeb9   -- CHANGED, so behaviour is live beyond the
                                       stated compatibility defaults

⚠️ CAPABILITY DIPPED SLIGHTLY, and I am not calling this an improvement:
  player-fit cells   20,835 -> 20,800   (-35,  -0.17%)
  reachable cells    10,901 -> 10,886   (-15,  -0.14%)
  largest component  10,886 = reachable, still one connected mass

Small enough to be a consequence of the 44-triangle geometry shift rather than a
real regression, but it is a drop, and my own rule for this work is that fit and
reachable must not fall. Treat it as unresolved until the round can be finished.

NOT DONE (the substance of the brief):
- terracing objective still minimises riser height, not step count
- the room does not yet clip the tunnel floor where they overlap (A > B)
- the traversal validator still cannot climb, so it cannot score real ledges
- the floor profile is still derived per sample, not authored at build time
- the support-column backstop is still present, i.e. the shape is still not
  correct by construction
- dig cost and the streaming-cap audit untouched

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-10 22:01:32 +02:00
Fr0zkaandClaude Opus 5 ce53552543 perf(relief): column cache for floor noise -- 8.5%, and my "it's 2D" premise was wrong
I predicted this would recover the D-section's 30% outright, on the grounds that the
floor relief is FMath::PerlinNoise2D and therefore a function of (X,Y) only, so every
voxel in a column recomputes the same pair of octaves. Codex checked the premise and
it does not hold:

  production applies a 3D CAVE WARP before the floor is evaluated, so the warped X/Y
  fed to the noise change with Z. A world-XY cache would have changed the field.

That is the third arithmetic prediction of mine that did not survive contact with the
code (after the double->float round-trips, and "the candidate scan is the last big
win"). Recording it because the pattern is consistent: I keep reasoning about cost
from a call site instead of from the data flow reaching it.

WHAT WORKS INSTEAD
Cache keyed on actual SDF-domain integer columns plus feature, owner, seed and
frequency -- correct in the warped domain, where world-XY is not. Fixed size, 32,768
entries, ~1.25 MiB per worker, no per-tile or per-region retention.

  28,154,092 column lookups
  25,842,374 hits / 2,311,718 misses      (91.8% hit rate)
  71,133,837 conservative relief skips    (amplitude bound, only where SmoothMax is
                                           provably the identity)
  77,109,353 actual two-octave evaluations

  my build, 3 runs:  1.384 / 1.405 / 1.415 s   (ff8bd7a was 1.509 / 1.542 / 1.543)
                     mean 1.401 vs 1.531  =  -8.5%
  geometry SHA-256 bc0dc059..a8ae, IDENTICAL to ff8bd7a's output

Capability unchanged exactly: player-fit 20,835, reachable 10,901, connected,
40,429 verts / 75,416 tris, dihedral p50/p90 2.5529/33.7191, hash 4520DC6F,
determinism repeat passed.

Codex also declined a sign-only shortcut for the main SDF, correctly: marching cubes
interpolates on the field MAGNITUDE, not just its sign, so a sign-only answer would
move vertices. It kept it for the landing helper where only the sign is consumed.

⚠️ NOTE ON MY OWN "17% NOISE FLOOR": that figure came from a profiled-vs-unprofiled
comparison, not from run-to-run variance. Within one binary the spread here is ~2%
(1.384-1.415). An 8.5% shift that is consistent across every run in both directions
is real. I have been using the wrong number as a threshold and should stop.

Net position: the D-section still costs ~17% against pre-swept-floor (1.401 vs 1.15),
in exchange for +682 player-fit and +418 reachable cells, floors that undulate instead
of presenting a slab, and the retirement of the 1.5M-call validator and two dead ops.

Build 47 actions, 0 errors. Only morphology source changed; zero reparse points;
RealtimeMeshComponent untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-10 18:55:06 +02:00
Fr0zkaandClaude Opus 5 ff8bd7a8bc feat(tunnels): tunnels get the floor rooms always had -- capability up, 30% slower (fix known)
The owner asked the question that unlocked this: "isn't the floor cut a modification
made to rooms afterward, instead of having them be naturally like so?"

Reading the code answered it, and the answer was that rooms and tunnels were built on
OPPOSITE principles:
  room   = SmoothMax(sphereSDF, floorHalfSpace), floor part of the SHAPE, evaluated
           in one pass, with two octaves of Perlin relief already on the floor height
           (FloorReliefStrength / RoomFloorCutMin=0.70, hash-rolled per room)
  tunnel = a bare capsule with NO floor, then CaveTunnelFloorOp + support columns +
           IsTunnelSupportFloorWorldPoint asserting a slab in afterwards

That is why tunnels read as "forced flat" and rooms never did, and why the previous
conditional-floor round could only find 1,539 skips in 1.5M tests: it was optimising
a correction instead of removing the need for one.

WHAT LANDED
Corridors now use a swept SmoothMax(capsule, floor): endpoint floor heights derived
from the rooms they join, deterministically interpolated along the control chain, the
existing two-octave relief reused (not reimplemented) and faded at mouths, and
TERRACES where a room-to-room transition is too steep -- risers capped at 1.71 voxels
(42.75 cm), with relief disabled on risers so the step bound holds.

  player-fit cells   20,153 -> 20,835   (+682)
  reachable cells    10,483 -> 10,901   (+418)
  connectivity       connected, both;  mouth gap 0
  vertices/tris      40,623/75,776 -> 40,429/75,416
  dihedral p50/p90   2.5429/33.8499 -> 2.5529/33.7191 deg

Capability went UP, which is the point: the shape now provides what the correction
was faking, and it reaches places the slab did not.

RETIRED: the 1.5M-call natural-floor validator and its counters, plus the now-dead
CaveTunnelFloorOp / CaveTunnelAirOp implementations and profiler labels.

NOT retired: the finite support-column backstop. Removing it failed hard --
19,097 fit, 396 reachable, DISCONNECTED. It stays, and Codex was right not to force it.

⚠️ THE HONEST COST: 30% SLOWER
  my own build, 3 runs:  1.509 / 1.542 / 1.543 s   (was 1.118 / 1.131 / 1.197 s)
Well above the 17% noise floor. The relief evaluation is what costs.

⭐ And the fix is already identified: the relief is `FMath::PerlinNoise2D` -- a
function of (X,Y) ONLY -- yet it is evaluated per 3D sample. That is exactly the bug
pattern the support floor had, where making the decision per-column took it from
2.29 to 0.138 us/sample (16x). Caching relief per XY column should recover most or
all of the 30%. Next round.

Renders confirm the floors now undulate gently instead of presenting a flat plane;
the global relief metric is wall-dominated and inconclusive, and Codex correctly
declined to claim the visual complaint solved on that basis.

Determinism holds: both final runs hash 4520DC6F with canonical repeat equality.
Analytically the authored floor profile is bounded by tan(44 deg) including the
Perlin derivative and mouth envelope; the full post-modifier SDF still is not
provable, so the player-fit audit stays.

Build 0 errors. Nothing outside Plugins/VoxelForge; RealtimeMeshComponent untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-10 18:00:11 +02:00
Fr0zkaandClaude Opus 5 383ec86c37 perf(apply): stutter is NOT the game thread -- and the flat floor cannot be relaxed
Two things the owner reported from playing 412a7ad. One is answered, one is a null
result that Codex flagged itself rather than dressing up.

THE FLATTENED TUNNEL FLOORS -- capability-safe, and worth almost nothing
He has now flagged these twice: every tunnel carries a smooth flat strip running in
and out through otherwise craggy rock. The idea was to make the floor CONDITIONAL --
assert it only where the natural rock fails walkability, leave the rock alone
elsewhere. Cheaper and better looking, in theory.

The floor is now skipped only when the exact no-floor field passes the same
player-fit validator at foot offsets 0, -1, +1. Unknown or failed keeps the slab.

  natural-floor skips accepted   1,539 of 1,529,241 tests   = 0.10%
  player-fit cells        20,153 -> 20,153
  reachable cells         10,483 -> 10,483
  arrival -> departure    connected, both
  dihedral p50/p90        2.5429 / 33.8499 deg, IDENTICAL
  mesh time (their batch) 1.757135 -> 1.757140 s

Capability preserved exactly. Visual complaint NOT solved, and no speed gained.

⭐ The useful part is what it PROVED by trying to go further. Every broader policy
lost walkable space:
    analytic-only        fit -217, reachable -195
    tightened analytic   fit  -76, reachable  -76
    exact +/-4 foot      fit  -16, reachable  -16
⇒ The floor cannot be relaxed without changing the walkability contract or adding a
route-aware global proof. That is a real constraint, now measured rather than assumed.

Codex's own words, and it was right to lead with them: "the conservative floor change
is capability-safe, but it does not visibly solve the flattened-strip complaint."

THE STUTTER -- measured, and it is not where we thought
Game-thread apply, meshed tiles, under -nullrhi:

  total apply            p50 0.107 ms   p95 0.153 ms   max 0.273 ms
    stream upload/RMC    p50 0.040      p95 0.088      max 0.124
    component creation   p50 0.019      p95 0.021      max 0.035
    registration         p50 0.010      p95 0.011      max 0.020
    collision config     p50 0.005      p95 0.007      max 0.019

A quarter of a millisecond at worst. The game-thread drain is NOT the hitch.
⇒ Remaining visible stutter is most likely render-thread/RHI work or asynchronous
collision cooking behind RealtimeMeshComponent, which is off-limits for edits.

Added anyway as a cheap safety valve: MaxMeshApplyMilliseconds, default 2 ms of drain
budget per frame. Over 729 apply frames: drain p50/p95/max 0.111/0.182/0.313 ms,
budget hits 0, at most 2 visible applies in a frame. It costs nothing today and caps
a future burst.

Request-to-ready, now timed after game-thread application:
  all     p50 57.1 ms   p95 432.9 ms   max 919.4 ms   -> 2.60 m at 6 m/s
  meshed  p50 134.1 ms  p95 457.4 ms   max 919.4 ms

⚠️ My own verification: 1.197 / 1.131 / 1.118 s vs 1.088-1.102 s on 412a7ad. Mean
+4.7% -- below the 17% noise floor, but slower in all three runs, and the validator
runs 1.5M times to accept 1,539 skips. Geometry moved by 4 triangles (153,740 ->
153,744), which is allowed. If the next round cannot make that validator earn its
place, it should come out.

Build 0 errors. Peak RSS 1.67 GB export / 1.90 GB game. Nothing outside
Plugins/VoxelForge; RealtimeMeshComponent untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-10 14:46:29 +02:00
Fr0zkaandClaude Opus 5 412a7ad219 perf(game): the path he actually walks -- meshed p95 4.91s -> 0.41s, RSS 19.5GB -> 1.7GB
The owner: "I want real performance gain for the ingame path... not in deletion, but
in optimization and tricks." Three rounds of ours had improved the commandlet export
he never runs. This one is the streaming path.

MEMORY -- it was retention, not the new lattice
19.5 GB was worker FChunkSDFCache arrays keeping capacity when keys were replaced
(946 MB across 15 workers in diagnostics), shared graph caches with no byte or
lifetime budget, and batch commandlets holding transient UObject worlds until exit.
BlockSkip is bounded to 8^3 bytes per tile; interval scratch is worker-local and
capped. Fixes: release storage on key replacement, LRU with a 256 MiB resident
budget, limit in-flight graph builds, drop oversized entries, CollectGarbage between
batch cases.

  peak RSS   19.5 GB  ->  1.643 GB (their runs) / 1.71 GB (mine, 3x128^3)   -91%
  a 3-case stress batch showed no accumulation

WHY SOME EMPTY TILES COST 5.6s AND OTHERS 3us -- a 1.9 MILLION x spread
Fast ones returned straight from the layout/edge proof (verdict AllAir/AllSolid,
refine=0, mesh=0). Slow ones returned Mixed, so the caller ran the FULL mesher grid
and only discovered emptiness by brute force -- logs show empty=1 after ~5 s of mesh
work. The interval machinery now reaches a verdict before a mesh job is enqueued.

IN GAME, 841 requests at streaming radius:

                     count     p50        p95        max
  empty                394   6.087 ms   12.113 ms   25.049 ms
  meshed               447 114.748 ms  410.314 ms  844.043 ms
  LOD0 empty           221   7.126 ms   11.901 ms   25.049 ms
  LOD0 meshed          122 355.794 ms  600.945 ms  844.043 ms

  against d38d4ca:  meshed p50/p95/max 0.6085 / 4.9075 / 5.0199 s
                    LOD4 empty max 5.618 s
  => meshed p50 5.3x, p95 12.0x, and the multi-second empty outliers are gone.

REQUEST-TO-READY -- the number that answers "I can outrun the generation and fall"
  all tiles     p50  57.576 ms   p95 429.052 ms   max 905.633 ms
  empty         p50  30.943 ms   p95  98.302 ms   max 305.505 ms
  meshed        p50 128.521 ms   p95 462.235 ms   max 905.633 ms

At the harness walking speed of 6 m/s: p95 = 2.574 m, worst case = 5.434 m. A walking
player now outruns tile readiness by a couple of metres, not by a strate.

CAPABILITY PRESERVED, which is the point
The owner asked that speed never come from quietly simplifying the world. It did not:
geometry SHA-256 98eaad61..d3c4f is IDENTICAL to d38d4ca, 153,740 triangles, 82,516
vertices, across three runs. No resolution, sample-count, octave or seed reduction.
Unknown intervals still cost the skip. Prove and skip, never simplify and hope.

Export path, my own build, 3 runs: 1.088 / 1.102 / 1.100 s (was 1.171-1.258 s). ~9%,
below the 17% noise floor, so not claimed as a speed win -- the export was not the
target.

Honest remainder: LOD0 empties are ~7 ms p50 with a 25 ms tail, not the 2-5 ms of a
pure layout proof. Those are bounded, sound interval refinements that never invoke
meshing; going lower needs stronger proof bounds, not more skipping.

Verified by me: build 0 errors, binary timestamp checked against sources, peak RSS
measured, hashes compared. Nothing outside Plugins/VoxelForge; RMC untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-10 11:05:09 +02:00
Fr0zkaandClaude Opus 5 d38d4cabef perf(gen): interval propagation -- blocks can finally be PROVEN empty. 1.55s -> 1.21s
Task 4 ran 11 hours and was killed by system memory pressure before it could file a
report. The work survived, builds clean (0 errors), and is verified faster with
byte-identical output. This commit is my verification, not its summary -- it never
got to write one.

THE STRUCTURAL FIX
0e9df9e established why no block could ever be skipped: OriginSpineOp,
PassageCarveOp and the active seals Lerp/Min/Max against the CURRENT density, so
with an unknown incoming density their supremum is genuinely unbounded. Not a
missing bound -- the operator semantics.

The answer is to propagate a density INTERVAL through the stack, and that is what
landed: FVoxelBoxSdfInterval + PropagateSdfOverBox on the operator interface, an
interval fold through every converter and modifier, interval arithmetic down into
the noise primitives themselves (gradient corners, warp, worm frequencies), and a
BlockSkip lattice the mesher consults per 8^3 block.

Its own guiding comment, which is the right default:
  "no finite interval is a proof. Unknown costs the skip and protects the geometry."

  128^3 TunnelNetwork seed 1337 slot 1, freshly built binary, 3 runs each:
    before  1.515 / 1.554 / 1.569 s
    after   1.213 / 1.258 / 1.171 s     -22.5%, 1.28x -- clears the 17% floor

  Geometry SHA-256 98eaad61..d3c4f IDENTICAL to 0e9df9e. 153,740 triangles either
  way. It skips only provably-empty work; nothing about the world changed.

It also fixed one cause of the 5.6 s empty tiles found in 0e9df9e: an LOD4 tile
legitimately spans 19^3 = 6,859 chunk keys and was rebuilding per-chunk state
thousands of times within a single tile. A shared cache removed that failure mode;
remaining slow empties were LOD0 and are a separate fast-classifier bail, still open.

Partially landed, unfinished when killed: RequestStartCycles on FChunkResult, to
report request-to-ready latency rather than worker generation time. Harmless as is.

⚠️ WHAT KILLED IT, AND IT IS OURS
A single UnrealEditor-Cmd commandlet grew to 19.5 GB, leaving 1.3 GB free on a
32 GB machine, and the OS killed the agent. Freeing it restored 21.6 GB. The owner
said twice that his machine should not be under memory pressure and he was right
both times: first the 411 MiB worker caches, now a 19.5 GB commandlet. A generation
run must not need 19.5 GB. Next task.

Cumulative on this region: 342.2 -> 16.2 -> 2.62 -> 1.49 -> 1.21 s  (~283x).
3,374 insertions / 342 deletions across 12 files. Build 0 errors. Nothing outside
Plugins/VoxelForge; RealtimeMeshComponent untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-09 21:11:13 +02:00
Fr0zkaandClaude Opus 5 0e9df9e5a7 perf(gen): the game path is NOTHING like the export path -- and empty tiles cost 5.6s
Overnight task 3. The headline is not a speedup: it is that we have been measuring
the wrong thing all along.

⭐ IN-GAME MEASUREMENT, FINALLY (headless -game -nullrhi, TunnelNetwork PIE map)

  46,078 profiled tile calls
  46,025 EMPTY  (99.885%)
      53 actually meshed
  meshed    p50 0.6085 s   p95 4.9075 s   max 5.0199 s   mean 1.3515 s
  LOD1-3 empty medians ~2-3 us
  LOD4 empty OUTLIERS up to 5.618 s

The pawn fell through successive strata during the run, reproducing the owner's
"I can outrun the generation and fall" exactly.

Two facts to sit with:
1. **Almost nothing is meshed. 99.9% of tile requests produce nothing** -- and some
   of those empty tiles take FIVE AND A HALF SECONDS. A tile that generates no
   geometry costing seconds is the bug, and it is classification cost, not meshing.
2. The export path we have optimised for three rounds does 64 tiles in ~1.5 s
   total. The game does 46,078 calls with multi-second tile latency. **They share
   the density function and nothing else about their cost profile.**

⇒ Every optimisation so far was aimed at the path the owner does not play.

WHY NO BLOCK CAN BE SKIPPED -- answered, and it is structural
Per-op bound-vs-brute-force table produced (op_bounds.json). Most ops bound fine
once given proven suprema, and those were tightened:
  FBM envelope 1.5, cellular 6.0, SdfConvertOp exact 2*|BaseDensity| = 16,
  Lerp radius envelope max(Min,Max), plus state-aware room/column/floor-bias/worm/
  terrace/cliff/arch/dome/pinch/roughness bounds. Structural ops now return exact
  zero outside their geometric bands.

But OriginSpineOp, PassageCarveOp and the active seals **cannot** take a finite
generic density bound: they Lerp/Min/Max against the CURRENT density, so with
arbitrary incoming density their supremum is mathematically unbounded. This is not
a missing EffectOverBox, it is the operator semantics.
⇒ Hierarchical evaluation requires propagating a density INTERVAL through the
stack, or changing those operators. That is the real prerequisite, now named.

Early-out with tightened bounds still skips 0 of 4,096 blocks and costs 96.6%
(3.305 -> 6.497 s). Left opt-in and DISABLED. ClassifyTile's TerrainZ/overhang
guard untouched.

SPATIAL INDEX REMOVED (ee89c4d's, which never earned its keep)
  wall clock   3.1407 s on -> 3.1894 s off   = 1.5%, below the 17% floor
  cache bytes  557,626,564 -> 246,049,920    = -297 MiB
  candidates   19.5M -> 81.6M scanned (4.17x), evaluated +25.6%
Memory the owner can feel beats counters he cannot. Per-primitive broadphase stays.

VERIFIED END-TO-END on a correctly built binary: 1.515 / 1.554 / 1.569 s, geometry
hash 98eaad61..d3c4f unchanged from 2f33870. Task 3 is memory, not speed.

⚠️ PROCESS, second time: the agent's reported DLL timestamp was from a staging copy
it built under the plugin's own Saved/, not Plugins/VoxelForge/Binaries. My first
three runs measured the 05:58 binary again. Verify the binary you are about to
measure, not the one someone says they built.

Determinism: all export hashes ED0F1F31 within the run; classifier vs generator
verdicts zero mismatches. Nothing outside Plugins/VoxelForge; RMC untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-09 09:00:41 +02:00
Fr0zkaandClaude Opus 5 2f33870a84 perf(mesher): adjacent tiles were re-evaluating 18% of the field -- 2.69s to 1.49s
Overnight task 2. Every claim here is end-to-end wall clock on a real 128^3 export,
not a profiled bucket, because the last two rounds shipped large bucket wins worth
0% in reality. Measurement floor is ~17% run-to-run; only Part 2 cleared it.

PART 2 -- KEPT. Density-grid reuse.
The mesher samples an expanded grid (-1..Cells+1), so adjacent tiles re-evaluated
their shared boundary planes. Measured: 2,744,000 requested samples contained
495,909 duplicates -- 18.07% of the field generated twice. A run-scoped read-only
lattice (FVoxelSharedDensityGrid) owns each world sample once and hands it to both
adjacent tile grids. Duplicates now 0.

  128^3 TunnelNetwork seed 1337 slot 1, freshly built binary:
    before  2.603 / 2.693 / 2.697 s
    after   1.488 / 1.499 / 1.486 s      -44.6%, 1.80x

  Geometry SHA-256 98eaad61..d3c4f IDENTICAL to the pre-change binary's output.
  153,740 triangles either way. Pure win, no field change.

The lattice is deliberately not a UObject and not a cache with eviction: ownership,
lifetime and iteration order stay explicit. A malformed or incomplete lattice is
ignored and falls back to the canonical generator path rather than altering geometry.

PART 1 -- REVERTED. Block-level empty-space early-out.
**0 of 4,096 blocks were safely classifiable**, and the attempt made it 85% slower
(3.632 -> 6.735 s). No unsafe bound was retained.
⇒ This is a finding, not just a failure: our conservative bounds are too loose to
prove ANY 8^3 block uniform. Hierarchical evaluation of any kind is blocked until
we know which operator has effectively unbounded reach. Next task.

PART 3 -- REVERTED. Task granularity.
15 workers, 64 tile jobs, mean job 0.594 s, launch fringe ~14.1 us. MinBatchSize=4
was 1.09% SLOWER end-to-end despite a better bucket time -- exactly the trap this
task's rule exists to catch. Default stays 1. ParallelForWithExistingTaskContext
not adopted.

PART 4 -- NO CHANGE. Renderer audited over 3 runs (8.736 s total, 1.483 s raster).
Mesh and BVH are already reused across viewpoints; no per-pixel heap allocation, no
redundant remeshing. Nothing gross to fix; it benefits from the above automatically.

STILL OPEN: the spatial index from ee89c4d has never earned its keep in wall clock
and accounts for ~214 MB of measured allocations. Codex proposes an on/off ablation
then removal if confirmed. Not done unilaterally.

⚠️ PROCESS NOTE: the agent reported "build passed" but the runtime DLL was still
from 03:07 while sources were edited at 05:22/05:42. My first three verification
runs measured the OLD binary and showed no change. Always check binary timestamps
against source timestamps before believing a measurement.

Cumulative on this region: 342.2 s -> 16.2 s -> 2.62 s -> 1.49 s (230x).
Determinism: identical hashes across all repeat runs. Build clean. Nothing outside
Plugins/VoxelForge touched; RealtimeMeshComponent untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-09 05:59:56 +02:00
Fr0zkaandClaude Opus 5 e63f016835 tools(profile): the instrument lied by 11.5x -- now it checks itself, and batching kills startup cost
Overnight task 1 of the owner's mandate: "better diagnostic, profiler and tools,
so we can batch test way more quicker."

WHY THIS CAME BEFORE ANY MORE OPTIMISATION
ee89c4d delivered a 5x candidate reduction and a 16x cheaper support floor for
0% end-to-end gain, because the profiler inflated by ~11.5x (14.74 us/sample
profiled vs 1.28 us/voxel real). Two rounds of bucket rankings were artefacts of
FPlatformTime::Cycles64() wrapped around ~100 ns operations -- the timer pair
calibrates at ~0.1 us, so it was measuring itself.

PROFILER
Deterministic count sampling plus amortised block timing every 64 density calls.
Coarse counters (calls, candidates) stay always-on -- they were never the problem.
Timing-only scopes are opt-in behind -profiledensityfull. UE CPU trace scopes added.

  before   profiled 14.74 us/sample  vs  1.28 us/voxel real   ~11.5x
  after    profiled 0.463050 s       vs  0.558470 s unprofiled   -17.09%
           geometry 235AC2DC in both

⭐ Every profiled report now prints profiled total, unprofiled total, discrepancy,
ratio and geometry equality, and EXITS CODE 2 if the comparison fails. An
intermediate run hit -22.6% and failed loudly, which is the point: this class of
bug must be impossible to miss rather than something noticed three rounds later.

⚠️ The -17% residual is run-to-run noise, so ~17% is our measurement floor.
Improvements smaller than that cannot be claimed from a single pair of runs.

BATCH EXPLORER
One process, many cases, one JSON. Startup was ~25-30 s of engine boot for ~3 s of
work, which is what made every iteration expensive.

  3 cases in 6.59 s of case time
  standalone vs batched, same binary: hash 235AC2DC, 2,461 tris, 343,000 tunnel
  lookups, 554,381 passage evaluations -- identical on both

-out= is enforced absolute under the plugin's own Saved/.

CACHE BREAKDOWN (the 272 KB/entry question)
The slot table is small. The weight is spatial-index bins and candidate arrays,
room arrays, support-column arrays and tunnel-world indices -- i.e. much of the
411 MiB is the spatial index ee89c4d added, which also bought 0% end-to-end.
Worth revisiting on those terms.

RESEARCH (recorded, not acted on) -- CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md
Ranked follow-ups: deterministic empty-space early-out, density-grid reuse, task
granularity (ParallelForWithExistingTaskContext + MinBatchSize), SIMD MC (~2x over
optimised scalar, but cross-machine determinism is the risk), sparse hierarchy,
RMC partial updates, Transvoxel LOD seams. Sources cited in the doc.
⚠️ Upstream RMC is 5.3.2; the owner's copy identifies as 5.4 with custom
modifications. No upgrade is safe. Left untouched.

PUSHBACK, accepted: "buckets must sum to their parent" needs a parallel-time
definition -- summing worker CPU is not wall time. Reports now separate worker CPU
sum from wall-clock estimate.

End-to-end unchanged, as expected for a tooling change: 2.746 / 2.691 s on the
128^3 seed 1337 region (was 2.678-2.705). Build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-09 05:02:21 +02:00
Fr0zkaandClaude Opus 5 ee89c4d8c2 perf(gen): 5x fewer candidates, 16x cheaper floor -- and 0% end-to-end. Read the last section.
Owner, after playing 0576e80: "slightly faster, still not on par... I can outrun the
generation and fall." Also, unprompted: "the tunnels seem forced, with a forced
flat -- it technically looks good, but could be a reason." He was right that it
was a reason; it was not THE reason.

WHAT LANDED, all real and all measured

Conservative spatial index (immutable per-chunk uniform bins, baked conservative
bounds, squared rejects, deterministic ordering):

  candidates scanned/sample   25.140 -> 5.192
  candidates evaluated/sample 13.879 -> 2.992
    cave tunnels  11.228/6.069 -> 1.525/0.723
    tunnel core   11.228/5.831 -> 1.216/0.567

The earlier flat AABB attempt failed because loose bounds culled almost nothing
while adding a pass. Technique from GPU Gems 3 ch.32 broad-phase.

The forced-flat floor, which the owner spotted by eye: a floor decision is
inherently per-COLUMN and was being recomputed for every Z sample in that column.
Now resolved once per integer XY column and reused.

  TunnelCorePosts   2.29 us/sample -> 0.138 us/sample   (16x)

The floor is unchanged in behaviour: still applied only after a tunnel passes its
world bounds and walkable projection, still walkable, look untouched.

FIRST MEASUREMENT OF OUR MEMORY FOOTPRINT
The owner: "it shouldn't be pressured on such system, even less when not running
anything." He is right, and it is ours:

  total resident worker cache   431,162,116 B  =  411 MiB
  15 workers                    27.41 MiB each
  average valid entry           278,704 B
  largest entry                 541,344 B
  static slot storage             2.4 MiB   (dynamic: 428.6 MB)

Cutting 4096 -> 128 slots only saved static storage. The weight is 272 KB PER
CACHED CHUNK STATE, ~102 live per worker. Nobody has yet asked why one cached
chunk state needs 272 KB. That is the open question, not the slot count.

⚠️ AND THE PART THAT MATTERS MOST: THIS BOUGHT NOTHING END-TO-END

  128^3 TunnelNetwork, seed 1337 slot 1, same region, unprofiled:
    before this commit   2.623 s
    after, 3 runs        2.705 / 2.678 / 2.682 s

Flat. A 5x candidate reduction and a 16x cheaper floor moved real generation time
by zero.

The reason is visible in the numbers and I should have caught it earlier: the
PROFILED path reports 14.74 us/sample while the real unprofiled cost is
1.28 us/voxel. **The instrument inflates by ~11x.** Every bucket we have been
ranking and optimising for two rounds was measured under that inflation, so the
ranking does not reflect where real time goes. We optimised what the profiler
exaggerated.

⇒ Before any further optimisation: make the profiler cheap enough that profiled
and unprofiled totals agree within a small factor, or switch to sampling. An
alarming number is the instrument until proven otherwise -- three for three in
this project, and now four.

Still true: ~21x slower than the pre-op-stack build, and generation still loses
to a walking player. Geometry changed by 40 triangles (153,692 -> 153,732), which
is allowed; two full exports hash identically (37E00BEE..0A7DF). Build 42/42.
No CPU-feature dispatch. Nothing outside Plugins/VoxelForge was touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-09 03:05:40 +02:00
Fr0zkaandClaude Opus 5 0576e800c4 perf(gen): the landing work was per-voxel -- 342s to 2.6s, same world
The owner played 64f5d61 and reported the truth: "generation still takes ages...
TunnelNetwork doesn't even finish generating, as big as the surface world" while
SurfaceWorld completed in under 10s, and stopping PIE hung for 30+ seconds.

e08b49a's 21x was real and irrelevant. It fixed per-CHUNK state rebuilds, which
dominate the commandlet export path. The game is bound by per-VOXEL evaluation.
Same symptom, different bottleneck, and nobody had measured the second one.

WHY PIE HUNG
bShuttingDown was never checked inside VoxelMarchingCubesMesher.cpp -- zero
occurrences. A worker inside GenerateMesh could not abort, so 64f5d61's correct
unconditional EndPlay wait was waiting on tiles physically unable to stop. The
hang was not a teardown bug; it was evidence that one tile took tens of seconds.
Added shutdown polling per slab/row in the mesher and through GenerateTileResult;
aborted results carry Epoch + bAborted and ProcessQueue drops them. EndPlay still
waits unconditionally -- the 3s mark only logs now.

  PIE stop   3.409s / 44.759s  ->  4 ms teardown, 38 us worker drain

WHERE THE PER-VOXEL COST WAS
The owner's own comparison was the sharpest instrument available. Profiled both:

  GetDensityAt          TunnelNetwork 12.64 us/sample   SurfaceWorld 1.32
  tunnel candidates evaluated      7.088 per sample                 0
  tunnel-core candidates evaluated 7.201 per sample                 0
  room candidates evaluated        1.000 per sample                 0

  largest buckets: RoomGraphSource 2.57, TunnelCorePosts 0.76,
                   PassageCarveOp 0.22, PassageStructuralPosts 0.20

The three regressing commits map cleanly, and one is innocent:
  7a20eee drainage tree  -- cache-build work, NOT per-voxel
  413c15d landings       -- repeated full passage modification per sample
  55bf807 landing rooms  -- room SDF + tunnel/core evaluation per sample

Fixes: FPassageCarveOp carves only, landing/support moved to the structural MC
post; tunnel core support and SDF fused into one pass; room graph caches shared
per worker on complete deterministic keys; tunnel density cache uses open
addressing with 4096 slots. Room cache rebuilds fell to 9 per 42,875 samples, so
what remains is genuine O(local cached room/tunnel count) work.

  TunnelNetwork 128^3, seed 1337 slot 1, same region:
    342.2 s / 163.161 us/vox   ->   2.623 s / 1.251 us/vox     (130x)

  Geometry SHA-256 aaf95559..066b4, IDENTICAL to the pre-optimisation run at the
  same roughness. 153,692 triangles either way. Verified independently of the
  agent's own report.

NOT FIXED, and the first one is the next real target:
- L4 tiles still cost 16.93 s. Hit rate falls 99.9% (L0) -> 23.7% (L4), but
  1024 -> 4096 slots only moved 18.39s -> 16.93s and the 1,927 misses ARE the
  distinct key count. That is low reuse, not eviction. A bigger cache will not
  save it.
- TunnelNetwork is still ~10x SurfaceWorld (0.354s vs 0.028s on a matched 32^3).
- The live PIE tile table is SurfaceWorld: this checkout's PIE map is configured
  that way. TunnelNetwork went through the same mesher and density path via the
  commandlet, but no TunnelNetwork tile has been timed in an actual game session.
  Export numbers are not presented as game numbers.

Still outstanding, untouched here: the inter-strate passage has intersections it
should not have, routes through open passage when it should not, and should
descend from a low point of the current strate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-08 23:47:53 +02:00
Fr0zkaandClaude Opus 5 64f5d61e08 fix(crash): EndPlay tore the world down under live workers -- the cache made it lethal
PIE crashed with EXCEPTION_ACCESS_VIOLATION reading 0xffffffffffffffff:

  FRoomGraphSource::Eval()   VoxelDensityOpStack.cpp:3056  (Manager->GetLayoutVersion())
  UVoxelGenerator::GetDensityAt()  VoxelGenerator.cpp:1103 (ActiveOpStack->EvalMC)
  ...  AVoxelWorld::LoadTile lambda, on a task-graph worker

Two lifetime bugs, combined:

1. EndPlay gave up after a 3-second deadline and destroyed UObjects while workers
   were still inside GenerateTileResult. The crash log records 4, 3 and 7 tasks
   still running during teardown. ARCHITECTURE has always said EndPlay blocks on
   ActiveTaskCount -> 0; it did not.
2. e08b49a's worker-local cache keeps 128 built op stacks alive per thread, and
   those stacks hold a raw UVoxelStrateManager*. Before the cache, a stack was
   rebuilt at every chunk crossing and never outlived its owner for long. After
   it, a freed manager stayed reachable and got dereferenced.

The cache did not create the teardown race. It made an existing one lethal.

Fixes:
- EndPlay now waits unconditionally for ActiveTaskCount == 0. The 3s deadline is
  demoted to a log line: a slow drain is reported, teardown is never torn out
  from under a reader.
- UVoxelStrateManager carries a process-unique CacheLifetimeId, folded into the
  cache key. Deliberately not a UObject pointer: allocator address reuse must not
  let a stale worker-local stack look current across PIE sessions. PassagesVersion
  restarts per layout and cannot make that distinction.
- Cached passage/room manager references are weak pointers, fail-closed.
- Verified FStrateGenerationParams is copied BY VALUE into tunnel operators, and
  stack moves preserve owned operator allocations, so no operator references a
  previous cache entry's parameters.

  TunnelNetwork mesh  342.332 s -> 16.199 s   (21.1x)
  per tile, 64 tiles    5.349 s ->  0.253 s

NOT verified, and it matters here:
- No PIE measurement. The owner restricted this session to analysis and fixes, so
  the numbers above are the commandlet's direct-mesher path, NOT the streaming
  path the game uses. They are not game timings and are not presented as such.
- The 128-slot cache may churn at distance: an L0 tile crosses at most 27 exact
  chunks, but L2/L3/L4 tiles touch 216 / 1,000 / 6,859 chunk keys. Whether that
  helps or hurts in game is unmeasured.
- EndPlay waiting forever converts a crash into a hang if a task never observes
  bShuttingDown. Workers do check it, but this trade is deliberate and worth
  watching.

Named, not changed: the straight "roads" the owner still sees are inter-strate
passages. FStratePassageConfig defaults Connections=1 and Style=Straight
(VoxelStrateTypes.h:1781); GeneratePassages maps Straight -> SlopedTunnel and
builds a two-leg switchback that FPassageCarveOp carves
(VoxelStrateManager.cpp:819). d97373c removed the landing-to-hub radial roads; it
did not touch these. FLatticeCorridorSource also still emits axis-aligned capsule
edges for Maze (VoxelDensityOpStack.cpp:548). Awaiting the owner's call on what
should replace them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-08 21:17:57 +02:00
Fr0zkaandClaude Opus 5 e08b49a183 perf(gen): a thrashing one-slot cache cost 18.7x -- and the world is unchanged
TunnelNetwork density evaluation was 45x more expensive than Maze for identical
work: 2,097,152 voxels, 64 tiles, same mesher, same machine. Profiling killed
every hypothesis I brought to it -- box classification skipping 0/64 tiles was
true but irrelevant (export bypasses classification), near-surface early-outs
fired at 49.7%, the room cache built 340 entries for 2.74M samples, and the
per-room overrides resolved once per sample rather than once per modifier.

The cause was the wrapper, not the stack. A mesher tile samples an EXPANDED grid
(-1..Cells+1), so one worker crosses several exact chunk coordinates while
meshing a single tile. The generator's cache held only the last coordinate, so
every boundary crossing rebuilt the resolved parameters, the whole 22-operator
stack and the tunnel-core cache. On one representative tile: 3,675 refetches,
38.0s. The entire operator stack was 18.5s of a 4,272s total -- 0.43%.

Replaced with a 128-slot direct-mapped thread_local cache keyed on owner, chunk,
layout version, params fingerprint and generator type. Collisions force a safe
rebuild, never a stale read. No shared mutable state, no order-dependent
reduction.

  TunnelNetwork  344.5s / 164.267 us/vox  ->  18.4s / 8.763 us/vox   (18.7x)
  Maze             7.4s /   3.535 us/vox  ->   1.08s / 0.513 us/vox  ( 6.9x)
  same tile        38.0s                  ->   0.51s ; refetches 3675 -> 27

Output is BYTE-IDENTICAL, before vs after and across repeats, both archetypes:
TunnelNetwork 50d6b4d0..f23126, Maze 364f7489..2fbe99. The permission to change
the field was not needed.

Also in this commit, from the two preceding tasks:

- Explorer renders from the MESH instead of raymarching. It meshes the region
  once through the canonical UVoxelMarchingCubesMesher, then CPU-rasterises via
  a BVH. A viewpoint went from ~16 minutes to 0.155s; 8 views now cost 1.25s of
  drawing. No render pixel samples the density field.
- One sampled grid and player-fit mask per run, shared by render/walk/export,
  instead of the same 368s walk computed twice.
- A wall-clock budget (25 min default, hard 30 max) that stops cleanly and
  records truncated/truncated_during/completed_modes rather than overshooting.
- ParallelFor across the measurement sampling; the suite was ~10% of a 16-thread
  machine.
- -surfaceroughness= on the commandlet, and a surface-relief metric in export
  mode (edge lengths, dihedral distribution, face-vs-smooth normal divergence)
  so "does this look like rock" is a number, not a screenshot argument.
- Smooth vertex normals in the rasteriser: FVoxelMeshData::Normals is the
  density-gradient normal the engine shades with, and the raster path was
  recomputing a face normal over it.
- ScaleDiagnosis: baked candidate indices are now derived from the current
  corpus, so a legitimate field change stops breaking the test.

NOT verified, and not claimed:
- TunnelNetworkSpineEquivalence exceeded the 30-minute cap and was stopped.
  Inconclusive, not passing. The identical hashes are strong evidence the
  op-stack/legacy equivalence holds, but the test has not confirmed it.
- CrossPlatformDigest passes but reports 2 samples within 1e-6 of the
  isosurface; FMath::Sin/Cos leaves Windows/Linux equivalence unproven.
  Pre-existing, not caused by this change.
- The cache's memory footprint (128 entries x ~16 workers) is unmeasured.

What the roughness sweep settled, for the record: SurfaceRoughness 0 -> 8 moves
cost by 0.3% (163.2 / 163.2 / 163.7 us), so roughness was never the cost. It
does reach the surface (dihedral p50 1.1 -> 2.2 -> 5.9), which kills my claim
that the density-channel operator contributed nothing. TunnelNetwork's hard
seams are present at roughness 0 (4.4% of edges past 40 deg vs Maze's 1.0%), so
they are structural -- in how primitives are unioned -- not a roughness
deficiency. More roughness makes them worse, not better.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-08 20:31:56 +02:00
Fr0zkaandClaude Opus 5 d97373c5a8 feat(gen): Jahni's playtest corrections -- rooms flattened, roads deleted
From walking a TunnelNetwork preset at (0,0). Four corrections, and two of
them were the same mistake.

2a. The (0,0) landing room is sized to host a FUTURE straight vertical
    shaft from the surface, with flooring around the origin itself. The
    room is the landing for a shaft the player digs later.
2b. The inter-strate descent no longer starts at the spine -- placed by
    deterministic hash within a radius around (0,0). The spine room is for
    the surface shaft; the descent is elsewhere.
2c. ⛔ DELETED the landing->hub connector roads from 413c15d. Jahni:
    "its straight up making other tunnels straightly through everything...
    they have nothing to do here." Zero references remain.
2d. ⭐ FLATTEN THE ROOM JOINS instead. Where two spherical rooms intersect
    and leave a lip above head height, the join is brought down to a
    walkable level.

⭐ 2c AND 2d ARE ONE CORRECTION. I solved "rooms do not connect" by
TUNNELLING BETWEEN THEM when the answer was MAKING THEIR FLOORS MEET. The
81-run sweep had already said so -- zero narrow-gap events, every failure a
vertical severance, renders showing "a sharp diagonal ledge descending into
black open space". A radial corridor to the origin could never have fixed
that; flattening the joins is aimed at exactly it.

2e. Descent tunnel 1.5x wider, per "1.5x bigger wouldn't be too much".
2f. Corridors WANDER. Jahni, from the renders: "its not a maze, its still a
    latice made straight paths." The spanning tree changed WHICH edges
    exist, not what an edge looks like -- every corridor was a straight
    axis-aligned capsule, every junction 90 degrees. Now hash-jittered
    chains, seam-safe like the tree.

RENDERER: 192x128 -> 512x288, and cameras are now seeded from the
player-fit set -- "a density-checked pose from the exact player-fit walk
mask". Previously they sat INSIDE ROCK looking at back-faces, which Jahni
spotted instantly and no metric could. First genuinely legible pictures of
this world.

ParallelFor landed at 6 sites in VoxelStrateMeasure before I stopped the
run; the rest of that work continues next.

⚠️ AND THE RENDERER IS ARCHITECTURALLY WRONG, per Jahni: "why, in engine,
does it take a minute to have clear view of thousands of angles, but in our
renderer it takes ages?" Because the engine MESHES ONCE and rasterises --
~32k density calls per chunk, then every angle is free. I RAYMARCH: tens of
millions of density calls PER IMAGE, nothing reused between frames. We
already have the mesher and an export mode. Next task renders from the
mesh.

Also from him: runs must stay under 30 minutes. This one hit 33 and
produced 2 images.

Build clean. Committed as a checkpoint -- the task was stopped mid-Part-3,
not finished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-08 14:14:29 +02:00
Fr0zkaandClaude Opus 5 55bf807f60 feat(descent): landing ROOMS and a walkable tunnel -- per Jahni's correction
Jahni corrected the design: "the connection between level is a tiny tunnel
connecting both strate, not vertical, it needs to be walkable" and "'the
spine' shouldn't be carved except optionally from the top first strate,
then it's used to have a landing room".

COMPOSER-NOTES §6.8 had said the spine "provides a clean landing space, not
a continuous open shaft" -- and the implementation carved a vertical
cylinder anyway. A vertical cylinder has no walkable floor except at its
bottom, so a carve guaranteeing open AIR guaranteed nothing STANDABLE.
That one omission is behind five of my dead hypotheses this week.

Now:
  origin spine   a finite rounded ROOM per strate, no vertical bore.
                 radius 14 -> 8 m wide, 7.5 m tall
  descent        a walkable tunnel, single-file by design: 3 m mouth,
                 2.5 m waist, gradient capped at a NAMED 15 deg
  exotic styles  VerticalShaft / SpiralDescent / CascadingDrops /
                 CrackCrevice preserved, no longer the default

⚠️ THE GEOMETRY BITES, and it is worth Jahni's eye: at 15 deg a 32 m drop
needs 119.4 m of horizontal run -- about 59.7 m per switchback leg. That is
a long walk between strates. It is the honest consequence of "walkable",
not a tuning artifact, and the alternative is a steeper number.

⭐ AND THE ROOT CAUSE GOT NAMED A THIRD TIME, by Codex mid-run: "the round
tube was hollow air but had no authored support strip, so 'open' still did
not mean 'standable'." Tunnels now carry a floor band, and the root join
terminates in the finite origin room rather than an annular shaft hub.

Measured, commandlet sweep, 8 archetypes x 8 seeds, 64/64 valid:
  origin column has player-fit cells   17/64 -> 63/64
  origin column reachable               0/64 ->  7/64
  arrival landing probes            315/320 fit AND in the arrival component
  tunnel audit    5,916/5,916 floor and capsule samples pass; zero gradient,
                  floor, capsule, geometry or seal failures
  narrow-gap events 0        box verdicts 14 checked, 0 violations

⚠️ arrival->departure is 6/64. The landings and tunnels are now provably
walkable; what remains is the INTRA-STRATE vertical connectivity problem
found in 6b91669 -- floors at different levels inside a strate with nothing
joining them. Separate and larger. Not tuned toward.

⚠️ PROCESS: this took 5.7 hours, of which Codex used 4 SECONDS of CPU. The
rest was polling the 73-minute automation suite at ~10% machine load. The
commandlet exists to avoid exactly that. New rule recorded: iterate with the
commandlet, full suite ONCE at the end.

Editor + Shipping builds clean. Automation 32/32, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-08 02:14:37 +02:00
Fr0zkaandClaude Opus 5 6b916699af test(explore): 81 runs in an afternoon -- every failure is a DROP, not a squeeze
First real use of the commandlet, and it paid for itself immediately: 64
sweep runs + failure renders + origin probes, which at the old 73-minutes-
per-automation-run would have been days.

⭐ FINDING 1: THE WORLD IS FRAGMENTED VERTICALLY, NOT PINCHED.
Across all 64 runs: ZERO narrow-gap events. Every failed run has separate
arrival/departure fit components with a positive gap, 12.75 m to 72.98 m.
So corridors are not too tight anywhere -- the space is severed.

And for the first time in this project we LOOKED at a failure instead of
inferring from aggregates:
  FlatPlain/42   "a broad flat floor sheet with a sharp diagonal ledge
                  descending into black open space" -- a drop, gap 63.9 m
  SurfaceWorld/7 "terrain shelves and a large black void between them.
                  There is no connecting floor" -- gap 51.8 m
  Maze/42        "a massive solid rock wall/overhang fills the separation"
                  -- gap 27.7 m
I checked the images myself. FlatPlain shows two floor LEVELS 27 m apart
in Z with nothing joining them.

⇒ The missing thing is VERTICAL CONNECTION between floor levels. Ramps,
slopes, ledges, stairs. Every archetype builds floors; nothing builds a way
between them.

⭐ FINDING 2: THE ORIGIN HUB IS NOT REACHABLE, which contradicts 413c15d.
  agent's reachable set included the origin column:  0 / 64 runs
  origin column had ANY player-fit cell:            17 / 64 runs
  Maze slot 4, all 8 seeds:                          0 fit cells, unreachable
Even in the 17, the origin cells were not in the arrival component.

The reason is structural and obvious in hindsight: the origin spine is a
VERTICAL CYLINDER. A vertical shaft has no walkable floor except at its
bottom, so a scalar-density carve that guarantees open air guarantees
nothing standable. The landings join a hub you cannot stand in.

That is a finding about the §6.5 hub join as exposed to the player-fit
layer -- not walk tuning. Recorded rather than patched.

Also from Jahni, looking at the renders -- two things numbers missed:
- The render cameras sit INSIDE ROCK, viewing tunnel surfaces from the
  solid side. Cameras must be seeded from the player-fit set.
- ⚠️ THE MAZE IS STILL A LATTICE OF STRAIGHT PATHS. The spanning tree
  changed WHICH edges exist, not what an edge LOOKS like: every corridor is
  still a straight axis-aligned capsule between cell centres, every junction
  90 deg. So 15.9% dead ends and mean degree 2.504 measured maze-ness of the
  TOPOLOGY while the eye sees a sparser grid. Sixth time a metric answered a
  question adjacent to the one that mattered.

Two tool issues found: PowerShell needs -modes="render,walk" quoted or it
parses as render-only; and "showcase slot" is ambiguous (cards use 1/6, the
Maze diagnosis uses 4).

Determinism spot-checked: repeated arguments give identical SHA-256 JSON.
Measurement-only changes; no generation, parameter or threshold edits.
Editor build succeeded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-07 18:33:15 +02:00
Fr0zkaandClaude Opus 5 bd547c35c4 feat(explore): a commandlet -- 91x faster, and the first real PICTURE
Every experiment in this project cost a build + full-suite cycle. The
landing task took 52 of them over ~9 hours. And nobody could SEE the world:
every visual judgement all session was made on density SLICES -- diagrams,
not pictures. That is how a floor heaving 1.4 m every two metres got
described as "roughness visible but modest" while Jahni spotted it in
thirty seconds by walking it.

-run=VoxelForgeExplore, editor-only module, three modes, one build many runs:

  render  raymarched PNGs, fixed 0.25-voxel step + 10 bisection iterations
          (NOT sphere tracing -- the op stack yields density, not a signed
          distance, so a long step tunnels through walls). Three views, each
          with a 1.76 m human marker in frame, because every scale mistake
          here came from numbers with no body beside them.
  walk    an agent using the EXISTING VF player-fit stencil, never its own
          movement model -- two definitions of "can a player stand here"
          would drift and we could not tell which was lying.
  export  OBJ through the canonical UVoxelMarchingCubesMesher. No second
          mesher; a forked meshing invariant is the bug this project keeps
          paying for. 23.2 MB, 106,762 verts, 200,842 tris for 128^3.

⭐ THE NUMBER THAT JUSTIFIES IT: the full automation run takes 4,396 s
(73m16s). The commandlet does all three modes in 48.1 s wall, 5.9 s of
which is engine startup. ~91x faster. And I had guessed the suite cost
~90 s -- it is 73 MINUTES. The iteration loop was far more expensive than
I knew.

First real walk metrics on the Maze (seed 0, slot 4):
  distance 214 m · tortuosity 4.98 · dead ends 0.935/100 m
  narrow-gap events 0 · reachable fit fraction 3.01%
  result NotConnectedAtThisResolution
Tortuosity ~5 is a genuinely winding route, which is right for a maze.
3% reachable is not.

Editor Development build clean. SHIPPING build clean with the receipt
containing VoxelForge and NOT VoxelForgeEditor -- the instrument cannot
reach a packaged game. Automation 32/32, 0 failed.

⚠️ Codex initially shipped this UNBUILT, and correctly: I omitted the
## Validation section from that spec, so it fell back to CLAUDE.md's
"NEVER build or compile". 100+ compile errors from three causes -- bare
TCondensedJsonPrintPolicy and TJsonWriter needing their template
arguments, and a missing UObject/Package.h. That include is the SECOND
miss this session, so CODEMAP §6 now carries the gotcha.

⚠️ Unrelated, found in passing: VoxelForge.uplugin does not declare
RealtimeMeshComponent although the runtime module uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-07 04:07:07 +02:00
Fr0zkaandClaude Opus 5 413c15de24 feat(passages): §6.5 landings -- you arrive somewhere you can stand
The last unbuilt Tier 1 item. A passage was a bore through rock with no
floor inside the walkable angle, so its mouth was not a standing position
and neither was anything near it -- the departure mouth measured a FOUR-CELL
player-fit component. You arrived and there was nowhere to stand.

Both ends of all 7 passages now open into a landing, sized from the body:
  capsule diameter 2.72 vox      floor >= 12 vox (3 m) to stand and turn
  capsule height 7.04 vox        room >= 12 vox tall, stock mouth gives 18 (4.5 m)
  3-voxel floor slab, tube tangent to it -- no lip to climb out of
  connector 5-vox radius: 2.5 m clear width, 3 m clear height
  ramps over 44 deg get a deterministic level dog-leg

⭐ THE MOUTHS ARE NO LONGER SPECKS:
  archetype        components      largest      arrival-departure
  Maze             309 -> 340      176 -> 3210    43-81  -> 3210-3210
  TunnelNetwork    467 -> 227     1292 -> 3841     2-1   -> 3841-269
  Underwater        86 -> 117       35 -> 3838     3-11  -> 3838-267
  FlatPlain         45 ->  11      259 -> 2130     1-1   ->  268-273
  CrystalChamber   197 ->  90       24 ->  286     1-1   ->  286-274
  FloatingIslands    2 ->   1        3 ->   72     1-1   ->   72-72

Law 1/8 -> 2/7 connected (1 refused on MaxCells), and critically ZERO
connections now run through a four-cell pocket.

Every landing floor was verified against the FINAL density, after all posts:
14/14 pass the 44 deg slope bracket, worst room gradient .059, worst
connector .522. 14/14 deterministic root joins. 0 seal failures.
0 box-verdict violations across 82 proved + 174 mixed, 109,142 voxels.
Op-stack bit-identical, order-independence green over 10 slots / 31
passages / 3 pool orders. Cache 0.665 us rebuild, 0.646 us hot.

Post order: origin spine -> vertical seal -> passage tube + landing floor
-> XY edge seal. Landings clamp 4 voxels inside the vertical seal; the XY
seal stays last and wins at world edges.

⚠️ ARCHITECTURAL NOTE, not a detail: landings join a deterministic annular
ROOT HUB at (0,0), built identically for every endpoint -- no nearest-
component search, no passage ordering. That is what makes the join
guaranteed rather than likely, and it extends the (0,0) spine concept from
a landmark into a structural network. It also means routes tend to pass
through the centre, which is a gameplay consequence worth Jahni's eye.

⚠️ AND IT DOES NOT CLOSE THE LAW. Codex is explicit: the landings guarantee
membership in the new root network, NOT in the archetype's pre-existing fit
component. The whole-strate Maze check still shows a 71.9-voxel gap between
1,235 and 2,938-cell components. So the native corridor networks remain
disconnected from the root network in places. Real, separate, next.

52 build-and-measure cycles, ~9 hours, finding real geometry bugs on the
way (a landing floor overlapping the ramp cells and hiding a floor
discontinuity; the disturbance pass refilling connector headroom).

Build clean, 32/32 success, 0 failed. Showcase regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 23:55:47 +02:00
Fr0zkaandClaude Opus 5 e4e159f0cd test(measure): the window was NOT the problem -- my sixth hypothesis dies
I thought the fitted mouth-AABB window was cropping an origin-routed path
and producing a false NotConnected. Codex tested it three ways at step 1 on
the Maze candidate Jahni walked:

  mouth-AABB + margin      842,996 cells    NotConnected
  origin-inclusive       3,251,556 cells    NotConnected
  WHOLE STRATE          31,881,528 cells    NotConnected

115,578 fit cells, 2,903 components, largest 11,418 -- and the mouths sit
in components of 172 and FOUR cells, 274 voxels apart. The window was never
the problem. Sixth measurement hypothesis, first one that was wrong in the
other direction: this time the instrument was fine and I doubted it.

The window rule still improved: bIncludeOriginInCoverWindow now covers the
route an origin-rooted topology actually implies (Maze, VerticalShafts,
TunnelNetwork, Underwater), expansion happens BEFORE MaxCells, and an
over-cap window is REFUSED honestly instead of silently cropping back to a
mouth-only box. VerticalShafts refused at the 4M production cap and said so.

Corrected law across eight: 0/7 measured Connected, 1 unknown
(CoarseLiedBudgetExhausted), 1 refused.

⇒ FIVE CAUSES NOW ELIMINATED: size (Jahni walked it), the fit stencil
(fixed, 309 pockets -> 96), placement (mouths land on legal poses),
corridor connectivity (spanning tree, 0 disconnected cells over 64 seeds),
and the measurement window (whole-strate says the same thing).

⭐ AND THE REMAINING CAUSE IS NAMED BY ITS OWN NUMBER: the departure mouth
sits in a FOUR-CELL player-fit component. A passage tube is a bore through
rock -- sloped or vertical, no floor within the walkable angle -- so the
mouth point is not a standing position and nothing around it is either.
You arrive and there is nowhere to stand.

That is precisely COMPOSER-NOTES §6.5 / Tier 1's unbuilt item: "the passage
mouth becomes a room plus a distinctive landmark". It was never started.
The law cannot pass while a mouth opens into a bore instead of a landing.

Build clean, 32/32 successful, 0 failed, same two known warnings.
WorldRadiusVoxels=0, lateral gating, determinism, equivalence all intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 12:58:56 +02:00
Fr0zkaandClaude Opus 5 2eaceb5984 feat(maze): a spanning tree -- it is a maze now, not a grid
Jahni: "mostly a cube full of straight pathway along the edges... basically
just a repeating pattern of tunnel connecting to each other, definitely
unwanted". And the measurement: the arrival mouth in a 30-cell pocket while
the network held 1,217. SAME CAUSE -- edge selection was independent hash
gating per +X/+Y/+Z edge, i.e. a random subgraph of the cubic lattice.
Every junction identical, and connectivity left to chance.

Fixed the way VerticalShafts was fixed in 7a20eee: guarantee it by
construction. Each non-origin node hashes ONE parent axis toward
(0,0,0) so Manhattan distance strictly decreases -- a proof of a global
rooted tree, not a probability. Loops are capped at 0.18 x
BranchProbability and 0.10 x Verticality and can never supply connectivity.
⛔ No edge probability was raised; percolation is a phase transition, not a
knob.

  64 seeds x 9,261 nodes:  0 disconnected cells, 0 edge-count violations
  determinism:             20,000 bit-identical checks
  seam freedom:            42 probes, 0 legacy and 0 op-stack mismatches

⭐ AND IT READS AS A MAZE, in numbers rather than adjectives:

  degree   1 (dead ends)  15.9%      mean degree 2.504
  degree   2              36.6%      run length median 1, p90 3, max 18
  degree   3              31.7%
  cubic-grid control:     100% degree-6, 0% dead ends, mean 6.000

Two abreast, derived not nudged: 3 m usable floor / 0.7 = 4.286 m clear
bore, + roughness reach 3.75 voxels => CorridorRadius 12.5 (6.25 m bore),
giving 3.06 m of usable floor. CellSize 64 still leaves a 7.875 m wall
between neighbouring bores.

BranchProbability and Verticality are now LOOP-DETAIL knobs, not
connectivity controls. Documented at their declarations so nobody tunes
them expecting the old meaning.

⚠️ The player-fit law still fails, and Codex named the likely reason in its
caveats: "global tree connectivity does not guarantee connectivity inside
an arbitrary finite cropped measurement window." The tree routes toward the
ORIGIN, and the fitted window is the AABB of the two mouths -- so the
connecting path runs A -> origin -> B and leaves the box. The coarse maze
graph now measures Connected while the fine windowed law does not. That
smells like the sixth measurement artifact of this session, and it is the
next thing to test rather than to assume.

Box audits: existing maze 104 proved / 0 violations; novel fill stack 240
proved / 319,440 voxels / 0 violations. Op-stack bit-identical over 20,000
samples. Perf 1.13 us rebuild, 0.145 us hot call.

Showcase regenerated. Build clean, 32 tests, 30 clean successes, 2 known
deliberate warnings, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 11:12:52 +02:00
Fr0zkaandClaude Opus 5 4378385b37 fix(passages): land mouths where a BODY fits -- and the cause is now cornered
VF_SuggestLandingPoint aimed at open AIR. Air is not where a body fits: a
mouth could sit in a perfectly open pocket no capsule can stand in. It now
requires a legal capsule pose -- clearance (r 1.36, half-height 3.52),
support within a 1.8-voxel / 45 cm step height, a walkable support PATCH
(4 of 5 columns, centre required, slope <= 44.8 deg), positive density
throughout the capsule, strict 2x seal interior, bounded lateral snap.

It stays a PURE free function, which was the hard constraint --
GeneratePassages runs inside Initialize while the layout is half-built. So
it cannot flood-fill, and Codex correctly did not pretend otherwise: it
aims at deterministic guaranteed features instead (room centre/floor, slab
floor, horizontal maze edge, shaft feature-core ledge pose, island top).
Both mouths query their own strate independently; no passage chaining, so
LayoutOrderIndependence stays green.

Placement measurably improved:
  Maze            comps 309 -> 82,  largest 176 -> 1217, gap 206 -> 122
  VerticalShafts  comps 112 -> 36,  largest  96 ->  619
  CrystalChamber  gap 47.5 -> 19.1, departure mouth now in a 717-cell comp
  Underwater      comps  86 -> 13

⚠️ AND THE LAW IS STILL 1/8. Reported honestly rather than tuned. Mouths
now land on legal body-sized poses; the poses just are not connected to
each other.

⇒ BY ELIMINATION, THE CAUSE IS GENERATION. Four candidates, three dead:
  size        DEAD -- Jahni walked it; bore measures 3.62 m vs 3.677 m spec
  the stencil FIXED -- fragmentation was 3x overstated by an exact-Z-1 floor
  placement   FIXED -- mouths now sit on legal poses, several in large comps
  GENERATION  STANDS -- body-sized space exists in big components that do
              not reach each other

⭐ AND THE MAZE TIES TWO OF JAHNI'S COMPLAINTS TOGETHER. Its arrival mouth
lands in a 30-cell pocket while the main network holds 1217. Edge selection
is INDEPENDENT HASH GATING per +X/+Y/+Z edge
(VoxelDensityOpStack.cpp:582/584/586) -- so the corridor graph is a random
subgraph with no connectivity guarantee, exactly the percolation failure
VerticalShafts had before its drainage tree. That is the same root cause as
"mostly a cube full of straight pathway along the edges": a spanning tree
would make it BOTH feel like a maze (dead ends, uneven runs) AND be
connected by construction. One fix, two complaints.

Codex found a real error in my spec: an infinite shaft cylinder has no
support surface AT its axis, so the old exact-axis assertion was
unsatisfiable. It now requires a deterministic feature core and audits tree
connectivity from the resolved axis.

Build clean, 29 succeeded / 0 failed / 2 known deliberate warnings.
Box audits 0 violations. Shaft refinement 16/16. Order-independence green
across 10 slots, 31 passages, 3 pool orders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 08:08:38 +02:00
Fr0zkaandClaude Opus 5 110ae9d0ed fix(measure): the fragmentation was the instrument -- 309 pockets -> 96
Jahni walked the maze end to end while the metric called it 309
disconnected pockets whose largest held 176 cells, with the arrival mouth
in a THREE-CELL pocket. Direct observation beat the instrument, again.

Cause, found exactly: the stencil demanded a solid cell at precisely
(X,Y,Z-1). No supporting-surface search, no slope tolerance, no step
height. At step 1 a floor that slopes even slightly loses its solid cell
under most of its air, so a walkable corridor dissolved into specks.

Now: a bounded downward search for a COHERENT SUPPORTING SURFACE within a
step-height tolerance (MaxStepHeight 45 cm = 1.8 voxels), a walkable-slope
normal test (WalkableFloorAngle 44 deg), then the full capsule re-tested at
the resolved support height. Deliberately NOT "any solid within N cells",
which would accept ledges and isolated pixels.

⭐ ON THE WALKED MAZE (seed 0, index 50, slot 4):
  components   309 -> 96          largest       176 -> 1401
  fit cells   2801 -> 6668        mouth comps  43/81 -> 147/1401
The 300-pocket world was a measurement artifact. Fifth time this session
the instrument was the suspect; fourth time it was the culprit.

⚠️ AND IT IS NOT SELF-FLATTERING -- a metric change that only ever makes
numbers rise is worthless, so it ships with controls that must REJECT:
  vertical shaft wall      0 fit cells
  isolated 1-voxel ledge   0 fit cells
  overhang underside       0 fit cells
  ramp steeper than 44 deg 0 fit cells
  coarse step 4            refused, not guessed
Open flat-floor control: 6.13% of air cells fit, ONE component of 12,168 --
sensible, since only near-floor poses qualify in a tall room.

⚠️ BUT THE ROUTE STILL DOES NOT EXIST, and that part of H3 survives. The law
is 1/8 before and after -- what changed is its honesty: the one connection
used to be FloatingIslands through FOUR cells, and is now FlatPlain through
2,221 with a zero gap. Seven archetypes still have both mouths in separate
player-fit components 56-169 voxels (14-42 m) apart, and those components
are now large and real (663-2902 cells), not specks.

⇒ So the world HAS substantial connected player-sized space. The passage
MOUTHS are not landing in it. That is the Tier 1 landing problem again,
one level down: VF_SuggestLandingPoint aims at open AIR, and air is not
where a body fits. Next task.

Codex found three errors in my baseline quoting, all mine: I wrote "0/8
connected" while also saying one connects through four cells (it was 1/8),
misquoted SurfaceWorld's gap as 1101 when the saved value is 110.648, and
mixed provenance on the VerticalShafts row.

Build clean, 31 succeeded / 0 failed. The 2 warnings are both deliberate:
the lateral-regions gate, and a test reporting that its own sample "barely
asked its question".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 04:46:01 +02:00
Fr0zkaandClaude Opus 5 cf5f83e9f0 test(diag): H1 dead, H3 measured -- player-sized space is SHATTERED
Diagnosis only, no generation or parameter changes.

⭐ H1 IS DEAD, confirmed by Jahni's playtest and by measurement. He walked
the maze candidate: "i CAN go through it, the tunnels are just the right
size with very tiny wiggle room." The radial probe agrees: measured bore
span 3.62 m against a 3.677 m parameter. The carve produces what it claims;
the scale change worked.

⭐ H3 IS WHAT THE NUMBERS SHOW. Player-sized volume EXISTS but is broken
into hundreds of disconnected pockets, with the two mouths in different
ones, tens to hundreds of metres apart (step 1, fitted window):

  archetype        fit cells  components  largest  arrival/departure  gap
  Maze                 2801        309       176      43 / 81       206 vox (51 m)
  TunnelNetwork        2748        467      1292       2 / 1        164 vox (41 m)
  VerticalShafts       1156        112        96       3 / 36        94 vox (24 m)
  SurfaceWorld          713        520         6       1 / 1       1101 vox (275 m)
  CrystalChamber        655        197        24       1 / 1         48 vox

Only FloatingIslands connects, through FOUR cells.

⚠️ BUT I DO NOT ACCEPT "H3, therefore the world is shattered" YET, and the
reason is Jahni. He WALKED a maze end to end that this metric says is 309
disconnected pockets whose largest holds 176 cells. Direct observation
contradicts the instrument. An arrival mouth sitting in a THREE-CELL pocket
is not a description of a cave; it is the signature of a stencil that
fragments a continuous floor.

H2 is the likely cause of that fragmentation, and Codex found it exactly:
the fit test demands a solid cell at EXACTLY (X,Y,Z-1) -- no supporting-
surface search, no slope tolerance, no step height. HeadroomCells applies
only to the legacy walkable metric, not the capsule mask.
  VoxelStrateMeasure.cpp:697 (stencil), :729 (the exact-floor test)
At step 1, any floor that slopes or is rough loses its solid cell at Z-1
under most of its air, so a walkable corridor dissolves into specks.

⇒ SEQUENCING: fix the stencil first, THEN re-measure H3. Until the
instrument stops shredding continuous floors its component counts cannot be
trusted. Fifth time this session that the measurement was the suspect.

Recommended stencil, Codex's design and I agree: a bounded downward search
for a COHERENT SUPPORTING SURFACE within a step-height tolerance, then
re-test capsule clearance at that support height -- explicitly NOT "any
solid within N cells", which would accept ledges and isolated support
pixels.

Fixed the mixed-resolution reporting that nearly cost me a false finding:
fields are now coarse_step4_* and *_step1, never both unlabelled on one
line.

Recorded from the playtest, not acted on:
- The maze is a GRID, not a maze. Edge selection is independent hash gating
  per +X/+Y/+Z edge (VoxelDensityOpStack.cpp:582/584/586), so every junction
  looks alike. A spanning subset would give dead ends and uneven runs.
- TunnelNetwork DOES have explicit room-to-room connections
  (VoxelCaveMorphology.cpp:1406 backbone, :1465 tunnels) -- so Jahni's
  "lack of real passageway" applies to FlatPlain/CrystalChamber, which are
  slab voids with no room graph at all.
- Two-abreast wants 2.5-3.0 m usable floor; a 4 m circular bore gives ~2 m.

Build clean, 31 succeeded / 0 failed, run by me after Jahni closed the
editor (the previous attempt could not link -- the DLL was held by his
running editor).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 03:16:55 +02:00
Fr0zkaandClaude Opus 5 fc43a86709 feat(scale): size the world to a human body -- and it is NOT enough
Jahni: "can't you try and make the world to scale?" Every length parameter
is now derived from the body and annotated with its metre value, so nobody
has to do the arithmetic to notice something is absurd.

  CorridorRadius   4 -> 8     1 m -> 2 m radius (4 m bore)
  CellSize        40 -> 64    keeps a real wall between wider corridors
  TunnelMinRadius  3 -> 6     0.75 m -> 1.5 m
  ShaftMinRadius   5 -> 8     1.25 m -> 2 m
  IslandMinRadius 18 -> 24    4.5 m -> 6 m
  MouthRadius      6 -> 8     passage mouths 4 m
  plus passage reach, spiral, cascade, bridges, ridges, chasms

The reasoning that motivated it still looks right: a 2 m circular bore for
a 1.76 m player clears the head only along the exact centreline, so almost
no cell has a floor with 7.04 voxels of clear air above it. A 4 m bore is
genuinely walkable.

⚠️ BUT IT DID NOT FIX TRAVERSABILITY, and this time it is not the
instrument. The player-fit law is measured at STEP 1 on a mouth-fitted
window -- full resolution, both mouths inside the box -- and across the
eight showcase archetypes it reports:

  6 NotConnectedAtThisResolution   (real negatives at full resolution)
  2 OutOfWindow                    (unmeasurable, not passing)
  1 StartCellNotPlayerFit          (the arrival mouth itself is too small)
  0 CONNECTED

And every archetype reports hard-gate survivors=0, so the showcase is
displaying the least-bad of an entirely failing set.

⛔ DO NOT READ player_fit_fraction / player_clearance FROM THE SHOWCASE
LINE. Those come from the COARSE step-4 measurement in the same log line,
where the 1.36-voxel player is sub-cell and fit is unmeasurable --
player_clearance reads a uniform "1.00 voxels" for all eight archetypes,
which is one cell, not one voxel. Only restricted_law is at step 1 and
only it is trustworthy. Fixing that mixed-resolution reporting is next.

So scale was necessary and insufficient. My hypothesis was that bore
diameter versus body height was the binding constraint; doubling it moved
nothing measurable. Something else is preventing player-sized volume from
existing at all -- worth finding before tuning any further.

Codex applied these edits and then died mid-poll without validating them
(third time a killed agent left a clean-looking unbuilt diff). Build and
suite run by me: build clean, 30 succeeded / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 00:43:33 +02:00
Fr0zkaandClaude Opus 5 eb47efdec9 feat(measure): a player does not fit. The law was passing impossible routes.
Jahni walked composed strates for the first time: "'maze' is way too tight
tunnel radius wise, i can't enter nor anything". The candidates he could not
enter were the ones our metrics ranked BEST -- Maze had the highest walkable
fraction (.285) and highest connected surface (.99) of all eight.

⭐ THE HEADLINE, AND IT IS BAD NEWS WORTH HAVING:

  old arrival->departure law     passing on 7 of 8 candidates
  player-fit-restricted law      CONNECTED ON 0 OF 8

**The primordial law has been passing on routes no player fits through.**
Every "the world is traversable" claim in this project measured air, not
passage. Player-fit fraction across the eight showcase candidates ranges
.0000 (FloatingIslands -- literally zero) to .0117. The season composer now
returns 0 survivors for 6 required slots, and the showcase gate rejects
8 of 8.

That is the correct outcome. The gate is not tuned to a threshold; it
requires a resolved player-fit volume and a connected player-fit route.

Capsule confirmed by Jahni as the base ACharacter: 34 cm radius, 88 cm
half-height = 1.36 voxels radius, 7.04 tall. Named constant with the
derivation beside it.

Two traps avoided, both of which would have produced confident nonsense:
- THE COARSE GRID CANNOT RESOLVE A PLAYER. SampleStep 4 makes a cell 4
  voxels, ~3x the player's radius, so fit is below the resolution of every
  metric built so far -- which is exactly why nothing flagged an impassable
  maze. Fit runs on step 1 and REFUSES (bPlayerFitResolved=false + reason)
  when the step is too coarse.
- Manhattan distance >= Euclidean, so reusing the existing transform for
  clearance would OVERESTIMATE free space and report fits that do not
  exist. Clearance uses a separate Chebyshev transform; Manhattan stays
  only on the legacy feature-scale metric.

⚠️ MY ROUGHNESS HYPOTHESIS WAS WRONG, and Codex disproved it rather than
confirming it. Forcing SurfaceRoughness=0 on the Maze moved PlayerFit
.0105 -> .0138 and the restricted law stayed NotConnected. Minimum corridor
clearance is 1.00 voxel (25 cm) with or without roughness -- the corridors
are simply too tight for a 34 cm capsule, full stop.

Roughness IS still a real pinch risk where reach exceeds corridor radius:
Underwater 2.21x, FlatPlain 2.31x, CrystalChamber 1.81x, TunnelNetwork
1.44x on corridors, VerticalShafts 1.30x on connectors. The 5.625-voxel
default bound stays dangerous.

Islands measured, not changed (feel is Jahni's call): 0/2 touch either
seal, median clear gap 47.8 above / 9.0 below -- but effective radius
spans 27-258 voxels at H=128 with ThicknessRatio .546, so the worst case
has zero guaranteed separation. Recommendation recorded: radius back to
18-42, StrateHeightInChunks 4 -> 6, ThicknessRatio <= .40.

Build clean, 30 succeeded / 0 failed, verified in the namespace log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 12:33:58 +02:00
Fr0zkaandClaude Opus 5 faa62d419e feat(runtime): the composed world can BE the game world
Jahni: "can you not implement those settings? as in, have them be live in
game?" This is that. The composer stops being a lab tool.

THE ARCHITECTURAL LINE, which is the whole design (COMPOSER-NOTES §3.1):
COMPOSITION stays editor-only, EVALUATION becomes runtime. Rolling,
measuring, preview, selection, promotion, the corpus -- none of it reaches
a player's machine. What crosses into the runtime is exactly two things:
reading a season manifest, and materialising a recipe into an op stack.

  UVoxelSeasonAsset      a cookable UPrimaryDataAsset holding the manifest,
                         so a season ships in a packaged game. The JSON
                         stays the reviewable, git-diffable artefact.
  UVoxelSettings::Season  optional. Unset => behaviour is exactly as today.
  GetEffective*()         season seed / spine radius / world radius / season
                         number override the authored values when assigned.
                         The test deliberately sets DIFFERENT authored
                         values, so "the season is authoritative" is proved
                         rather than coincidental.

⭐ MULTIPLAYER DETERMINISM, PROVED: two independent runtime managers built
the same season and compared 393,216 density samples BIT-IDENTICALLY. This
is the property the whole multiplayer model rests on -- replicate seed +
layout + diff, never geometry.

⭐ AND A STALE SEASON IS DETECTABLE, NOT SILENT. Initialize() now returns
bool and an assigned-but-invalid season NEVER falls back to the authored
pool. Falling back would hand a client a world that looks fine and is not
the season -- silent divergence, the worst failure mode in a shared world.
GetCurrentSeasonContentHash() is exposed BlueprintPure to compare at join.

HasComposerRecipeOverride() is gone. It force-classified every custom recipe
as Mixed, which was correct while box verdicts were only proved for
hand-written stacks -- but 8e346a3 made EffectOverBox correct in ISOLATION,
proved on a stack no builder produces. Keeping the force would have cost
tile skipping on every composed world forever.

VF_RollStrateRegionCount moved behind WITH_EDITOR while the partition-seed
helpers stayed runtime: rolling out, evaluation in. Lateral regions remain
gated off.

⚠️ Sol hit its usage limit mid-task, so the edits arrived unvalidated. I
built and ran them myself rather than assume -- and they did not compile:
VoxelForgeComposerSeasonTest.cpp was missing UObject/Package.h, which every
other test picks up transitively through the shared fixture it does not use.
One include; every NewObject/GetTransientPackage in the file was failing.

Editor build clean. SHIPPING BUILD SUCCEEDS -- the invariant that proves no
composer roller is linked into a packaged game. 30 succeeded / 0 failed.

⚠️ UNVERIFIED: whether it looks right on screen. Nothing headless can tell
us that; it needs Jahni to assign a season asset and press play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 08:37:01 +02:00
Fr0zkaandClaude Opus 5 088b40216d feat(composer): showcase of all 8 archetypes -- and "not navigable" answered
Jahni asked for one strate per archetype to look at, and said the previews
looked "not navigable nor anything except maybe the mazes". Both delivered.

⭐ PART A -- HIS EYE WAS RIGHT, AND SO WAS THE METRIC. BOTH EXPLANATIONS
WERE TRUE, and they split by archetype:

  archetype        walkable   FLOOR AREA   largest connected surface
  CrystalChamber     .059        .9999          1.00
  FlatPlain          .048        .9985           .9998
  SurfaceWorld       .064        .9374           .9936
  Maze               .285        .2629           .9905
  Underwater         .229        .2061           .3312
  TunnelNetwork      .155        .2084           .4640
  VerticalShafts     .108        .1138           .1449
  FloatingIslands    .004        .1016           .4992

1. THE METRIC WAS WRONG FOR CHAMBERS. Walkable fraction is volumetric: an
   air cell with solid one step below and headroom above. In a cathedral
   almost no air cell is near a floor, so a huge open room scores ~.06 while
   a tight maze scores ~.29. It was measuring CORRIDOR-NESS, not
   navigability -- and mazes win that by construction, which is exactly the
   pattern he spotted. Slab families actually have 94-100% walkable FLOOR
   AREA, fully connected. They were never the problem.

2. AND FOUR ARCHETYPES ARE GENUINELY FRAGMENTED, which is a real content
   finding, not an artifact. Largest connected walkable surface:
   VerticalShafts .14, Underwater .33, TunnelNetwork .46, FloatingIslands
   .50. In VerticalShafts, 86% of the floor is unreachable from the biggest
   patch. The primordial law still passes -- you CAN get from arrival to
   departure -- but most of the floor is not part of that. That is precisely
   "not navigable" and it needed a new metric to see.

New metrics: WalkableFloorAreaFraction, NumWalkableSurfaceComponents,
LargestWalkableSurfaceShare. A floor you cannot cross is not a floor.

Web references checked, per his suggestion -- we invented every metric in
this project without once testing them against reality: NPS solution-cave
and Mammoth Cave morphology, Lehman Cave dimensions (Talus Room 90x376 ft,
21,511 sq ft floor, 113 ft ceiling), Deep Rock Galactic cave-design notes,
Team Cherry on Metroidvania maps. They support heterogeneous room+passage
space with branches and shafts -- NOT a universal floor-to-volume target.
So there is no single number to optimise, which is itself worth knowing.

PART B -- Saved/VoxelForge/Showcase/index.html, 8 cards, 64 PNGs, fine
renders (step 1, radius 64, centred on LargestComponentPoint), 0 blanks.
Each card carries the exact PIE values to type in, because navigability is
judged by walking:

  CrystalChamber  seed 7331 idx 34 slot 1     Maze            seed 0 idx 58 slot 1
  FlatPlain       seed 0    idx  4 slot 1     SurfaceWorld    seed 0 idx 11 slot 2
  FloatingIslands seed 0    idx 34 slot 1     TunnelNetwork   seed 0 idx 61 slot 1
  Underwater      seed 0    idx 46 slot 1     VerticalShafts  seed 0 idx 15 slot 1

All bComposerRollStructure = false. Selection searched seeds 0/7331 x
indices 0-63, applied the three hard gates, then scored
floor_area + .25*clearance + .10*largest_surface.

Caveats kept honest: floor area is a sampled XY proxy, projected
connectivity is not a 3D traversal proof, "best" means best within the
searched range.

Build clean, 30 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 06:59:51 +02:00
Fr0zkaandClaude Opus 5 dd38832bb6 feat(composer): lateral REGIONS -- built, measured, and gated OFF
Jahni's own idea: "archetype could even mix within the same strate at some
point, tunnel network leading to a big chamber". An archetype becomes a
spatial REGION inside a strate, not a label on it.

Built by extending the mechanism that already exists rather than inventing
one: the stack blends VERTICALLY between strates, so regions blend the same
way LATERALLY. Deterministic jittered Voronoi sites on a 256-voxel lattice,
1-3 regions per strate from (seed, strateIndex), 24-voxel symmetric blend
band, per-chunk lookup cache. Parameters are never blended -- a shaft's
ShaftSpacing means nothing to a slab -- only the resulting DENSITY is.
Structural posts stay global and unconditional, so an invented region
cannot switch off the primordial law.

  single-region path   BIT-IDENTICAL to legacy, 256 probes, native + recipe
  cross-boundary boxes 20 of 40, 199,800 voxels brute-forced, 0 VIOLATIONS
                       (every seam-spanning box correctly returned Mixed)
  blend-band cost      663 ns vs 338 ns per voxel, over 16.0% of voxels
                       ~1.15x field-weighted
  survival             16/16 valid, non-vacuous, largest share >= .50

⛔ AND IT IS GATED OFF, because the seam breaks the one law that matters:
arrival -> departure across a region boundary passes 9/16 seeds, against
11/16 same-region controls in the same harness. Density blending alone does
not guarantee a player can cross a seam. A strate that mixes archetypes and
strands you at the boundary is strictly worse than one that does not mix.

VF_LateralRegionsAreShippable() returns false and the test ASSERTS THE GATE
IS SHUT while the rate is imperfect -- so flipping it without fixing
connectivity turns the suite red by design. The rate is a warning, not an
assertion: it is the measured state of an off-by-default feature, not a
regression in a live world, and a permanently red suite stops being read,
which is how real failures get missed.

Everything that ships is still hard-guarded: single-region bit-identity,
zero cross-boundary box violations, and every shipping-path law test
untouched.

The fix is an explicit CROSS-SEAM CORRIDOR CONTRACT -- a design decision,
recorded rather than made unilaterally. Codex diagnosed this correctly and
declined to tune the threshold, which was the right call.

Build clean, 29 succeeded / 0 failed, run and verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 05:06:32 +02:00
Fr0zkaandClaude Opus 5 42e1f0ec8c feat(composer): Tier 4c -- a season that can actually ship, with a veto page
The composer could invent, measure and show strates. What did not exist was
an ARTEFACT a season could ship. Now it does.

VF_ComposeSeason: generate N candidates -> measure -> reject failures ->
select a descent sequence -> emit a JSON manifest + a review page.

⭐ THE PROPERTY THAT MATTERS: round-trip bit-identity. The manifest was
loaded, every selected stack rebuilt, and 393,216 density samples compared
BIT-IDENTICALLY. Re-composing the same seed yields byte-identical JSON.
A manifest that cannot reproduce its own world is worthless, and that is
the one thing that could not be assumed. Floats serialise as readable
values PLUS exact IEEE bits, so a season survives a text round trip.

Test season: 24 generated, 19 survived the hard gates, 6 selected.
18 not selected -- 4 vacuous, 1 primordial-law failure, 13 policy
non-selections. 4 grounded, 2 outliers. EVERY selected slot passes the
primordial law, asserted per slot rather than sampled.

Selection policy is explicit, named and PROVISIONAL, because Jahni has not
defined "good" yet and said so: "it'll be easier once i'm able to see the
strates candidates." So no quality score was invented and presented as
settled. The knobs are 75/25 grounded-to-outlier (outlier axis = distance
from corpus centroid), adjacent-similarity threshold .20 with penalty 1.0,
new-archetype bonus .10, new-recipe bonus .05, boss cadence every 5th
non-terminal slot. All of them are his to turn.

The veto is a review page in DESCENT ORDER -- the season read top to bottom
the way it is played, not the survivor/outlier ordering the candidate sheet
uses. Reuses VoxelStratePreview rather than a second renderer.

  Saved/VoxelForge/SeasonTest/season_0000_seed_9320/season_manifest.json
  .../review/index.html

⚠️ FIVE GAPS CODEX RAISED, recorded not buried:
1. §4's Tier A is strates PLUS passages; only the strate spine is persisted.
   Manager-generated passage geometry and content placement are not.
2. Authored fixed strates keep scalar vectors + asset paths, not every
   asset-level passage/biome/disturbance/content field.
3. Boss slots are MARKERS, not arena or content guarantees.
4. WorldRadiusVoxels == 0 is enforced as specced but contradicts a
   production bounded world -- Jahni's call, still unset by design.
5. Bit identity is proven for the same build and platform. A frozen season
   spanning engine versions needs an explicit schema contract. Real risk,
   since seasons freeze generation for months.

Build clean, 28 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 02:55:50 +02:00
Fr0zkaandClaude Opus 5 0b3ec18b43 feat(composer): PROMOTION -- the corpus grows itself, and it did not collapse
Tier 5. §3.3's answer to Jahni's "I'd like the system to not rely at all on
the handmade ones": promotion does not REMOVE the authored seed, it dilutes
it. Every invented strate that measures well rejoins the corpus.

Five simulated seasons, 24 candidates each:

  season  corpus  project/default/promoted  survival  spread
    1       15          4/8/3               20.8%     0.869
    2       20          4/8/8               54.2%     0.849
    3       20          4/8/8               54.2%     0.849
    4       21          4/8/9               45.8%     0.916
    5       21          4/8/9               50.0%     0.916

Corpus 12 -> 21, of which 9 are discovered. Survival roughly doubles.

⭐ AND THE COLLAPSE RISK DID NOT MATERIALISE, which was the thing worth
measuring rather than assuming. Promotion selects survivors, and blending
survivors repeatedly can converge the corpus onto a narrow safe region --
the composer would get better at passing and worse at surprising. Spread
went 0.869 -> 0.916. It dipped and plateaued mid-run, but did not narrow.

Recorded honestly: seasons 3 and 5 promoted ZERO. The near-duplicate gate
(normalised metric distance < 0.20 over air, largest share, walkable,
feature scale, clearance) is doing its job, but it also means the corpus
saturates at this scale. Five seasons of 24 demonstrates the MECHANISM, not
§3.3's "season 5 blends 200 discovered ones" -- that needs the overnight
budget, not a test.

Safety: a promoted record is RE-MEASURED on load, never trusted. Verified by
corrupting a stored metric -- the loader discarded it and re-derived the
geometry. A stale or tampered record therefore cannot introduce an
unescapable strate into a future season, which is the one failure that would
matter. Provenance (project/default/promoted) is tracked and logged.

Codex's recommendation, and I agree: keep a small explicit OUTLIER QUOTA
among candidates that pass every hard gate. This run showed no collapse, but
survivor-only promotion keeps the long-term risk, and a quota costs nothing.

PART A: the fine ROI now centres on LargestComponentPoint instead of (0,0).
Blank fine renders 6/42 -> 3/42. The label now reads "largest open space",
not "strate centre" -- the image moved, so the caption had to.

Build clean, 27 succeeded / 0 failed, verified by me. WorldRadiusVoxels=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 01:46:38 +02:00
Fr0zkaandClaude Opus 5 43eaf1ef0e test(composer): the handmade corpus is load-bearing for QUALITY, not validity
Answers Jahni's standing wish directly: "in the best of both worlds, i'd
like the system to not rely at all on the handmade ones."

Three arms, same size, same measurement, same criteria:

  approach              survival   walkable mean  feature-scale mean
  corpus blend (today)  13/16 81%    0.0661          92.3
  naive uniform          7/16 44%    0.0126          64.6
  constraint-sampled     7/16 44%    0.0129          31.4

⭐ THE ANSWER IS NOT THE SURVIVAL RATE, IT IS THE QUALITY GAP. Constraint
sampling produces VALID worlds -- it passes the primordial law more often
than naive rolling (1 law failure vs 3) -- but the survivors are materially
worse: ~5x lower walkable fraction and ~3x lower feature scale. A composer
that emits technically-passing but uniformly mediocre places has not solved
the problem.

So the corpus carries something the declared constraints do not. §3.3 said
independent rolling destroys the CORRELATIONS; stating those correlations
explicitly recovers validity but not the tacit quality that authored
vectors encode. That is worth knowing and it was not knowable by argument.

⇒ VERDICT: keep the corpus in production. Constraint sampling is a
corpus-free BOOTSTRAP (it can seed a corpus from nothing), and PROMOTION
(Tier 5) is the real route off handmade content -- dilution, not removal.
Naive rolling is rejected outright.

⚠️ UNDER-POWERED, stated plainly: 16 candidates per arm, not the 256 the
spec asked for -- the fixed 8M-cell box/law workload made 256x3 too slow to
finish. The 5x walkable gap is large enough to be believable at n=16, but
the survival rates (44% vs 44%) are not separable at that sample size. Do
not quote these as precise.

Authored-corpus audit, and Codex read it correctly: 3 of 12 vectors violate
the proposed relations (DA_Strate3 room coverage 2.68 vs bound 0.35;
Default_SurfaceWorld ground bound; Default_VerticalShafts shaft radius 5 vs
roughness margin 5.875). That does NOT mean those assets are broken -- it
means the new relations are conservative HYPOTHESES, not universal corpus
invariants. Recorded as such.

Box law held across all three arms: 768 proved, 673,486 voxels brute-forced,
0 violations. §6.2 did not reopen under corpus-free parameters.

Also noted: "uniform within clamps" was underspecified in my spec -- many
fields have no finite upper clamp. Both corpus-free arms use the same
declared H-relative envelopes so the comparison stays honest.

Build clean, CorpusFree green, WorldRadiusVoxels=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 00:04:24 +02:00
Fr0zkaandClaude Opus 5 d717bc74ff feat(editor): walk a composed candidate in PIE + why rooms read angular
PART B -- the thing Jahni asked for: "any way to test those in game?"
A candidate existed only as measurements and PNGs. Now he can stand in one.

  Select the VoxelWorld actor in PIE -> Live Edit|Composer ->
  ComposerSeed / ComposerCandidateIndex / ComposerTargetStrateIndex /
  bComposerRollStructure -> click Apply Composer Candidate.

The button and the automation tests call the SAME roll wrapper, which was
the load-bearing requirement: a second roll path could drift, and then he
would be judging a different world than the contact sheet describes. A
determinism sample is logged on apply (seed, index, recipe, archetype,
metrics, epoch, eight-point density verdict) so drift is caught the day it
happens rather than in a month.

Hazards handled, since this is the first composer code to touch the live
runtime path: preflight materialisation before any live mutation, generation
and decoration tasks drained, PassagesVersion bumped to invalidate
thread_local caches, reuse of the existing RegenerateAllChunks / Epoch
invalidation rather than a new path, no Initialize() or GeneratePassages()
during apply (the re-entrancy trap), DiffLayer locks untouched, and custom
recipes conservatively force ClassifyTile to Mixed.

Scope is deliberate: a DENSITY-ONLY overlay. Layout height, authored
passages, content, water and biome metadata stay from the existing slot;
RebuildStrates() clears it. A full composed layout replacement needs a safe
rebuild path that does not exist yet.

Editor-only, verified: a Shipping build succeeds with the bridge excluded
(96/96 actions), so §3.1's "offline only, never on a player's machine"
still holds.

⚠️ UNVERIFIED BY DESIGN: a button cannot be tested headlessly and Codex was
told not to claim it works. Jahni presses it and confirms the visual plus
the "determinism=PASS" log line.

PART A -- why rooms read angular, measured not guessed. One of three room
shapes is VoxelSDF::RoundedBox:
  ShapeA = (.8 Rxy, .8 Rxy, .8 Rz)   ShapeR = .25 Rxy
so rounding is 31% of the horizontal half-extent but 78% of the vertical.
Corners are rounded; the broad planar FACES survive. SmoothMin (blend 4)
softens room/tunnel junctions not isolated corners; the cave warp bends
geometry but preserves flat regions; roughness is surface-scale only.
The name is accurate -- "rounded box" is not "blob".

NOT ACTED ON. Jahni corrected his own read: the angular archetype is Maze
(lattice source, rectilinear BY DESIGN and correct), and TunnelNetwork is
"alright-ish". Codex's recommendation is recorded for if he ever wants it:
~5-10% boxes, 10-20% capsules, rest ellipsoids, or a larger ShapeR.

Also closed REVIEW_FINDINGS EVoxelPassageType/Style as WONTFIX, enacting a
2026-08-16 judgement that recommended closure and was left open.

Build clean (Editor + Shipping), 25 succeeded / 0 failed, run by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 22:03:01 +02:00
Fr0zkaandClaude Opus 5 90c40c8d5a feat(preview): fine ROI renders + density contour -- the caves look like caves
Implements Codex's own recommendation from its critique, after it settled
the owner's two observations with evidence:

  "hard walls"  -> THE RENDERER. rg (room graph) re-rendered at step 1 is
                   rounded and organic. The hard edges were 4-voxel
                   sampling drawn nearest-neighbour. Fifth time in this
                   project that an alarming result was the instrument.
  "like a maze" -> REAL AND CORRECT. lt (lattice) stays rectilinear at
                   step 1 because it IS a maze; 19 of 64 candidates use
                   that source.
  "noisy"       -> ALIASING. nr#22 speckle 7 solid / 1 air at step 4 ->
                   0 / 0 at step 1, broad shapes intact.

Built: an optional fine ROI pass at step 1, radius 64 (~1.8M cells, cap
2M), run only for the 42 coarse survivors, plus a deterministic density=0
CONTOUR rendered beside the filled view.

Both views are kept side by side on purpose. The filled view is honest
about what was sampled and carries the walkability evidence; the contour
is legible. Codex's line, and it is the right principle: anti-aliasing the
coarse image "would only cosmetically blur the evidence and could invent
geometry". Do not replace evidence with beauty.

Fine labels carry their own sample step and resolved window so a fine
image can never be mistaken for a coarse one, and a MaxCells refusal shows
in the page rather than looking like a failed render.

  fine sampling 71.1 s · rendering 0.6 s · peak grid 9.34 MB
  42 filled pairs, 42 contour pairs, 0 refusals, 424 image links, 0 missing

Codex corrected my spec twice in the critique, both sharp: a binary step-1
raster ALWAYS has one-voxel axis-aligned edges, so only persistent
LARGE-SCALE right angles prove boxy generation (my criterion would have
produced a false "it is boxy" verdict); and persistent speckle is not
necessarily a roughness modifier.

Roughness deliberately untouched -- its call to judge that only after fine
rendering exists, and it stands.

⚠️ KNOWN, next: the fine ROI is centred on (0,0), so a candidate whose
geometry sits elsewhere renders blank (candidate 003 is a flat sheet with
no surface in the ROI). FVoxelStrateMetrics already carries
LargestComponentPoint -- centre the ROI there instead.

Build clean, 25 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 20:05:14 +02:00
Fr0zkaandClaude Opus 5 993512f0bc feat(composer): F1 -- the candidates are VISIBLE, and detail is invisible
PART B: the preview. Jahni, asked what "good" means for ranking a season:
"good is hard to describe, i believe it'll be easier once i'm able to see
the strates candidates." So the ranking policy was blocked on this, not on
more metrics. COMPOSER-NOTES §3.4 promised the preview comes nearly free
once the measurement exists; collected.

Per candidate, rendered from the grid the measurement ALREADY built (no
second sampling of the world): a vertical XZ section and a plan XY slice,
solid dark, air light, walkable cells highlighted, with a chunk scale bar
so a corridor is distinguishable from a cathedral. Plus a self-contained
index.html contact sheet of all 64 -- survivors first, outliers first
within them, recipes, metrics, and every rejected candidate labelled with
WHY (vacuous 3, fragmented 2, law failed 19).

  Saved/ComposerPreview/<runid>/index.html   64 candidates, 128 PNGs

Two honesty rules are on the page itself, not buried in a comment: every
number states the window that produced it (margin/step/Z span), and a
2D slice CANNOT show connectivity -- two regions that look joined in one
slice may not be.

Plan-Z is chosen by highest solid/air boundary-transition count, not
hardcoded mid-height; a slab strate would otherwise render as a blank
sheet. Codex flagged the ambiguity in my spec and picked correctly.

PART A: bootstrap jitter for zero-spread fields. ±25% of a field's own
magnitude when corpus spread is ~0, ±15% of measured spread otherwise,
clamps last. Marked in code as a BOOTSTRAP for a starved corpus that
promotion (Tier 5) should retire. 1,422 applications over 168 fields in
the parameter roll; 6,848 over 107 in the structure roll.

⚠️ AND IT DID NOT BROADEN THE DISTRIBUTIONS. Feature scale 0-376 either
way; walkable actually NARROWED (.389 -> .287). Codex's conclusion, and I
agree: the measurement is INSENSITIVE to these fields, not merely
corpus-starved.

The reason is now obvious and worth writing down: the grid samples every
4 voxels, and cave DETAIL -- terraces, scallops, ribbing -- lives at 1-3
voxels. It is below the sampling resolution. A step-4 grid structurally
cannot see feel; it sees size. So "vary the detail" and "score the detail"
need different instruments, and the preview images inherit the same limit.

Box proofs stayed sound under wider jitter: parameter roll 1,204 proved /
1,602,524 voxels / 0 violations; structure roll 659 proved / 877,129 /
0 violations. §6.2 did not reopen.

fable-idea.md F1 ticked (measurement + preview).

Build clean, 25 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 17:35:07 +02:00
Fr0zkaandClaude Opus 5 f088c19f88 feat(composer): cave FEEL is rollable -- 23 of 34 fields were live all along
Jahni: "we need to be able to have everything tuned, so feeling can be
quite different." So the 34 excluded terrain-detail fields had to be
settled -- they are the difference between varying how BIG a cave is and
varying how it FEELS.

Settled by EXPERIMENT, not by reading, because the exclusion rested on the
same kind of structural claim that produced the withdrawn fuse assertion
and the "99.95% void" reading. Sample GetDensityAt over a lattice, change
ONLY those fields, re-sample, per operation group:

  LIVE (23 fields)  terrace, layer-line, ribbing, overhang, cliff,
                    scallop, arch, dome, pinch  -> now rolled
  DEAD (11 fields)  column, pit, chimney -> pre-baked into the chunk cache
                    from Definition->TerrainOperations, so the values in
                    FStrateGenerationParams are transport, not tunables

The call chain for the dead ones: BuildParamsFromDefinition returns only
the base params (VoxelStrateManager.cpp:1399); VoxelGenerator separately
reads Definition->TerrainOperations (VoxelGenerator.cpp:1099);
BuildChunkCache calls RoomOp->ApplyTo and pre-bakes pits/chimneys/columns
(VoxelCaveMorphology.cpp:1741).

⚠️ AND THE TRAP FIRED, exactly as specced against: these are native
unreflected fields, so REMOVING THEM FROM THE EXCLUSION LIST WOULD HAVE
ROLLED NOTHING. A knob that looks like it works and does not is worse than
an honest exclusion. An explicit jitter path was needed. This is why the
task was specced as an experiment with a liveness assertion (24 group/path
rows, 0 mismatches) rather than as a list edit.

⚠️ HONEST NEGATIVE: rolling detail did NOT materially broaden the measured
distributions. Feature scale 4-376 -> 0-376; walkable unchanged. All 17/17
TunnelNetwork/Underwater candidates activated live detail and every one of
the nine live groups appeared, so it is not that the fields are inert --
it is that most of them have ZERO CORPUS SPREAD, because the four authored
assets leave them at header defaults. Jitter is a fraction of measured
spread, and a fraction of zero is zero.

⇒ Real limitation, recorded not patched: "ranges come from corpus spread"
(§3.3) is right in principle and DEGENERATES on a homogeneous corpus. The
composer needs a bootstrap for zero-spread fields. Next.

Box verdicts with detail rolled: 1,228 proved, 1,634,468 voxels
brute-forced, 0 violations. Survival 59/64.

Recommendation recorded for later: roll terrain-operation parameter
STRUCTS directly into a transient candidate pool rather than corpus-ing
over assets -- there is exactly one such asset in the project, so an asset
corpus would starve like the strate corpus did.

Build clean, 25 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 16:30:57 +02:00
Fr0zkaandClaude Opus 5 859a9b25c1 feat(composer): the structure roll -- 64 distinct stacks nobody wrote
Tier 4b. Until now the composer varied PARAMETERS within an archetype:
differently-sized caves. This rolls STRUCTURE, which is what makes a strate
a different KIND of place.

Per COMPOSER-NOTES §3.2: roll root polarity (rock+carve or void+fill) ->
a shape source legal for that polarity -> the conversion FOLLOWS from
polarity rather than being rolled -> k modifiers drawn from what the
CHANNEL DECLARATIONS say is legal, not from a hand-written list ->
structural posts appended automatically and not omittable.

  64 distinct recipes of 64          (perfect structural diversity)
  invalid recipes emitted: 0         (the roller cannot produce an
                                      order the DAG validator rejects)
  survival 46/64 (71.9%)             non-vacuous 62, largest-share 60,
                                      law pass 48

⭐ AND THEY ARE REAL PLACES NOBODY DESIGNED. Root polarity as a one-bit
identity lever, exactly as §3.2 predicted:

  VF|r=vd/T|s=rg/T|x=fl/T   TunnelNetwork's ROOM GRAPH rooted in void with
                            a FILL -- rooms become solid blobs hanging in
                            open space. air .978, feature scale 64.
  VF|r=vd/M|s=lt/M|x=fl/M   Maze's lattice corridors filled instead of
                            carved. air .938, feature scale 16.
  RC|r=rk/V|s=sh/V|x=cv/V   shafts carved from rock with a Maze modifier
                            set they never ship with.

Same parts, different worlds. That is the whole thesis of the op stack,
finally exercised.

⭐⭐ AND THE VERDICTS HOLD ON STACKS NOBODY VERIFIED:
  489 proved (213 AllSolid, 276 AllAir), 2,071 mixed
  650,859 voxels brute-forced
  0 VIOLATIONS

This is what 8e346a3 was for. A false uniform verdict on an invented stack
means no geometry, no collision, and a player falls through the floor --
and it is the failure that cannot be seen without brute force. Zero.

Survival is 71.9% against 92.2% for the parameter roll, and that is the
EXPECTED direction, not a regression. Rolling structure aggressively is
supposed to fail sometimes; §3.5's reject-and-resample is what that is for.
A composer that only emits safe stacks is not exploring. Deliberately not
tuned.

Recipes are serialisable (op ids + param-block bindings), so a season
manifest can ship one -- which is also what will let an archetype become a
spatial REGION inside a strate rather than a label on it.

Build clean, 24 succeeded / 0 failed, verified by me in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 15:21:27 +02:00
Fr0zkaandClaude Opus 5 190ff381c0 feat(composer): it invents strates that work -- 59/64, and §6.2 is CLOSED
The corpus was starved: the loader followed the active settings pool and
found ONE vector, so all 64 candidates were bit-identical and every number
was a placeholder. It now loads every UVoxelStrateDefinition via the Asset
Registry (Saved/Autosaves and Saved/Cooked excluded -- including them would
silently weight the corpus toward whatever was autosaved last) plus the
per-archetype defaults from VoxelStrateTypes.h, which are themselves
hand-tuned known-good vectors. Blending stays WITHIN an archetype;
different archetypes read different subsets of the 73 fields.

⭐ THE COMPOSER WORKS. 64 candidates blended from real parents, every one
measured:

  survival 59/64 (92.2%)
  non-vacuous 60 · largest-share >= .50  60 · primordial law 63 · roll
  failures 0

and they are genuinely different worlds, not one world 64 times:
  air fraction   .0137 -> 1.000
  walkable       .000  -> .389
  feature scale  4     -> 376 voxels
  archetypes     TunnelNetwork, VerticalShafts, FlatPlain, CrystalChamber,
                 SurfaceWorld, Underwater, FloatingIslands, Maze

⭐⭐ §6.2 IS CLOSED, and it was the real risk. Box-verdict bounds were only
ever proved against the parameters Jahni typed; the whole worry was that
invented parameters would violate a bound nobody had checked -- silently,
leaving no geometry and no collision. Against ROLLED parameters:

  1,224 boxes proved (549 AllSolid, 675 AllAir), 1,336 Mixed
  1,629,144 voxels brute-forced
  0 VIOLATIONS

The bounds hold across the rolled range, not just at authored points. The
isolation work in 8e346a3 is what made that true.

The 5 rejections are a real finding, not noise: FloatingIslands can jitter
its islands out of existence and return an all-air strate (air 1.000,
walkable 0.000, feature scale 0). That is exactly what reject-and-resample
(§3.5) is for -- the measurement caught it without anyone anticipating it,
which is the whole argument for measuring rather than trusting.

Roll+measure 112.6 s for 64 candidates. §3.1's budget is "runs overnight",
so there is room for thousands.

Build clean, 23 succeeded / 0 warnings / 0 failed (the deliberate vacuity
warning is gone because the check is no longer vacuous). Verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 08:32:01 +02:00
Fr0zkaandClaude Opus 5 358bd415a9 feat(composer): the parameter roll -- infrastructure real, corpus starved
Tier 4a. The first piece of the composer that INVENTS rather than fixes.

The design, from COMPOSER-NOTES §3.3: do NOT roll 73 parameters
independently. The coherent region of a 73-dimensional space is a
vanishingly thin sheet and uniform sampling never lands on it -- and
independent rolling destroys the CORRELATIONS (room spacing <-> room
radius <-> tunnel length) that make a vector coherent. So blend known-good
vectors through the existing FStrateGenerationParams::Lerp instead:
constraint satisfaction is free, because a convex combination preserves
every relation holding in both parents. Min <= Max stays true because it
was never broken.

Built: FVoxelStrateCorpus (loads authored vectors, computes per-field
spread), VF_RollStrateParams (2-3 weighted parents, convex blend, jitter
by +-15% of each field's MEASURED corpus range -- not by the clamps, which
are "don't type nonsense" limits, not "this makes a good world" limits),
and VoxelForge.Composer.ParameterRoll. Determinism asserted by rolling
twice and comparing every field.

Exclusion list found by reflection, not assumption: StrateTopWorldZ and
StrateBottomWorldZ (manager-owned runtime bounds) plus 34 terrain-op
transport fields (TerraceStepHeight..PinchLength) that are copied in from
room operators rather than being strate tunables. Bools inherit from the
dominant parent; ints/enums use Lerp's existing snap.

⚠️ THE RESULT IS VACUOUS AND SAYS SO. 64/64 candidates "survived" -- but
the corpus loaded ONE vector (DA_Strate3), so all 64 are bit-identical.
100% survival of a single point is not evidence of anything. The box-
verdict check is vacuous too: 2,560 boxes, 0 proved, 0 voxels brute-forced,
so §6.2 is NOT closed. The test emits a warning saying exactly this rather
than reporting a clean pass, and no workaround was added to hide it.

Four strate assets exist (DA_Strate1..4); the loader found one. Fixing the
corpus is the next task -- with one parent there is nothing to blend, and
every number above is a placeholder.

Also owed: clamp metadata is editor-only, so the safety net needs a
cook-time baked table before this ships.

Nothing calls the composer from any runtime path (§3.1: offline, once per
season, on a build box).

Build clean, 23 tests / 0 failed (1 carries the deliberate vacuity
warning), verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 07:19:11 +02:00
Fr0zkaandClaude Opus 5 8e346a3054 feat(opstack): EffectOverBox correct in ISOLATION -- the blocker is closed
COMPOSER-NOTES §6.1, titled THE BLOCKER, gated everything after it. A
source writing only Sdf did not touch density, so it answered the box
query ON BEHALF OF the converter that followed it -- fine when a human
wrote and verified the pair, unsafe the moment the composer assembles a
combination nobody checked. And the failure is the silent one: a tile
wrongly proved uniform means no geometry, no collision, no error, and a
player falls through the floor.

FVoxelBoxHypotheses now carries FVoxelBoxSdfInterval. SDF writers publish
their own [Min, Max]; replacement writers replace it, accumulating writers
MinWith it; converters and detail ops consume the prior interval through a
state-aware EffectOverBox. Each op answers only for itself and the fold
composes them. Unknown, invalid, non-finite or unimplemented bounds force
Mixed -- when in doubt the answer is Mixed, because a missed skip costs
milliseconds and a false uniform costs a player.

Bounds are SUPREMA of what Eval can produce, never the param named "max":
FBM 1.5 (not 1.0), max(Min,Max) radius envelopes, warp sqrt(2), SmoothMin
slack. [FLT_MAX, FLT_MAX] is the exact no-surface identity.

⭐ THE PROOF IS THE NOVEL STACK. Built a combination no shipping builder
produces:
    ConstantVoid -> LatticeCorridorSource -> SdfRoughness -> SdfFill
brute-forced 332,750 voxels across 256 boxes: 250 proved AllAir,
0 VIOLATIONS. If isolation is real an unverified stack must still be
sound, and it is. That test -- not the refactor -- is the deliverable.

Every archetype re-proved by brute force, 0 violations everywhere:
  Maze 112/256 · CrystalChamber 38/60 · Underwater 33/40 · FlatPlain 32/60
  VerticalShafts 28/60 · FloatingIslands 15/60 · TunnelNetwork prod 9/40
  TunnelNetwork dense 0/40 (correctly -- it really is all mixed)

COST, reported not hidden: isolation gives up some skipping. TunnelNetwork
production 12->9, VerticalShafts 32->28. Answering for yourself alone is
strictly weaker than answering for a verified pair. The classifier stays
materially useful and the trade buys safe composition, which is the point.

Eval is untouched; equivalence tests bit-identical; no per-voxel box query.
SurfaceWorld keeps its separate exact height/lattice classifier.

Also: ⛔ WITHDREW the "corrective ops must be fused" claim from §3.2.
FFloorBiasMod does NOT undo FCaveRoughnessMod -- it adds an independent
NormZ^2 x FloorBias term to the ACCUMULATED density and never reads
TotalRough. Not a correction, no fuse, no "corrects" dependency. The claim
came from reading a name and a comment; one grep would have shown the ten
Density-writing ops in between before the paragraph was written.

Build clean, 22 succeeded / 0 failed, verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 06:20:40 +02:00
Fr0zkaandClaude Opus 5 7b982ad5ae feat(opstack): every op declares its channels -- ordering becomes derivable
The composer must assemble stacks nobody wrote by hand. Today the legal
order of operators lives in a human's head and in the hand-written
builders. COMPOSER-NOTES §3.2: an op's legal position is fixed by which
channel it READS and which it WRITES, so dependencies form a DAG and any
topological sort of that DAG is a legal stack.

EVoxelOpChannel names the two fields FVoxelOpSample actually carries
(Density, Sdf) -- read from the struct, not assumed. All 29 concrete
density ops declare reads, writes and additive/transformative, filled in
by reading each Eval rather than trusting its name. The declarations are
PURE VIRTUAL so a new op cannot silently omit them; the composer's safety
depends on every op answering.

A declaration nobody checks is a comment, so ValidateChannelOrder exists
and VoxelForge.OpStack.ChannelDAG runs all eight shipping stacks through
it. All pass: TunnelNetwork/Underwater 20 ops, Maze 8, VerticalShafts 9,
FloatingIslands 8, FlatPlain/CrystalChamber 6, SurfaceWorld 6.

Metadata is snapshotted in FVoxelOpStack::Add, so no declaration virtual
is ever called per voxel -- ARCHITECTURE §8.10 respected.

⚠️ THE FUSE WAS DELIBERATELY NOT APPLIED, and the reason is the finding I
asked for instead. §3.2 assumed FFloorBiasMod sits next to
FCaveRoughnessMod. It does not: TEN ops separate them (Terrace, LayerLine,
Ribbing, Overhang, Cliff, Scallop, Arch, RoomColumn, Dome, Pinch), and all
ten read and write Density. Moving FloorBias would reorder the live
density accumulation, and IEEE-754 addition is not bitwise commutative --
so the note's claim that fusing "leaves the field bit-identical" is false
as written.

That leaves a real open question for the composer, recorded not resolved:
channel rules alone say any Density->Density order is legal, but they
cannot express "this op CORRECTS that one". Either the fuse proceeds and
accepts a changed field (legitimate -- peer determinism never required
bit-identity across builds), or corrective intent needs its own
declaration. Settling it needs FFloorBiasMod's actual math read, which is
the next question I put to Codex.

Build clean, 22 succeeded / 0 failed, verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 04:30:45 +02:00
Fr0zkaandClaude Opus 5 4f11ce81a4 fix(shafts): the primordial law holds -- 0 trusted negatives across 16 seeds
Closes the VerticalShafts bug. Two remaining failures were investigated
and neither was a broken world.

PART A -- my measurement window was binding, not the world.
The window was a square centred on (0,0) with a radius, while mouths sit
up to ~200 voxels out in arbitrary directions, so a box holding both was
mostly empty space paid for at full price. Seed 6 at step 1 needed
16,515,072 cells and blew the cap; fitted to the two mouths it needs
6,553,344 and measures fine.

  seed  6  step 2  NOT_CONNECTED -> CONNECTED at doubled margin
  seed  6  step 1  NOT_CONNECTED -> CONNECTED at doubled margin
  seed 14  step 2  CONNECTED
  seed 14  step 1  CONNECTED

A fitted box can cut off a route that detours outside it, so every
negative is re-run at doubled margin. 5 of 16 were binding negatives and
all 5 flipped. TRUSTED NEGATIVES: 0. There is no evidence of a single
unescapable strate in 16 seeds.

PART B -- the drainage tree has no orphans, and a third cause existed.
  0 / 10,372 unreachable shaft paths (0% orphan rate)
  16 strict local minima, all caught by the direct-spine fallback
  neighbour fallback fired 0 times; emergency fallback 0
  91.8% of non-largest components are smaller than one shaft volume --
    they are roughness bubbles, not geometry

So a mouth could land in a noise bubble that is open but unreachable --
worse than useless, exactly the trap the law forbids.
VF_SuggestLandingPoint now returns the selected shaft AXIS, which is on
the drainage tree by construction.

⚠️ Codex found a third cause neither of us listed: tree capsules could
cross ledge bands, so a guaranteed connector could be blocked by a ledge.
Connector Z is now drawn from a deterministic ledge-free interval,
mirrored in the op stack.

⚠️ And an 8th error in my spec: "connect to any neighbour shaft" as a
fallback can create CYCLES -- a loop of shafts that drains nowhere and is
disconnected from the spine. The fallback must preserve strictly
decreasing origin-distance. Implemented that way.

  per-archetype law 6/6 · box-verdict violations 0 · stack equivalence
  bit-identical over 20,000 samples · sweep 45.3 -> 20.4 s
  cache 7.970 us rebuild, 0.073 us hot call (225-cell rolls on rebuild only)

Build clean, 21 succeeded / 0 failed, verified by me in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 03:57:30 +02:00
Fr0zkaandClaude Opus 5 9d2324879d feat(world): the XY edge seal -- the fourth structural invariant
VF_ApplyBoundarySeal was purely VERTICAL: it sealed each strate's ceiling
and floor and there was no horizontal equivalent, because until the depths
were bounded the world had no sides. The GDD now says "bounded, but
gigantic", so a player who walked far enough reached unterminated
generation running off into nothing.

Built as a FORCING op, appended automatically and not author-omittable, so
an invented strate cannot forget its own edge. Because a forcing op proves
AllSolid in its band, the outer shell also becomes free to skip in tile
classification -- correctness and a perf win from one change.

  order  spine -> vertical seal -> passage carving -> XY EDGE SEAL
         -> disturbances -> diff
  A near-rim passage may pierce a vertical seal; the XY seal runs after and
  wins. A passage must never punch a hole through the world's edge.

ClassifyBox/ForcedMarginOverBox proved via the closest AABB radius with a
one-voxel margin: 8 boxes proved, 1 tile proved, 600 voxels brute-forced,
0 violations. FXYEdgeSealOp is IsXYPure. Max rim discontinuity .187 against
a 1.0 threshold -- a ramp, not a cylinder wall.

⚠️ WorldRadiusVoxels DEFAULTS TO 0, and that is deliberate. Codex flagged
that DA_Settings and BP_VoxelSettings predate the property and so do not
serialize it -- they inherit the C++ default silently. A default of 8192
would have walled in every existing world on next editor open with nothing
in the asset to explain it. 0 is a bitwise no-op (45 interior comparisons
against the unbounded path prove it). The GDD's intended ~8192 is a value
to set in the asset, deliberately, once. Documented at the declaration.

Codex found four spec errors, three of them mine: VF_ApplyBoundarySeal
lives in VoxelDensityPrimitives.h not VoxelCaveMorphology; the asset
default trap above; and my "density increases through the band" wording
contradicts the MC sign convention (solid is more NEGATIVE) -- the test
measures increasing solidness.

Build clean, 21 succeeded / 0 failed, run and verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 01:17:12 +02:00
Fr0zkaandClaude Opus 5 44938f3f98 fix(measure): a blocked route is not a disconnected pair
VF_AreConnected took the shortest coarse BFS route, re-walked it at full
resolution, and on hitting a wall returned false and gave up -- never
looking for another way round. That is a false NEGATIVE, and it was
inflating the failure count of a bug we were actively trying to fix.

On refutation it now excludes that specific coarse step and re-searches,
up to a bounded budget (16). Outcomes are distinguished honestly:
  Connected                   a route survived full-resolution verification
  NotConnectedAtThisResolution no coarse route remains after exclusions
  CoarseLiedBudgetExhausted    UNKNOWN -- routes kept being refuted
The third is deliberately NOT folded into either other verdict. Collapsing
"unknown" into "disconnected" is precisely what misled us.

  arrival->departure   7/16 -> 14/16
  All 7 former COARSE_LIED became CONNECTED.
  arrival->spine   15/16 connected      departure->spine 15/16 connected
  per-archetype law: 6/6 applicable routes pass

Retries needed are small -- max 3 per query, 0 budget hits -- so 16 is
generous rather than load-bearing. Runtime slightly IMPROVED (45.3 -> 44.9 s
on the 16-seed sweep) because a satisfied query stops early.

The false-POSITIVE guard is untouched and is now stronger in meaning:
every Connected still requires a fully re-walked, air-verified route, and
it now says "this specific route is open" rather than "the first route I
tried was open". The controlled wall still returns BudgetExhausted after
exactly 16 retries, and a repeat query is bit-identical in verdict, snap
flags and retry count.

⚠️ REMAINING: seeds 6 and 14 are genuinely NOT_CONNECTED. Real or a
step-2 resolution limit is not yet settled -- refine before touching
generation again, per COMPOSER-NOTES §13.

No generation, placement or parameter changed. Verifier only.
Build clean, 20 succeeded / 0 warnings / 0 failed, confirmed in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-03 23:56:25 +02:00
Fr0zkaandClaude Opus 5 271bbdd034 docs(composer): record what measuring the world actually found
The notes described Tier 1 as done and had no record of today. Adds §13
(Tier 2 built, and the three artifacts that looked exactly like findings),
§14 (the VerticalShafts bug end to end -- three bugs wearing one coat),
§15 (working notes that cost something), and corrects the Tier 1/Tier 2
entries in §7.

The rule worth keeping, because it held three times out of three: when a
measurement reports something alarming, SUSPECT THE MEASUREMENT FIRST.
"FlatPlain is 99.95% void", "5 of 15 endpoints disconnected" and "7 of 16
seeds fail" were all method, not world.

Also recorded: the two guard directions are not symmetric (a false
positive drops a player through the floor; a false negative merely sends
you fixing healthy code), percolation is a phase transition and not a
knob, and Tier 1 shipped half built in a way only a connectivity
measurement could reveal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-03 15:21:41 +02:00
Fr0zkaandClaude Opus 5 9d5633ad54 fix(passages): aim the UPPER mouth too -- Tier 1 was only half built
VF_SuggestLandingPoint was called exactly once per passage, for the LOWER
mouth. The upper mouth fell out of the control-point chain with no landing
query at all: Tier 1 fixed where a player ARRIVES and never touched where
they DEPART. That predicted the measurement exactly -- departure->spine was
0 connected across all 16 seeds.

The ring proxy could never have caught this. It checks each mouth for open
space individually, and an unaimed mouth still has open space around it,
because the passage tube carved some. It took a connectivity measurement.

Each mouth now queries ITS OWN strate independently, which is what keeps
the layout a set rather than a sequence -- passage i never reads passage
i-1, so LayoutOrderIndependence stays green. Seed contracts kept unmixed;
SurfaceWorld still refuses; a declined query still preserves today's
placement rather than degrading it.

  departure->spine   0 -> 9 connected of 16
  arrival->departure 4 -> 7 of 16
  aiming coverage    upper 6/7 and lower 6/7 mouths now aimed at
                     generator-confirmed open space

⚠️ ACCEPTANCE STILL FAILS at 7/16, and the breakdown says why it is not
what it looks like:

  arrival->departure: CONNECTED 7, NOT_CONNECTED 2, COARSE_LIED 7

Only TWO seeds are genuinely disconnected. Seven are COARSE_LIED -- the
coarse BFS found a route and the full-resolution walk hit solid on it.
VF_AreConnected checks that ONE route and gives up, so a blocked candidate
is reported as failure even when another route exists. That is a false
NEGATIVE in the verifier, and it means this acceptance number probably
understates the world. Next task fixes the measurement, not the world.

Codex was killed mid-flight by a session teardown, so the edits arrived
unvalidated; I built and ran the suite myself rather than assume.
Build clean, 20 succeeded / 0 warnings / 0 failed, verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-03 15:14:25 +02:00
Fr0zkaandClaude Opus 5 7a20eeed0d feat(shafts): a drainage tree -- the field goes from 22% to 97% connected
The plateau was the tell. Roughness changed the component COUNT (it
manufactures noise pockets) but the giant component sat at ~0.22 in every
variant, and widening connectors past the proven roughness bound barely
moved it. That is a random graph below its percolation threshold: 0.6
sites, 0.35 bonds, and a 1.6-cell pair cutoff that drops many neighbour
pairs once jitter spreads them 1.7 (orthogonal) to 2.4 (diagonal) cells
apart.

So the lever was topology. Every shaft now emits a deterministic connector
to the nearest shaft STRICTLY closer to the origin, so chains drain inward
and terminate at the spine. The probabilistic connectors stay as texture
and loops -- the tree guarantees you can get out, the noise keeps it from
reading as a diagram.

Deliberately NOT tuned: raising CrossConnectChance/ShaftDensity/the cutoff
toward the percolation threshold buys a world connected at one seed and
shattered at the next, which is exactly the 1-in-16 behaviour we had. A
phase transition is not a knob.

  seed 1337   components 30 -> 17,  largest share .220 -> .975
  16 seeds    spine is the largest component in 16/16 (was 8/16)
              largest share .58 - .9999

Seam safety, because a parent computed from a 3x3 window would differ
between chunks and the connector would exist in one and not the other:
collect over 7x7, emit only for the inner 3x3, whose 5x5 candidate windows
are then fully contained. 154 seam probes per path, 0 mismatches.

Box verdicts re-checked against the new air: 27 proved, 33 mixed,
0 violations. Stack mirror bit-identical over 20,000 samples.
Perf: 1.634 us forced rebuild, 0.100 us hot call -- the 49 rolls happen
only on cache rebuild, never per voxel.

⚠️ ACCEPTANCE STILL FAILS: arrival->departure 4/16 (was 1/16). Reported,
not tuned. With 97% of the air in one component, two mouths that both land
IN the field would essentially always connect -- so the remaining failure
is placement, not topology. Next.

Build clean, 20 succeeded / 0 warnings / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 17:26:20 +02:00
Fr0zkaandClaude Opus 5 58e61cdefa feat(shafts): the spine joins its neighbours -- necessary, not sufficient
Jahni chose: make the (0,0) column join the shafts it passes, so
VerticalShafts behaves like the other seven archetypes. Built, and it
works as far as it goes -- the spine is now the LARGEST air component in
13 of 16 seeds (share up to 0.81, was 0.096 and never largest).

⚠️ IT DOES NOT MEET THE ACCEPTANCE CRITERION. arrival->departure is still
1/16. Reported as a failure rather than dressed up, because the criterion
was the measured law, not "the code was written". The test now prints
ACCEPTANCE arrival->departure: FAIL (1/16 seeds pass) so it cannot be
quietly forgotten.

Why it is insufficient, and it is structural: the spine only joins its own
3x3 cell neighbourhood, while passage mouths land 60-200 voxels from
(0,0). The departure network is unreachable from the spine in all 16
seeds. Joining the hub to its neighbours does not help if the rest of the
graph never reaches the hub.

Part A diagnosis (seed 1337, margin 0, step 2, radius 192):
  baseline              30 components, largest .220
  roughness 0           17 components, largest .226
  connector radius 8    29 components, largest .229

H1 is real but partial: roughness fragments the space (13 of the 30
components are noise pockets), yet widening connectors past the proven
roughness bound barely moves it. So roughness is NOT what severs the
network -- the largest component sits at ~0.22 in every variant, which is
the signature of a graph below its percolation threshold. The lever is
topology, not radius. H2 (the hardcoded 1.6 pair cutoff) went untested.

Implementation notes: the spine is added ONLY as a connector endpoint, not
as a carving shaft -- VF_ApplyOriginSpine remains sole owner -- and is
excluded from ledge placement. Spine connectors use radius 6.625, above
the proven roughness supremum of 5.625 (sup|FBM| = 1.5, not 1.0).
SpineRadius is threaded through BuildVerticalShaftStack, not hardcoded.

Op-stack mirror bit-identical over 20,000 samples, 0 box-verdict
violations. LayoutOrderIndependence green.

Build clean, 19 succeeded / 0 warnings / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 16:52:57 +02:00
Fr0zkaandClaude Opus 5 5519b323c8 test(measure): the spine is the backbone in 7 of 8 strates -- and not in
VerticalShafts

Diagnosis only. No generation or placement changed.

The hypothesis was that the (0,0) spine (OriginSpineRadius 14) is the
universal connector, so "can this mouth reach the spine" would be the
real primordial-law test. Measured at margin 0, step 2, radius 192:

  strate           spine share  largest?  arr->spine   dep->spine
  TunnelNetwork      0.979        YES     CONNECTED    not connected
  FlatPlain          1.000        YES     CONNECTED    CONNECTED
  CrystalChamber     1.000        YES     CONNECTED    CONNECTED
  Maze               0.997        YES     coarse-lied  CONNECTED
  SurfaceWorld       1.000        YES     CONNECTED    CONNECTED
  VerticalShafts     0.096        NO      not conn.    not connected
  FloatingIslands    1.000        YES     CONNECTED    CONNECTED
  Underwater         0.999        YES     CONNECTED    n/a

The spine forms one uninterrupted open interior run in 8/8 strates, and
holds essentially ALL the air in 7 of them. VerticalShafts is the lone
anomaly: the spine punches through as an isolated column holding 9.6% of
the air and is NOT the largest component.

So the archetype has three mutually unreachable things -- the spine, the
arrival network, the departure network. It has no lateral connectivity at
all. Over 16 seeds: spine open 16/16, spine largest only 8/16,
arrival->spine 1 connected, departure->spine 0 connected.

Neither extreme was right. The spine is not a universal connector, but it
is far from irrelevant: it is the de-facto backbone everywhere except
here, which reframes the bug as an ANOMALY to bring in line rather than a
missing feature to invent.

Ordering hazard confirmed on the record: making passage i depend on
passage i-1 would break LayoutOrderIndependence. Both candidate fixes
avoid it, because the spine is a fixed deterministic anchor that two
mouths can target independently.

Build clean, 19 succeeded / 0 warnings / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 15:10:00 +02:00
Fr0zkaandClaude Opus 5 0bbfdeb7a7 test(measure): VerticalShafts breaks the primordial law in 15 of 16 seeds
The margin is exonerated. Every earlier run excluded 8 voxels at each
strate boundary, and voxels 4-8 of those are ordinary interior, not seal
-- the same window mistake that produced "FlatPlain is 99.95% void" at
margin 32. So the query was re-run at margin 0, 2, 4 and 8. All four stay
negative; margin 0 includes the seal itself and still reports negative.
(The implementation had clamped explicit margins to >= 1, which made the
margin-0 check impossible; Codex found and fixed that.)

Diagnosis -- this is not a near-miss wall:
  arrival   LowerPoint (65.8,  95.8, -1047.25)
  departure UpperPoint (88.4, -104.6,  -991.58)
  XY separation 201.7 voxels (~50 m), Z delta 55.7
  arrival component   22,531 cells (22.0% of air)
  departure component 21,621 cells (21.1% of air)
  nearest approach between the two components: 135-179 voxels

Two large, healthy, entirely separate shaft networks half a strate apart.
Nothing to breach; the archetype has no lateral connection at all.

16-seed sweep: 1 pass, 15 fail. Not one unlucky world -- structural.

So: descend into a VerticalShafts strate and you probably cannot reach
its exit. This is the "live hole in the shipping game" COMPOSER-NOTES
Tier 1 predicted, now measured instead of assumed.

Stated precisely, because it names the fix: VF_SuggestLandingPoint makes
each mouth land in open space, but NOTHING makes the two mouths of one
strate land in the SAME open space.

New: VF_DiagnoseConnectivity returns component sizes and the spatial
separation between the two mouths' components, so a near-miss and a
never-meant-to-connect read differently instead of both being "false".

No generation change, no threshold tuning. The fix is a design decision.

Build clean, 19 succeeded / 0 warnings / 0 failed. Sweeps 3.4 s + 6.9 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 14:42:47 +02:00
Fr0zkaandClaude Opus 5 cd31dec869 test(measure): the VerticalShafts disconnection is real, not a coarse lie
Our guard was one-directional. FullResolutionPathIsAir catches the false
POSITIVE -- coarse says joined, reality says walled -- which is the
dangerous direction for safety, so we built it first. The opposite error
was unguarded: a corridor narrower than SampleStep falls between sample
points and vanishes, so connected regions read as separate. That is
exactly what a network of narrow vertical shafts would fake at step 4.

Refinement sweep, radius held constant for the controls:

  step 4 / r256  NOT_CONNECTED  57 comp  largest .119     458,752 cells
  step 4 / r192  NOT_CONNECTED  32 comp  largest .220     258,048
  step 2 / r192  NOT_CONNECTED  27 comp  largest .220   2,064,384
  step 4 / r128  NOT_CONNECTED  20 comp  largest .291     114,688
  step 1 / r128  NOT_CONNECTED  11 comp  largest .303   7,340,032

Both fixed-radius pairs stayed negative, so neither the step nor the
radius explains it. At one-voxel sampling the arrival and departure
mouths are still in different air components.

VerticalShafts is a live primordial-law violation: a player descends into
it and cannot reach its exit.

NotConnected is renamed NotConnectedAtThisResolution and documents its own
limit at the declaration, so a bare negative can never again be read as
proof. CoarseLied names one direction; this names the other.

Sweep split into VoxelForge.Generation.StrateConnectivityRefinement so the
fast test stays fast. 4.4 s. The MaxCells refusal path was exercised and
still refuses.

Build clean, 19 succeeded / 0 warnings / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 14:24:24 +02:00
Fr0zkaandClaude Opus 5 efcc8fcb65 fix(measure): ask the primordial law the right question
Three defects, two of them mine.

1. The anchor was a guess. LargestComponentAnchor() returned a hardcoded
   point near the (0,0) spine and the comment admitted it, so
   NOT_CONNECTED meant "not connected to a guessed point". The flood fill
   already knows the answer; it now returns LargestComponentPoint (tie-
   broken by lowest cell index, so it stays deterministic),
   LargestComponentCells and NumComponentsAtLeast1Pct.

2. "Connected to the largest component" is NOT the primordial law. The
   law is that a player must not descend into a place they cannot leave
   -- a question about MOUTHS. A strate can be 57 disconnected shafts and
   be fine, provided the shaft you arrive in is the shaft you leave from.
   The test now asks arrival -> departure per strate: the LowerPoint of
   the passage arriving, to the UpperPoint of the passage departing.

3. A false verdict and a true one were indistinguishable. VF_AreConnected
   returned false both when no route existed and when the endpoint's own
   coarse cell sampled solid -- and a passage mouth is a carved tube whose
   cell centre lands in adjacent rock easily. It now returns
   EVoxelConnectivityResult and snaps to an adjacent air cell first,
   reporting the repair.

RESULT: the earlier "5 of 15 endpoints disconnected" was an ARTIFACT of
the guessed anchor plus the wrong question. Corrected: 13 of 15 connected,
0 StartCellSolid, 0 GoalCellSolid.

One real finding survives. arrival -> departure passes for 5 of 6
applicable strates; VerticalShafts FAILS -- its arrival mouth is in the
largest component, its departure mouth is not. NOT yet reported as a bug:
a coarse grid can falsely SEPARATE as well as falsely join, and a
connecting corridor thinner than SampleStep vanishes. Next task settles
it at a finer step.

Also noted: GetPassages() contains a self-referential surface entry
(0 -> 0), excluded from the chain law.

Build clean, 18 succeeded / 0 warnings / 0 failed, verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 14:06:43 +02:00
Fr0zkaandClaude Opus 5 13399529b5 fix(measure): measure the strate, not the band above its floor
The window excluded a whole chunk (32 voxels) at each end to avoid a
4-voxel seal band -- 8x too much. On 4-chunk strates that discarded half
of every strate, floor included, so the pass was measuring the open air
above the ground and calling it the strate.

The margin now derives from the seal that actually exists:
ceil(2 x BoundarySealThickness), clamped to [1, height/4], overridable
via InteriorMarginVoxels. The fixture resolves to 8 voxels.

FlatPlain/CrystalChamber walkable fraction: 0.00044 -> 0.0578, ~132x.
The "99.95% empty void" reading was the window, not the world.

Metrics now carry ResolvedMarginVoxels, SampledMinZ and SampledMaxZ, and
the test prints them. A measurement that does not state the volume it
measured is exactly how the previous defect survived review.

VerticalShafts largest-component share rose only 0.074 -> 0.119 across
57 components: the window explained part of the swiss-cheese signal, not
all of it. That one is at least partly real.

Coarse-lie guard still load-bearing: 24 routes checked, 3 refuted at full
resolution (was 21 / 2).

Build clean, 18 succeeded / 0 warnings / 0 failed, confirmed in
Saved/Logs/VoxelM.log rather than from the agent's summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 13:46:28 +02:00
Fr0zkaandClaude Opus 5 6988e5ace1 feat(measure): one grid, one flood fill -- the strate measurement pass
Tier 2 slice 1 from COMPOSER-NOTES.md 3.4. A read-only, deterministic,
headless measurement of a built strate: sample a coarse grid once,
flood-fill the air once, derive every metric from that same grid.

What it answers today: air fraction, air component count and largest
component share (the swiss-cheese signal), walkable fraction, median
feature scale, median vertical clearance -- and whether two points are
actually connected.

The connectivity verdict is the point. Coarse sampling LIES about it in
the dangerous direction: a wall thinner than the sample step vanishes and
two sealed caverns read as joined. So VF_AreConnected recovers the actual
BFS route and re-walks that one corridor at one-voxel spacing; a true
verdict is never returned on coarse evidence alone. It fired: 2 of 21
routes were coarse false positives.

The test carries a negative control, because a green suite that cannot
fail is what hid the last defect. Two of them, in fact: a direct probe of
the known-solid inter-strate gap (AirFraction 0.00042), and -- Codex's
improvement on the spec -- a diff-layer box filled solid and measured
through VF_MeasureStrate itself, so an inverted sign is caught end to end
rather than around the function under test.

Build clean, 18 succeeded / 0 warnings / 0 failed.

KNOWN, fix in flight: the interior window excludes a whole chunk (32
voxels) at each end to avoid a 4-voxel seal band -- 8x too much. On the
fixture's 4-chunk strates that discards the floor, so the reported
FlatPlain/CrystalChamber air fractions of 0.999 describe the open band
above the floor, not the strate. The engine is right; the window is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
2026-08-30 13:20:42 +02:00
Fr0zkaandClaude Opus 5 8cd5face9b fix(strate): aim at the room's centre, not beside it
Commit 1ab8c0c reported Tier 1 working on the strength of a green suite and a
fixture reading 48/48. A million-seed sweep then showed the room query put the
landing point outside the selected room 92.795% of the time. Seven sampled
passages cannot see a systemic defect; the 48/48 was luck.

The cause was my instruction. I specced MaxLateralSnap = 0 for rooms and slabs
to "preserve existing placement", so the query found the nearest room and then
returned the CALLER's XY with that room's Z -- it located a room and aimed
beside it. Precisely the defect I had identified for Maze, VerticalShafts and
FloatingIslands, then exempted rooms from.

The function already computed the room's XY and threw it away. It now returns
it. A room's centre is inside its own room by construction, so this needs no
SDF evaluation and no warp handling -- and the centre also holds the largest
margin against the production warp, which displaces the field about 1.5 voxels:
decisive at a room's edge, irrelevant at its centre. Simpler and more robust
than the warped-SDF inside-proof I had been asking for.

Caves get a real RoomSpacing budget, because a room is an XY-sparse target just
like a maze corridor. Slabs correctly keep zero: their void band is
XY-continuous, so the requested XY is already inside it.

Room/slab rings go 38/48 to 32/32. But TunnelNetwork and Underwater now decline
entirely at the fixture seed -- with RoomDensity below 1 the nearest room is
often past one spacing, so they fall back to random reach. Honest, not landing.
Roughly 2x spacing would restore them at the cost of passages drifting from the
(0,0) spine, which is what makes them findable. Left untuned deliberately;
raising a number until coverage looks better is the move I twice told Codex not
to make.

One guard softened: the test asserted all four room/slab archetypes must answer
within seven sampled passages. That asserts a property of the sample, not the
code, and it failed on this fix, since a supported query is allowed to decline
on budget. Now reported rather than asserted. Vacuity, per-mouth ring and
footing checks, endpoint matching and the SurfaceWorld refusal all remain hard
failures.

Built clean; 17 succeeded, 0 warnings, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-30 00:52:22 +02:00
Fr0zkaandClaude Opus 5 1ab8c0cae4 feat(strate): passages aim at open space -- for half the archetypes, measurably
Tier 1. GeneratePassages placed every passage at a random angle, distance and
reach, never consulting the destination strate. The carved tube is air by
construction, so nothing looked wrong -- but whether the lower mouth joined the
destination's connected cave space was luck. A player could descend and arrive
in a sealed pocket. The primordial law was a hope.

VF_SuggestOpenPointZ is a pure free function of (archetype, params, seed, XY):
no operator stack, no StrateManager, no cache. That is deliberate. This runs
inside Initialize, where the layout is half-built and LayoutVersion is in
flight, so anything holding a manager pointer there is a re-entrancy trap.

Coverage is four archetypes of eight, and the number is the deliverable:
7 passages, 3 aimed at generator-confirmed open space with 48/48 ring samples
air, 4 falling back. The hole is not closed. It is measured, where this morning
it was invisible.

Two corrections from Codex, both mine and both would have shipped confident
nonsense: room placement is seeded by MakeStrateSeed, not the passage salt, so
my version would have queried a cave graph that does not exist; and slabs need
the world seed because that is what GetSlabDensity uses.

And one of mine caught in review: the first test sampled GetDensityAt at the
mouth and asserted air, which is true by construction because the passage carve
is a structural-post invariant. It could not fail. Replaced with a 16-point
ring outside the carve radius requiring 8/16 air -- a proxy for connectivity,
not a proof. The proof is a flood fill, which is Tier 2.

Logging: my first spec asked for a per-passage warning on fallback. It fired
572 times and turned 14 clean tests yellow, which is precisely the anti-pattern
OPSTACK-HANDOFF.md warns about -- a warning that always means "this is fine"
teaches people to ignore warnings. Now one sorted Log line per layout build.

Built clean, suite green: 17 succeeded, 0 with warnings, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-29 17:45:05 +02:00
Fr0zkaandClaude Opus 5 10a8bbc677 feat(strate): the layout is a set, not a sequence -- and the suite is green
Tier 0. Reordering Settings->StratePool in the editor used to change every
world in the game, because Initialize Fisher-Yates shuffled the array in
editor order. The pool is now sorted by stable soft-asset path before the
shuffle, so the layout is a pure function of (seed, pool contents as a SET).
The selection algorithm itself is untouched -- same Fisher-Yates, same cycling,
same FixedStrates handling, same Z ranges. Sorting by pointer would have looked
right and been load-order dependent, which is the trap here.

GeneratePassages walked one sequential FRandomStream across every strate, so
changing an early connection count shifted every later passage. Each draw is
now a salted VoxelHash of (seed, boundary index, connection index), with the
distributions preserved exactly -- ToFloat01 returns [0,1), so Lerp(Min,Max,t)
reproduces FRandRange precisely.

Codex implemented it and caught a real flaw in my spec: I asked for a test that
permutes the pool and asserts the passages match, but once the layout is
order-independent that test cannot fail whether or not the passage fix works --
the layout comes out identical, so even the old sequential stream would agree.
It added the perturbation that actually bites: change an early connection count
and verify every later boundary keeps its own values. It also contradicted me
on passage type being randomly drawn. It isn't, and it preserved that.

Built VoxelMEditor clean, then ran the suite headless: 16 succeeded, 0 failed,
0 not run. That also closes the thread OPSTACK-HANDOFF.md left open -- 4d33321
and 91585ea were built but never re-verified, and both acceptance conditions
are now met: violations 0 everywhere (12 of 40 TunnelNetwork tiles proved over
15972 brute-forced voxels), all equivalences green. No revert needed.

Expected: every seed now lays out differently than before. Accepted,
pre-content-lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-29 16:30:47 +02:00
Fr0zkaandClaude Opus 5 a60138b19c docs(composer): split the transcript -- notes become generator-only
CLAUDE.md says each doc has one job and they must not duplicate. After GDD.md
landed, COMPOSER-NOTES.md and it both held the death loop, the city tiers and
the economy -- and duplicated documents drift until neither is authoritative.

So the notes are now the engineering document and nothing else: the vision and
its correction, what the game *requires* of the generator (constraints only,
pointing at the GDD for why), the composer itself, the manifest, naming, every
code finding and hazard, the build order, and the corrections nobody should
re-derive. 1561 lines down to 527, with the game design replaced by pointers.

Two sections earn their place on their own. Section 6 is the verified code
findings, including the one that blocks free composition (EffectOverBox answers
for the source+carve pair, so an op's box verdict is only correct in the stack
it shipped in) and the primordial law's live hole (GeneratePassages never
consults the destination strate's layout, so a passage can open into a sealed
pocket). Section 8 is the list of corrections that cost real time: Phase 3 is
reopened, Jahni does not want to author strates either, peer determinism never
required matching the old system.

The full 1561-line transcript remains in git at b781e3b for the reasoning
behind anything here that reads as bald assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-29 15:51:35 +02:00
Fr0zkaandClaude Opus 5 b781e3bab2 docs: a game design document, distilled from the transcript
COMPOSER-NOTES.md is a conversation: chronological, with my wrong turns kept
on purpose so nobody re-derives them. That is the right shape for us reading it
back and the wrong shape for anyone else, including Codex.

GDD.md is the other half -- the decided state without the journey. Seventeen
sections covering the premise, the core loop, the city, the depths, a strate,
descending, the economy, death, housing, the verbs, the season, the community
layer and the opening. Every section separates what Jahni decided from what is
still open, and design that is mine rather than his says so, so nothing reads
as settled that isn't.

Written deliberately plainer than the notes: prose rather than dense tables,
because a design document that only its authors can read has failed at the one
thing it exists for.

Also records why the file is in the wrong folder. This is a document about the
game, and most of it -- the city, combat, the economy, seasons -- is nothing to
do with VoxelForge. It lives in the plugin because the plugin is the only part
of the project under version control. It should move to the project root once
that becomes a repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-29 15:44:35 +02:00
Fr0zkaandClaude Opus 5 b7cf12af4c docs(composer): the depths regenerate, the city does not -- game design session
Second design session. Started on the composer, ended up with the whole game's
economic loop, and one terminology fix that was overdue.

The loop, closed: the surface is a sky island or an unclimbable mountain, so
resources must come from below; the depths supply them; the resources build the
settlement; the settlement enables deeper descent. The settlement is communal,
identical for everyone, and permanent -- a season advances it one tier
(camp, village, town, city). So the wipe means something beyond freshness: the
world is impermanent, the civilization is not. We lost our maps, but we built a
town.

I had that arc backwards and it is marked as such in the file, along with the
vocabulary that caused it: "the world" was being used for two different things.
The depths are what VoxelForge generates and what regenerates each season; the
city is a plain Unreal level, permanent, and entirely outside this plugin. That
distinction is now pinned at the top of the notes.

Also settled: no bottom, boss strates every ~5 as intermediary gates, gear stats
capped around depth 30 with deeper loot buying better rolls and unique materials
rather than bigger numbers, materials tagged by difficulty tier rather than
depth so their location is rediscovered every season, and death leaving a
possessed corpse that copies your weapons without taking them -- a mirror match
that self-balances at every tier forever, provided the copied gear is never
lootable.

Two design records worth keeping: the manifest is two things (a tiny stored
spine and an infinite hash-addressable field, where the ID encodes the position
so resolving it needs no table), and the opening cinematic -- civilization
emerged from the depths, humans crystallized in the first artificial strate --
justifies the authored/generated seam in fiction. A player who senses strate 1
feels different from strate 2 is reading the story correctly, not spotting the
join.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-28 11:56:23 +02:00
Fr0zkaandClaude Opus 5 302cf16681 fix(world): the voxel field is in ACTOR space -- make every entry point agree
The city's hole is not at world (0,0), so the AVoxelWorld actor has to move.
It couldn't: the plugin was only HALF actor-relative, and had been for a long
time without symptoms because the actor always sat at the origin.

Already correct: GetBiomeAtWorldLocation, GetVoxelSurfaceHeightAt,
UpdateDecorations, UpdateLandmarks, UpdateWater, UVoxelDensityVolume::Update,
the passage debug draw, the clipmap material.

Not correct: the streaming centre, the streaming anchors, all six carve/fill
entry points, GetStrateAtPosition, and UVoxelAtmosphereManager::UpdateForPlayer
-- whose own comment documented the assumption out loud ("actor at
origin/identity"). Carving would have landed offset; the strate lookup would
have been quietly wrong by the actor's Z.

The trap worth recording: because the content managers already convert
internally, converting PlayerLastPos once at the source would have DOUBLE-
converted four call sites. The fix has to be per-boundary. That was only
visible after reading each consumer -- the estimate before that read was wrong
in both directions.

WorldToLocalCm / WorldToLocalVoxel / LocalVoxelToWorld are now declared as the
only sanctioned boundary between Unreal coordinates and voxel coordinates, and
the real deliverable is the invariant next to them: no `/ VOXEL_SIZE` applied
to a parameter named `World*` outside those three functions. A grep can check
that. The risk was never a missed site among the ones found -- it is the next
one nobody notices.

Also adds a translation-only guard in BeginPlay. Tiles, ClassifyBox verdicts,
the clipmap window, culling and streaming distance all assume world-axis-
aligned boxes; a rotation or non-unit scale breaks that structurally rather
than gracefully, so it logs an Error naming what was set.

With the actor at the origin every conversion is identity, so this is a no-op
until the actor actually moves. Built and confirmed working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-23 17:47:23 +02:00
Fr0zkaandClaude Opus 5 dfa1d5f5b1 test(strate): the X-macro guard -- a warning in prose does not fail CI
FStrateGenerationParams::Lerp blends the hand-written VF_STRATE_PARAM_FIELDS
list, not the struct. Add a field to one and not the other and it compiles,
tests green, and silently takes its default inside every Gradient/Interleaved
transition band -- terrain that changes shape at the boundary and nowhere else.
CODEMAP has warned about this in prose for a month.

The test expands the X-macro a third way (after LERP and SNAP), into a list of
names, and diffs it against the struct's UObject reflection. No fixture, no
world -- a shape test, instant. GExemptFieldNames is empty on purpose: every
reflected field is covered today, so an exemption has to be written down as a
decision rather than happening by omission.

The stakes rise with the world composer, which intends to invent parameter sets
through this same Lerp: a missing field would then be constant across every
invented strate, with nothing to notice it by.

(Its CODEMAP §3.12 row lands in the following commit, which also touches that
file for an unrelated reason.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-23 17:47:07 +02:00
Fr0zkaandClaude Opus 5 eae7e68caf docs(composer): the composer runs once per season, offline -- so it can take all night
Second half of the design session. The realisation that resets the whole cost
budget: everyone shares one seed per season, so there is exactly one world per
season and the composer never runs on a player's machine. It runs once, on a
build box. Not "fast enough for a loading screen" -- "runs overnight". It can
generate a thousand candidates, keep eight, and let Jahni veto the season before
it publishes. What ships is the seed plus the chosen parameter vectors.

The measurement pass itself is one coarse grid and one flood fill, producing one
feature vector that serves validation, danger and rarity at once. Noted the one
place coarse sampling can lie -- a wall thinner than the sample spacing makes
sealed spaces look connected, which would pass the primordial-law check on an
impassable strate. Coarse to find the route, full resolution to confirm it.

And the answer to "how do we ever judge what's good": the judge is the corpus,
not a model. Good never gets defined, only collected -- strate-level thumbs
up/down, player behaviour, and best of all where players choose to snapshot
their home, which is an ungameable "I love this place" that costs nothing to
collect. Known risk written down: promote only what you already liked and the
system converges on a comfortable middle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-23 13:05:59 +02:00
Fr0zkaandClaude Opus 5 d8f7dabee8 docs(composer): the system should invent worlds -- design session notes
A long design conversation, written down as it happened. The headline: the
op stack's "Phase 3" is REOPENED. Asked what the world was missing, Jahni
described exactly it -- the system assembling worlds from data assets and a
seed -- so the 2026-08-16 handoff's "do NOT start Phase 3" is superseded.

Then a correction that moved the whole thing a level deeper: he does not want
to author strates either. The system must INVENT them. He authors raw content
(operators, meshes, materials, sounds, creatures) and tags it; everything above
that is composed and measured.

Decided along the way: worlds are wiped every 6-12 months, which dissolves the
"generation is frozen forever once public" problem -- generation is frozen only
within a season. Shared player builds are blueprint structures, vote-ranked.
Housing is an instanced level built from a snapshot of land the player chose.
Danger is depth x a per-strate multiplier, not a sort. Every strate has a
findable entrance and exit -- built first, generated around.

The range problem turned out cheap: don't roll 73 parameters independently
(that is what makes mush), blend known-good ones with FStrateGenerationParams::Lerp,
which already exists and already blends the whole field set. Ranges come from
the spread of the existing corpus, not from authored numbers. Verified against
the code: the X-macro is in sync with the struct, only Alpha differs.

Recurring mechanism, arrived at three separate times: measure, don't declare --
and promote what measures well back into the corpus, so the system's vocabulary
grows from its own history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
2026-08-23 12:57:04 +02:00
Fr0zkaandClaude Opus 5 451ebcb776 docs: close the operator stack -- handoff rewritten in plain language
Jahni built the last two commits and the game looks fine. Closing the thread.

OPSTACK-HANDOFF.md fully rewritten. The old one had become a private dialect --
"you've started using very unique terminology, i have NO idea what you've been
doing" -- which is a failure of the document, not the reader. The new version
leads with what the op stack was for, says plainly that Phase 3 was never built
and is not being built, carries the honest three-week ledger (120 commits, every
feat: a port, the world unchanged by design), and translates the jargon.

Final state: 8/8 archetypes live and bit-identical, 14/14 tests green through
871ca19, violations 0, and 39% of tiles skipped in the running game against 0%
that morning.

Flagged in the handoff: 4d33321 and 91585ea are built but not re-verified -- the
suite has not been re-run since. Two named checks, and revert 4d33321 if either
fails; the 39% does not depend on it.

Direction agreed: no Phase 3, no further refactoring, old switch stays as the
oracle, next work is visible content from fable-idea.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 03:23:31 +02:00
Fr0zkaandClaude Opus 5 91585ea493 test(tunnel): a warning that always means "this is fine" is noise -- demote it
Jahni asked whether a "warning stage" he had been seeing for several sessions
meant something was broken. It did not: the only Warning in that run is the
tunnel test's "[dense fixture] No tile was proved", whose own text explains that
0 proved is the EXPECTED and correct result there -- cull spheres cover the
dense fixture ~3.6x, so no box can be outside all of them. Test passes, density
bit-identical, all coverage non-zero, violations 0, and production defaults
reports 11 proved / 14641 voxels / 0 violations right below it.

But a warning that fires every run and always means "fine" trains the reader to
ignore warnings, and this one quietly worried him across several sessions.

RunTileScan now takes bZeroProvedIsExpected: AddInfo on the dense fixture where
zero is the only possible answer, AddWarning on production defaults where zero
would be a real regression from 11. Same message, severity now carries meaning.

Same defect as the lumped bail counter one layer out: a signal that always says
the same thing measures nothing.

Test-only. Not built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 03:20:07 +02:00
Fr0zka 4d33321bfa feat(classify): fold cave boundary categories safely 2026-08-17 01:35:58 +02:00
Fr0zkaandClaude Opus 5 2303442d8f docs: T1.d fires in production -- 39% of tiles skipped, measured in game
Classified 1.88 / Meshed 1.15 / SkippedAllAir 0.73 (1.15 + 0.73 = 1.88, exact).
Was Classified 1.90 / Meshed 1.90 / nothing skipped. Cave Bail No Layout went
1.58 -> gone.

39% of classified tiles now skipped against 0% this morning, each avoiding
~43000 density evaluations plus marching cubes. First observation of
tile-skipping in the running game rather than an automation harness.

Not yet firing, stated plainly: TilesSkippedAllSolid and the two op-stack
counters remain absent, so the cave branch still produces no verdicts in game --
its attempts land in Cave Bail Mixed Content (0.77) and Stack Verdict (0.32).
The win here is the open-air class proved by the classifier's own Z categories,
not by ClassifyBox. That is the next thread and the counters now name it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:23:41 +02:00
Fr0zkaandClaude Opus 5 871ca190af fix(classify): use the public GetStrateChunkZBounds; the index accessor is protected
VoxelGenerator.cpp(2796): C2248 -- FindSlotIndexForChunkZ is protected.

I claimed it was public "verified" in the previous commit. The grep printed the
declaration line, not the access specifier above it. Same shape as the partial
read that produced a false accusation earlier today.

GetStrateChunkZBounds is public (VoxelStrateManager.h:177), returns false for
exactly the no-slot case, and ClassifyTile already calls it three times for the
same question. Wrapped in a VF_ChunkZHasSlot lambda so the condition reads as
the predicate it is. No accessor promoted, no API widened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:22:12 +02:00
Fr0zkaandClaude Opus 5 a2c5e02713 fix(classify): out-of-layout chunks are constant AIR, not TunnelNetwork -- unblocks T1.d
Measured in game: Cave Bail Not Op Stack No Layout = 1.58 of 1.90 classified
tiles (83%), with SoleSlot and BoundaryTile never firing. The assets were
correctly ticked all along.

Cause: GetGeneratorTypeForChunk returns TunnelNetwork for any chunk outside the
strate stack, commenting "the fallback density path will produce solid rock
anyway". That comment is FALSE. GetGenerationParams for SlotIdx < 0 returns
BaseDensity = -1, RoomDensity = 0, WormStrength = 0 -- a CONSTANT AIR field.
IsGapChunk's "open air, NOT a gap" was the correct description.

So every tile touching the open air above the world looked like a cave
archetype, entered the cave branch, found no layout slot, and bailed. T1.d was
never failing -- it was unreachable, behind a routing mistake in the archetype
lookup that had nothing to do with the operator stack, the box verdicts, or the
flags.

Fix: ClassifyTile gains a fourth Z category for no-slot chunks. It sets
bAnyNonCave (never enters the cave branch) and bCanSolid = false (AllAir
survives), and bails to Mixed if a disturbance could add rock there -- chasms
only carve, so they cannot threaten an air verdict.

A tile entirely above the stack now resolves AllAir and is skipped: the
majority of a surface flight, a class T1.d has never been able to prove.

Classifier only -- no density value changes. Equivalences must stay
bit-identical and violations must stay 0.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:20:06 +02:00
Fr0zkaandClaude Opus 5 05986bd875 docs: T1.d blocker FOUND -- out-of-layout chunks misroute into the cave branch
In-game with Sol's four-way split: NotOpStackNoLayout = 1.58 of 1.90 classified
(83%), while SoleSlot, BoundaryTile and MixedContent never fire. So the assets
ARE ticked -- Jahni was right -- and the neighbour theory is dead. The split
earned itself on its first flight; the old lumped counter would have sent us at
the assets again.

Mechanism: GetGeneratorTypeForChunk returns TunnelNetwork for any chunk outside
the strate stack ("fallback produces solid rock anyway"), while IsGapChunk
returns false for above-stack chunks ("open air, NOT a gap"). So every chunk
above TopChunkZ = 0 -- all the open air over the world -- looks to ClassifyTile
like a cave archetype, enters the cave branch, finds no layout slot, and bails.

T1.d was never failing. It was unreachable, behind a routing mistake in the
archetype lookup that has nothing to do with the operator stack, the box
verdicts, or the flags -- all of which are correct and tested.

Not fixed: the two comments disagree about whether out-of-layout is solid rock
or open air, and the right verdict depends on which. Read GetDensityAt's
no-slot path before choosing; guessing there writes a hole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:16:12 +02:00
Fr0zkaandClaude Opus 5 4ae9e6e72f fix(strate): opt-in diagnostic goes Verbose under automation, not Log
The verbosity fix stopped the diagnostic failing the suite, but it still printed
at Log on every Initialize -- which the fixture calls dozens of times per run,
burying the actual test output under hundreds of identical lines.

Under GIsAutomationTesting it now logs at Verbose (off by default, still
available with -LogCmds). Editor and game keep Warning, where it is actionable
and fires once per rebuild.

The information was worthless in tests anyway: the fixture's opt-in state is
known by construction, and the one test that needs the opted-in variant
(OpStack.ClassifyTileSoundness) already reports "all 7 cave layout slots have
Use Operator Stack enabled" on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:09:22 +02:00
Fr0zkaandClaude Opus 5 b06c39c077 docs: suite green after the verbosity fix; FIELD digest proves nothing moved
14/14 pass with no StrateManager warnings. Every reported number is identical
to the pre-change run, including the CrossPlatform FIELD digest
(0xF62F3D355B1C0BDB) -- a bit-for-bit hash over 115,000 samples of the whole
field.

That digest is the strongest evidence available: the six-box LRU, the
DensityCacheOwnerId key, the bail re-attribution and the scope fix did not move
a single voxel between them. Four changes to caching, keying and diagnostics,
and the world is byte-for-byte identical -- exactly the contract each claimed,
now tested rather than asserted.

Still unmeasured: the performance benefit of those caching changes. "Quite a
fraction of what it used to be" is a real observation, not a same-seed/
same-route number.

Still open: read `Operator-stack opt-in` in the GAME log. That line is the
ground truth for T1.d and supersedes both the bail-counter inference and the
recorded belief that the assets have the switch on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:04:46 +02:00
Fr0zkaandClaude Opus 5 9740d117e0 fix(strate): opt-in diagnostic must not fail the automation suite
Sol's opt-in warning logs at Warning. UE's automation framework counts a
Warning as a failure, and the Determinism.* tests deliberately build a
NON-opted-in world -- that world is their comparison oracle, the baseline every
OpStack.* number is measured against. The diagnostic was failing the suite by
correctly reporting an intentional configuration.

Verbosity is now Log under GIsAutomationTesting and Warning otherwise. The
message is written once and only the verbosity branches (Printf then log %s):
four copies of a long format string across two sites would have been exactly
the "one definition, not two kept in sync" failure this project has rules
about. CoreGlobals.h included explicitly for GIsAutomationTesting -- IWYU, no
shared PCH here.

Note for the record: the tests must NOT be switched to the op stack. The
non-opted-in fixture is the oracle, not an oversight.

The diagnostic also did its job: 7/7 cave slots report disabled in the fixture
(slot 4 absent = the SurfaceWorld narrowing working). The same line read from
the GAME log is now the ground truth for whether T1.d has ever been enabled in
production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 01:01:55 +02:00
Fr0zkaandClaude Opus 5 45c61dd00e fix(opstack): hoist the acquired column box out of the integer-XY scope
VoxelDensityOpStack.cpp(832): C2065 'Box' undeclared.

The six-box LRU refactor moved acquisition to `FColumnBox& Box = Cache.Acquire(...)`
inside `if (bIntegerXY)`, but the Computed flag is set after the column is
computed, outside that block. The previous single-box version had Box at
function scope so the write-back compiled.

Hoists `FColumnBox* AcquiredBox` beside MemoColumn and writes back through it.
The guard becomes `if (AcquiredBox)` instead of `if (bIntegerXY)`: non-null
implies the integer path, so the pointer proves its own safety rather than
relying on two conditions staying in agreement.

No caching or logic change. Other Box. uses (761-763) are in scope, verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:50:34 +02:00
Fr0zka 6e7ea7038a docs(opstack): record unbuilt experimental landing
Append the exact commits, Approach A memory decision, cave-only warning caveat, diagnostic semantics, CP owner scope, compile watchpoints, deliberately open measurements, and recoverable worktree cleanup. No build or runtime result is claimed.
2026-08-17 00:44:27 +02:00
Fr0zka f90c4e56c3 fix(generator): key CP cache by world owner
Function-static thread_local CP state previously trusted only chunk coordinates and each manager's locally-reused layout version, allowing a worker to carry params, biome context, CP_UseOpStack, and the built stack into another world. Allocate each generator a monotonic owner ID and add one uint64 comparison to the hot key. This intentionally fixes only the proved CP_* path; other TLS caches remain outside this change.
2026-08-17 00:41:51 +02:00
Fr0zka 1e02c6314f diagnostics(opstack): separate disabled-slot bail causes
The old Not Op Stack counter fired before slot attribution, so interior disabled strates and boundary encounters produced the same number. Split the instrumentation into sole-slot, boundary-tile, unresolved-layout, and late-recheck counters while preserving every ClassifyTile condition, return point, and verdict.
2026-08-17 00:39:47 +02:00
Fr0zka 8295f6e76b perf(opstack): retain six surface column regions
The single direct-indexed box discarded all 6,561 computed columns whenever spatial or column identity moved. Port the reference six-box LRU so interleaved regions keep five warm working sets, accepting the documented ~0.79 MiB TLS cost per worker to preserve the recommended and measurable A/B path. Also report disabled cave opt-ins at layout initialization; SurfaceWorld is excluded because its exact-lattice tile proof does not use that flag.
2026-08-17 00:37:56 +02:00
Fr0zkaandClaude Opus 5 588f9e0294 docs: Sol investigation -- refutes my boundary-tile hypothesis, corrects my false accusation
Sol High investigated with Luna xHigh sub-agents; two approaches built in
isolated worktrees, main tree untouched.

Two corrections to my own assertions:

1. "One un-ticked neighbouring strate kills boundary tiles" is WRONG.
   UsesOperatorStackForChunk depends only on ChunkCoord.Z, and ClassifyTile's
   earlier Z loop already required the predicate for every sampled cave Z within
   one slot -- so the later XYZ sweep is redundant, not over-strict. Removing it
   unlocks nothing; boundary tiles still fail the one-slot/type/params guards,
   correctly.

2. I falsely wrote that VF-03's fixture citation was fabricated. It exists
   (VoxelForgeTestFixture.h ~134/146, explicit about CP_UseOpStack
   contamination). I had read only the 30-line header and asserted a negative
   from a partial read. Header corrected. VF-03's core claim is now CONFIRMED:
   GetDensityAt keys thread_local CP_* by (ChunkCoord, LayoutVersion) with no
   world identity, and every manager's version starts equal.

Also: the Cave Bail Not Op Stack counter I specced is ambiguous -- it fires
before the different-slot attribution, so it cannot distinguish "flown slot
un-ticked" from "boundary tile met an un-ticked slot first". My 80% reading was
over-confident. Same defect I fixed for TilesSkippedAllSolid, reintroduced one
layer down.

Approach A recommended (explicit opt-in + six-box LRU) over B (code-enforced
cutover): the code does not justify weakening ClassifyTile, and B would delete
the flag, which is the only same-route A/B lever available to prove the
refactor. Narrow A's warning to cave archetypes before adopting.

Nothing built. Worktrees left in place for review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:22:06 +02:00
Fr0zkaandClaude Opus 5 4ba53f2ede docs: T1.d is blocked by CONFIG not code (Cave Bail Not Op Stack = 80%); retract memo claim
Part B paid for itself on its first flight. Over the caves, Tiles Classified
== Tiles Meshed (nothing skipped at all) and CaveBailNotOpStack accounts for
~80% of cave-branch attempts. That counter has exactly one cause:
UsesOperatorStackForChunk returned false -- so either the strate flown over has
not ticked bUseOperatorStack, or a chunk in the tile's box belongs to an
adjacent strate that has not, since the per-chunk sweep requires every chunk to
agree. CaveBailStackVerdict being non-zero (0.32) corroborates: where the flag
did hold for the whole box, the stack built fine and merely returned Mixed.
The machinery works; it is mostly not being reached.

RETRACTION: I called the column-memo port a regression after comparing 14.7%
from one flight against 23.3% from another. Invalid -- the miss rate is
strongly location-dependent (~10-30%) and was in that band before the port.
Comparing single-flight averages from different routes measures the route. A
real before/after needs the same seed and route; parked until then, and not
urgent.

Kept while diagnosing: GSurfColCache is a 6-box LRU, not one box. My spec said
"a direct-indexed box" and Codex implemented one faithfully, so any recentre
now wipes all 6561 cells where the old table lost one entry. Plausible thrash
source, unmeasured. Port the LRU if the memo is revisited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:36:40 +02:00
Fr0zkaandClaude Opus 5 3ad2720d0a perf(opstack): direct-indexed column box (measured 14.7% miss); + T1.d bail attribution
BUILD RESULTS FIRST: 14/14 tests green, 0 violations. VerticalShafts box
verdicts went 0 -> 30 of 60 (zero was the number for the project's whole life).
TunnelNetwork production held at 11 proved / 14641 voxels, byte-identical --
that line was the isolated diagnostic for cab8e8f, so its stillness proves
Jahni's room/tunnel Min <= Max are correctly ordered. All eight equivalence
tests bit-identical, which TESTS the "bounds only, cannot reach density" claim
made on 7dbdf51 / eaa44bf / cab8e8f rather than asserting it.

PART A -- the column memo thrash is confirmed quantitatively. Measured 14.74%
miss rate against a prediction of 14.0% if thrashing and 1.4% if not; and 8,300
recomputes per tile against a healthy 1,225 (6.8x) from an independent
statistic. Replaced the 4096-slot direct-mapped hashed table with the
direct-indexed box scheme GSurfColCache has always used: no hash, no
collisions, every column computed exactly once. ParamsFingerprint is retained
in the box key -- its absence was the shipped bug that silently deleted the
overhang, and GSurfColCache's own omission of it was deliberately not copied.

PART B -- T1.d never fires in game: the two op-stack counters never appeared,
and since every displayed row has Min 1.00 rather than 0.00, rows only render
when a counter fires. The cave branch has 13 return-Mixed paths; guessing which
is the mistake this project keeps paying for. Six attribution counters now name
the bail category outright. Only edits there are brace-expansions so a counter
fits before each existing return -- every condition and returned value is
byte-identical.

Open falsifiable hypothesis: Tiles Meshed averages 2.13/frame, plausibly the
reason LOD rings update slowly. If misses drop ~10x and LODs speed up, the memo
was the ceiling; if not, they are separate problems, cleanly separated.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:28:46 +02:00
Fr0zkaandClaude Opus 5 49a9959aed fix(world): drain workers before mutating strate layout/passages (VF-01)
Initialize does StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and
reallocating both -- with no guard, while mesher workers read them through
AnyPassageNearBox, EvaluateModifierSDF and FindSlotIndexForChunkZ. The epoch is
bumped AFTER, so previous-epoch workers are live during the mutation; the epoch
rejects a finished result, it cannot make a read of a freed allocation safe.
Same class as the DiffLayer.ChunkMods carve-vs-stream AV that ModsLock fixed.

Chose the drain over the two alternatives:
  - FRWLock on the arrays: correct, exact in-repo precedent, but a read lock on
    the per-voxel hot path (~43k/tile) would contaminate the perf measurement
    CODEX-TASK-001/002 are queued to take. Worst possible timing.
  - Immutable generation snapshot (Sol's proposal): right long-term, refactors
    the whole StrateManager API surface. A design conversation, not an agent's
    unprompted call.
  - Drain: zero hot-path cost, reuses the machinery EndPlay already proved, and
    its stall lands only on human-initiated editor actions, never during play.

FScopedGenerationPause raises a new bGenerationPaused (deliberately NOT
bShuttingDown, which means teardown) and drains both reader populations: chunk
tasks via ActiveTaskCount and decoration tasks via a new
WaitForDecorationTasks, whose refactor also removed NotifyShutdown's duplicate
spin loop. On timeout it does NOT mutate -- logs an Error and leaves the world
consistent. A timeout that proceeds is what the bug already does.

RebuildStrates, OnObjectModifiedInEditor and ChangeSeed cannot reach Initialize
without the pause. BeginPlay untouched (no tasks yet). EndPlay untouched --
VF-02's 3s timeout is a separate deliberate decision.

Verified: four files, no density/mesher file, so terrain cannot move.
ShouldAbortWork replaced three existing checks, adding none. No worker path
waits on the game thread, so the drain is bounded.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:51:19 +02:00
Fr0zkaandClaude Opus 5 bc0bf83c64 docs: VF-01 confirmed (worker race on rebuild) + VF-10 triaged; hold further code
VF-01 CONFIRMED and it is the most serious find of the day. Initialize does
StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and reallocating both
-- with no lock, barrier or drain anywhere in that file, while worker threads
read the same arrays through AnyPassageNearBox (:460), EvaluateModifierSDF and
FindSlotIndexForChunkZ, reached from GetDensityAt/ClassifyTile. RegenerateAllChunks
bumps the epoch AFTER the mutation, so previous-epoch workers are live during it;
the epoch rejects a finished result, it cannot make a read of a freed allocation
safe. Same class already fixed once here: DiffLayer.ChunkMods got ModsLock after
a carve-vs-stream AV. The dangerous call site is OnObjectModifiedInEditor, which
fires automatically on a strate asset edit while streaming -- routine here.

VF-10 confirmed real (74 fields, per near-surface sample) but Sol missed the
conclusion: the copy is INHERITED from GetDensityWithParams, so both paths pay it
and it does NOT explain the op-stack regression. The op stack improved it by
memoising across eleven detail ops. Future optimisation, not the answer.

Deliberately NOT fixing VF-01 now. Four code changes are stacked unbuilt and
three have "nothing should move" as their acceptance signal; a fifth change to
streaming lifecycle would make an odd build result unattributable. The three
fix options (drain / snapshot / RWLock) trade a possible crash for an editor
hitch or hot-path lock traffic -- a product decision, not an agent's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:38:09 +02:00
Fr0zkaandClaude Opus 5 cab8e8fe55 fix(morphology): BuildChunkCache under-bounds room/tunnel collection when Min > Max
Third instance of the same class, and the worst one -- found by the Sol-High
read-only audit (VF-05) and verified against the code before acting.

MaxInfluence, RoomZBuffer and EvaluateSDF's Margin all derive from
MaxRoomRadius / TunnelMaxRadius, while the radii are Lerp(Min, Max, hash), which
yields up to max(Min, Max). A room able to reach a chunk can therefore sit in a
cell the collect region never visited.

Worse than the two fixed earlier today because:
  - it is TunnelNetwork, the largest archetype;
  - BuildChunkCache is called by BOTH density paths (FRoomGraphSource calls it
    rather than transcribing it), so this was never an op-stack bug -- it is in
    the shipped original code and always has been;
  - the failure mode is a window-invariance break (ARCHITECTURE 8.4): whether a
    room exists depends on which chunk you queried from, which in multiplayer
    means two peers generate different geometry from the same seed.

Four bound sites now use RoomRadiusEnvelope / TunnelRadiusEnvelope; the two
duplicated copies of the formula still compute the identical expression. No
Lerp, placement, hash or bStore line changed -- with correctly ordered params
max(Min,Max) == Max, so this is bit-identical. A no-op at correct values is the
acceptance signal.

Also adds a reviewer's header to AUDIT-2026-08-CODEX.md marking which findings I
verified (VF-05 confirmed, VF-02 premise confirmed, VF-03 evidence overstated --
its cited fixture corroboration does not exist) and which are unverified leads.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:32:09 +02:00
Fr0zkaandClaude Opus 5 8ef4d7dc09 docs: CORRECTION -- bUseOperatorStack is ON in the game's data assets
The handoff said "No strate asset has the box ticked -- that is my call and I
still haven't made it." Stale, and I reasoned from it all session. Jahni: "the
data assets in game have the switch on."

Everything downstream flips. The operator stack is the PRODUCTION density path,
so the measured perf regression is one players feel, and any unsound
EffectOverBox is a live hole rather than a latent one. The two correctness fixes
committed today (7dbdf51, eaa44bf) both say "nothing in the running game was
affected" in their bodies -- that sentence is wrong. Those commits are pushed
and stay as written; OPSTACK-PROGRESS 2026-08-16 (h) is the correction of record.

Concrete consequence: 7dbdf51 did not fail in practice only because this
Perlin's empirical sup (~1.0-1.1) happened to stay under the margin the wrong
bound left (1.27 for shafts, 1.40 for maze) -- not because the code was right.
And eaa44bf is worth checking against the real assets: if any strate has
ColumnMinRadius > ColumnMaxRadius or ShaftMinRadius > ShaftMaxRadius, it WAS
producing tiles with no geometry and no collision.

Process failure worth keeping: the flag lives in .uasset binary data, which is
not greppable from here, so the docs carried a hand-written claim about it that
nobody re-checked. A fact that lives outside the repo cannot be maintained
inside it -- record what it was and when it was checked, never assert it as
current. Rule added to the handoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:12:01 +02:00
Fr0zkaandClaude Opus 5 21f62c4a70 docs(opstack): two comments still described a half-finished port that is finished
Both said the refactor was mid-flight and both contradicted the code around them.

1. BuildTunnelNetworkStack: "stage A of three, the twelve detail modifiers and
   the per-room override are not ported yet, which is why
   UsesOperatorStackForChunk returns false for TunnelNetwork." All three stages
   are done, the twelve modifiers are added a dozen lines below the comment, and
   that function returns true.

2. FIslandBlobSource: "two of the three frame users are not ported yet; revisit
   when TunnelNetwork brings the second real use." TunnelNetwork is ported, and
   BuildTunnelNetworkStack already records the verdict: CaveWarp wraps exactly
   one operator and VerticalScale is a pure scalar function, so ZERO frames out
   of three candidates. The question was answered; only this comment still asked
   it, and it would have sent the next reader off to build frame infrastructure
   that was explicitly decided against.

A comment that contradicts the code beneath it is the "read the code, not the
comment" trap in reverse. This file already carries the forward version of that
lesson, at the cliff modifier whose comment promises a gradient it never samples.

Comments only -- no code changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:06:24 +02:00
Fr0zkaandClaude Opus 5 f737488d88 test(opstack): three tile scans sampled <1 lattice period; fix the CLASS this time
The "a sampler must cover at least one period" bug was found in the tunnel test
(+/-32 vs RoomSpacing 80) and again in the shaft test (+/-48 vs ShaftSpacing 55).
Both were fixed. The class was not: three box-verdict scans still ran the
original RandRange(-6,6)*Extent at Step 1 / Cells 8, i.e. +/-48 voxels.

  Island  +/-48 vs IslandSpacing 95 = 0.51 periods  (worse than either fix)
  Slab    +/-48 vs ColumnSpacing 60 = 0.80 periods
  Maze    +/-48 vs CellSize      40 = 1.20 periods

Verified the fixture does not override any of those spacings, so the header
defaults are what these tests really ran against.

This matters now specifically: 7dbdf51 changed the island and maze verdicts and
eaa44bf changed the slab column verdict, and these are the tests that guard
them. Widening before the build is what makes that build's green mean anything.

The rule the fixed tests already encoded, now explicit: Extent = 8, so
SpanCells = the lattice spacing gives exactly 8 periods -- which is where the
shaft test's 55 came from. Island 95, Slab 60, Maze 40, each printing the ratio
computed LIVE from the params struct so a future narrowing cannot be silent.

No assertion, tolerance, AddError, brute-force loop, seed, Step, Cells or tile
count was touched: this changes what the measurement looks at, never what it
demands. Proved counts will move (expected); NumUnsound must stay 0.

Test-only -- git diff lists three files under Private/Tests/. Not built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:04:55 +02:00
Fr0zkaandClaude Opus 5 c993e6e877 docs(opstack): handoff heads with the four-things-on-one-build table
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:50:56 +02:00
Fr0zkaandClaude Opus 5 eaa44bf0c0 fix(opstack): two cell-sweep pads assumed MinRadius <= MaxRadius; a third already didn't
Second defect from the full 28-op EffectOverBox audit. Three ops roll a radius
as Lerp(MinRadius, MaxRadius, hash01), which lands in [min(A,B), max(A,B)] --
Lerp does not require A <= B. Their EffectOverBox sweeps lattice cells padded by
the largest radius a cell could hold, so a pad below the true maximum means the
cells are never examined and the op reports Identity for a box its own Eval will
fill or carve: no geometry, no collision.

  FGridColumnMod   ~1086  Max(MaxRadius, 0)                        exposed
  FShaftFieldSource ~1436 Max(ShaftMaxRadius, ConnectorRadius)     exposed
  FIslandBlobSource ~1803 Max(IslandMinRadius, IslandMaxRadius)    already correct

The third one is the argument: the concern was met and guarded once in this same
file, and the other two shipped without it. Fixed with FMath::Max3, a spelling
already used here (~2481) and in VoxelCaveMorphology.cpp.

Shipped defaults are ordered correctly (2/5, 2/7, 5/11), so this is a NO-OP at
defaults -- that is its acceptance signal, and any moved number means the diff
did more than intended. It needs a mis-ordered asset value, which nothing
prevents: ClampMin is a per-property floor and UE cannot express "<= that other
property". ColumnMinRadius is also settable per-room via UVoxelTerrainOpDefinition.

Deliberately NOT fixed by normalising the params: swapping the Lerp endpoints
maps the same hash to a different radius, changing geometry and breaking the
eight equivalence tests. Only the bound becomes conservative; Eval is byte-
identical -- verified, no Eval/Roll/GetCells line in the diff.

Also audited and found sound (recorded so they are not re-chased): FPassageCarveOp
and the passage bounding sphere, FOriginSpineOp, FBoundarySealOp, FShaftLedgeMod,
FCaveArchMod, FDomeMod.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:50:01 +02:00
Fr0zkaandClaude Opus 5 108982d135 docs(opstack): the proved-bound rule now names VF_PerlinAbsBound and the FBM normalisation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:36:45 +02:00