Commit Graph
263 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 7dbdf51b44 fix(opstack): three ExtraReach box verdicts used sup|FBM| = 1.0; the proved bound is 1.5
CORRECTNESS on a box verdict, not perf. This file derives |Perlin3D| <= 1.5
rigorously, exposes it as PerlinAbsBound, uses it for the tunnel warp, and says
outright that the header's "~[-1,1]" is an observation and that a box verdict
resting on one is a hole. Three ExtraReach formulas in the same file assumed
sup|FBM| <= 1.0 -- and VoxelNoise::FBM normalises (Total / MaxValue), so
sup|FBM| = sup|Perlin3D| exactly. The bound was wrong by 1.5x.

At shipped defaults, soundness needed B <= 1.27 (VerticalShafts, Rough 3.0) and
B <= 1.40 (Maze, Rough 2.0); both are violated at B = 1.5. Break-even roughness
is 1.6. FloatingIslands survives only because its SDFBlendRadius is 5 -- sound
by parameter luck, not construction.

Nothing in the running game was affected (no strate has bUseOperatorStack
ticked) and the empirical Perlin sup ~1.0-1.1 is why nothing surfaced. But the
tile scans SAMPLE, and for shafts they sampled a source that proved zero tiles
until e002bd4 -- which is what makes this reachable rather than latent.

Fix: PerlinAbsBound hoisted to file scope as VF_PerlinAbsBound (one definition,
not a class-static plus three implicit 1.0s) and multiplied into all three
reaches; the three comments now state the real justification instead of the
refuted one.

Cannot change density: ExtraReach is read only inside EffectOverBox, verified
mechanically -- no Eval/GetCells line appears in the diff. Direction is strictly
conservative, so it can only cost CPU. Expect FEWER proved tiles for Maze and
VerticalShafts; that is the correct outcome, not a regression.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:36:04 +02:00
Fr0zkaandClaude Opus 5 eb317d9933 feat(stats): stat VoxelForge -- make tile skipping and the column memo observable
The plugin had ZERO stat counters, so every claim this refactor makes was
harness-only: "skipped correctly" and "skipped nothing" render identically.
CODEX-TASK-001 and -002, executed by Codex (gpt-5.6-luna xhigh), reviewed
against the source.

Eight DWORD counters in a new stat group:
  GenerateTileResult -- TilesClassified / SkippedAllSolid / SkippedAllAir / Meshed
  ClassifyTile bAnyCave exit -- TilesOpStackSolid / TilesOpStackAir
  FSurfaceColumnSource::GetColumn -- ColumnMemoHit / ColumnMemoMiss

The op-stack counters are separate from the lumped skip counters on purpose:
ClassifyTile also proves AllSolid on its hand-written bedrock-gap path with no
strate opted in, so the lumped number cannot show a before/after. The op-stack
pair is zero BY CONSTRUCTION until a strate ticks the flag.

GetColumn is instrumented and deliberately NOT fixed -- the instrument and the
fix in one build would make each other unreadable.

Verified against UE_5.7 Stats.h rather than assumed (first stats use in this
plugin, no in-repo precedent): all four macro arities, and INC_DWORD_STAT ->
FThreadStats::AddMessage, so the worker-thread safety both sites need holds by
mechanism. Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:01:26 +02:00
Fr0zkaandClaude Opus 5 e002bd4e2e feat(opstack): VerticalShafts' connector branch never tested a connector -- test the real capsules
Jahni's call: bank T1.d at 11/40 and take VerticalShafts rather than squeeze the
Perlin sup. Right call -- a clearly-scoped defect with no hole-risk maths, on an
archetype that was getting nothing.

The defect is stated in its own comment. FShaftFieldSource::EffectOverBox ended
with a "connectors" branch that never looks at a connector: it returns CarveOnly
because a shaft EXISTS within Spacing*1.6 + Pad. At ShaftSpacing 55 and
ShaftDensity 0.6 that box spans ~4x4 cells and ~10 shafts, so the condition is
true essentially everywhere -- exactly the reported 0 proved of 60. Conservative,
never wrong, completely sterile; the same shape as the worm's unconditional
CarveOnly one archetype over.

It now rebuilds the connectors the way GetCells does and tests the real capsule.
Two things had to be right and both were read in the source rather than assumed:

- The enumeration is a superset. Eval reads connectors from the 3x3 of ITS
  query's cell, so any pair visible from a point in the box has both shafts in
  the 3x3 of some cell the box touches, i.e. in [box cells] +- 1 -- the range
  swept here. Pairs no query ever sees may be produced: extra CarveOnly, never a
  hole.
- The pair order matches, so the hash matches. VoxelHash::Pair is fed in
  insertion order and GetCells inserts over (dy, dx), row-major; this sweeps
  (cy, cx), and row-major order restricted to a sub-grid preserves the relative
  order of two cells. So the same pair gets the same hash WITHOUT assuming
  Pair() is symmetric -- which was never verified and now need not be.

The capsule test splits the axes because a connector is HORIZONTAL at height Zc:
Z is exact, XY is point-to-segment from the box centre minus the XY half
diagonal. Tighter than the 3-D half-diagonal the tunnel version had to settle for.

Also the same sampler trap, caught before the build this time: tile XY was drawn
from +-48 voxels against ShaftSpacing 55 -- under one period of the pattern,
identical in kind to the +-32-vs-80 bug that cost the tunnel test three runs.
Widened to +-440, and the report prints its own extent in units of ShaftSpacing.

The safety net was already there and is untouched: this test brute-forces the
full lattice of every proved tile, both hypotheses, and AddErrors on the first
violation. If the superset or hash-order argument is wrong, the assertion fails
rather than a player falling through the floor.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:36:34 +02:00
Fr0zkaandClaude Opus 5 b2938d38f1 fix(opstack): the warp bound was the dominant term all along -- 2.0 -> 1.5, and MEASURE it
Jahni asked whether I was in a loop. I was, and re-reading the original found it
in one line.

PerlinAbsBound was set to 2.0 in the first commit of the spatial EffectOverBox
and never revisited. The dilation is CaveWarpStrength * VOXEL_NOISE_SCALE *
PerlinAbsBound, and CaveWarpStrength is 8.0 by default:

  8 * 1.25 * 2.0 = 20 voxels, applied +/- on every axis
  tile 10 voxels -> query box 50 per side -> 125x the tile volume

So the tunnel test I called "an order of magnitude tighter" actually needed
DistToAxis >= 78 against a cull rejecting at ~107. 27% tighter, not 10x -- and
the run said so: tunnels reaching went 78.0 -> 58.5, a 25% cut. The instrument
was right and I credited the tunnels.

Four rounds -- worm, columns, sampler, tunnel disjunction -- each individually
correct, every one of them tightening around a term nobody had measured. I kept
instrumenting what I had just changed and never instrumented what I had assumed.

What re-reading showed: BuildChunkCache is called everywhere with
Expansion = CaveWarpStrength + 2.0f, which is only correct if |Perlin3D| <= 0.8.
The whole plugin has always bet on 0.8. I picked 2.0 -- 2.5x more conservative
than the assumption the cache's own correctness already rests on.

The corrected bound is derived, not guessed. GradDot returns +-u +-v with u and
v two DISTINCT components (checked on all four hash branches). Splitting the
eight corners by i gives, per axis, sum w*|dx| = (1-su)*fx + su*(1-fx) <= 0.5
(max at fx = 0.5). Hence |Perlin3D| <= S_x + S_y + S_z <= 1.5, with no case
analysis on the hashes. Dilation 20 -> 15.

And the instrument that should have existed from the start: EffectOverBox now
also runs every room/tunnel test with the warp dilation set to ZERO and reports
both, so Hit* - Hit*NoWarp is exactly the blocking caused by my box rather than
by geometry. The report says, in the output, that if that gap dominates the next
move is the warp bound and not the primitives.

Separately: the diagnostics had turned into narrative, printing hardcoded numbers
from previous runs next to live ones ("32 of 34 tiles vs 21 for rooms" while the
live figures said 21 of 28). Unreadable, and I wrote all of it. Diagnostics now
report THIS run; the history stays in OPSTACK-PROGRESS.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 03:21:37 +02:00
Fr0zkaandClaude Opus 5 9733179723 feat(opstack): tunnels get a threshold test -- T1.d is measured at 6 of 40 tiles, 0 violations
THE PRIZE LANDED. At production defaults: 6 of 40 tiles proved AllSolid, 7986
voxels brute-forced, 0 violations. 15% of tiles skip GenerateMesh entirely, one
BuildChunkCache traded against 30000+ density evaluations each, and not one
verdict is asserted -- every voxel was re-evaluated and came back on the claimed
side. The dense fixture still reports 0, exactly as the arithmetic said it must,
so this is a measurement of production rather than of a widened test.

The breakdown finally isolated the tunnels: of 34 unproved tiles, 32 were
blocked by a tunnel and 21 by a room, so >=13 were tunnel-only. That is what
justified the deferred disjunction, and why it was deferred rather than guessed.

A primitive no longer matters if it misses its cull OR its own SDF stays >= T+K
over the box. Each branch wins on a different class, by calculation: for rooms
the cull (Rmax+3K) is tighter than the threshold (Rmax+T+K), so rooms keep the
cull alone; for tunnels the cull is a capsule BOUNDING SPHERE, radius ~107 for a
200-long tube of radius 7, against a true segment distance. Order of magnitude.

The bound is exact, not cautious. TaperedCapsule was read, not assumed --
Dist(P, ClosestOnSegment) - Lerp(Ra,Rb,t) -- so SDF >= dist(P,segment) -
max(Ra,Rb); and dist(box,segment) >= dist(centre,segment) - half-diagonal by
triangle inequality. VF_DistPointSegment is written locally rather than taken
from FMath: five lines, and "I think that function does that" is not good enough
under a correctness bound.

Identity CHANGED MEANING, from "Sdf stays FLT_MAX" to "Sdf >= T" with
T = max(3*SDFBlendRadius, WormNetworkRange). Sound only because all three
consumers of the SDF channel were read one by one: FSdfConvertOp's Blend is
MakeSdfCarve(P.SDFBlendRadius, ...) => K; the twelve modifiers gate at 3K;
FWormFieldSource at WormNetworkRange. Plus the one that could have bitten --
FCaveTerraceMod re-probes the SDF at Z+-1, OUTSIDE the box, but its gate is line
13 and the probes are lines 28-29, so a gate false everywhere never emits one.
ANY NEW CONSUMER OF THE Sdf CHANNEL MUST HAVE A THRESHOLD <= T OR JOIN THAT MAX,
or it gets tiles with no geometry and no collision. Written at the site.

Why -K suffices for any N: SmoothMin's penalty is exactly zero once |A-B| >= K,
so the running minimum saturates at K below the smallest term and cannot descend
further. Sdf >= min_i(SDF_i) - K for ANY number of primitives, not - N*K/6 --
without which the slack would scale with the ~88 tunnels and be worthless.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:45:56 +02:00
Fr0zkaandClaude Opus 5 87a0b996ec test(opstack): measure the box verdict on BOTH densities -- the fixture cannot prove a tile
The widened sampler worked: 39 of 40 tiles now land clear of the (0,0) spine.
The verdict did not move -- rooms still hit 40 of 40. By my own pre-registered
rule that means the room-lattice model was wrong, and it was, for a reason the
report had been printing at me for three runs: "= 7.6 x RoomSpacing 42".

RoomSpacing is 42 here, not the 80 I did the arithmetic with. I read the
UPROPERTY default out of the header instead of the fixture that overrides it.
Fifth premise this refactor that reversed on being checked, and the plainest:
the number was on screen.

EnableTunnelFeatures densifies on purpose -- RoomSpacing 80 -> 42, RoomDensity
0.35 -> 0.85 -- because at the defaults only 1.1% of samples were in open cave
and the equivalence compared solid rock to solid rock. That densification is what
makes check 1 mean anything, and it is exactly antagonistic to provability:

  room cull radius = max(R*1.5, R*HeightRatio) + 3*SDFBlendRadius
                   = 1.5R + 12  for R in [10,30]  =>  27..57, mean ~42
  lattice spacing  = 42 at 85% occupancy

The mean cull radius equals the lattice spacing, so cull spheres cover that world
~3.6x over and NO box can be outside all of them. "6.3 of 8.3 rooms reach" is not
a loose test, it is a saturated world. No sampler change and no tunnel tightening
can move it -- which is why widening the sampler correctly changed nothing.

So 0 proved on this fixture is the RIGHT answer, and informative: the prize
shrinks to nothing as caves saturate. It is simply not an answer about
production.

Check 4 is now a lambda run twice -- parameterised rather than copy-pasted,
because two copies of the criterion would drift and the second would lie:
[dense fixture] as before, expected to stay ~0 and now documented as correct;
[production defaults] the same 40 tiles against RoomSpacing 80 / RoomDensity
0.35, the real UVoxelStrateDefinition defaults. Both brute-forced voxel by voxel.
Not "widen it until it passes": the dense run stays in the report, must stay ~0,
and a false verdict in either still fails. The two stacks deliberately share
FRoomGraphSource's thread_local caches, so this run and check 3 now watch each
other through the params fingerprint in the key.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:37:45 +02:00
Fr0zkaandClaude Opus 5 f2fefade4c test(opstack): the tile sampler never left the (0,0) spine -- widen it to 4x RoomSpacing
The per-class breakdown refuted my own hypothesis on its first run. I predicted
"tunnels >> rooms, so it is capsule bounding spheres". Tunnels ARE wildly
over-counted (69.2 of 80.4 reach), but rooms hit all 40 tiles too, so fixing
tunnels alone would have moved the number by exactly zero. Fourth confident
chain this refactor that reversed on contact with a measurement.

The real finding is that the measurement was invalid. The sampler drew tile XY
from RandRange(-4,4)*8, i.e. +/-32 voxels, against RoomSpacing = 80 and a
guaranteed OriginRoomRadius = 20 room at (0,0) whose cull radius is 42, with the
(0,0) spine descending exactly there. All 40 tiles sat inside the origin room's
cull sphere, in the most cave-riddled spot in the world. "4.9 of 7.2 rooms
reach" was measuring the spine hub, not the world's cave density -- every
conclusion about whether deep rock is provable was answering another question.

Widened to +/-320 voxels (4x RoomSpacing). The report now prints its own
sampling extent and how many tiles landed clear of the spine, because a sampler
whose extent you cannot quote is one nobody is watching. This changes what the
measurement LOOKS AT, never what it demands: every verdict is still brute-forced
voxel by voxel, so a wider sampler that produced a false verdict still fails.

Also worked out, before writing any code, why rooms cannot be tightened and
tunnels can. SmoothMin's penalty is exactly zero once |A-B| >= K, so the running
minimum saturates at K below the true minimum and Sdf >= min_i(SDF_i) - K for
ANY number of primitives. With K=4, WormNetworkRange=24, mods gating at 3K=12,
the threshold T is 24. For rooms the cull rejects at Rmax+3K=57 while an
"Sdf >= T+K" test rejects only at Rmax+T+K=73 -- the cull is strictly better.
For tunnels the cull is a capsule BOUNDING SPHERE, radius up to ~107 for a
200-long tube of radius 7, while the real segment distance rejects at 35. An
order of magnitude, and sound because TaperedCapsule is a genuine distance
function (verified, not assumed).

That disjunction is NOT in this build on purpose: it changes what Identity means
here, from "Sdf stays FLT_MAX" to "Sdf >= T", which is only sound if every
consumer threshold is <= T. Three premises still need reading rather than
assuming -- VF_NearCaveSurface's constant, the Blend passed to FSdfConvertOp,
and whether any modifier re-probes the SDF outside the box before its gate.
Fix the measurement first; it costs nothing and it is wrong today.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:25:43 +02:00