Commit Graph
143 Commits
Author SHA1 Message Date
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 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
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 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 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 f537648867 fix(opstack): the worm inherits the room source's box verdict -- the actual blocker
First build of the spatial EffectOverBox came back green with 0 tiles proved of
40. The warning written for exactly that case fired, and then was not good
enough: it offered two candidate causes and BOTH were wrong. The real one was a
third operator neither candidate mentioned.

FWormFieldSource answered CarveOnly unconditionally with a provably correct
amplitude bound of WormStrength. But BaseDensity = 8 and WormStrength = 10 are
the DEFAULTS -- the field's own comment requires the worm to exceed BaseDensity
or it could never carve air. So SolidMargin = 8 - 10 < 0 on every tile of every
strate with worms on, before the fold reached anything the room source proved. A
numerically correct bound that is structurally always fatal.

The fix was already written three lines up in the worm's own Eval: NetworkMask
is 0 when CaveSDF >= WormNetworkRange, which FLT_MAX always satisfies. The worm
IS spatially bounded -- by the room source's bound, exactly like the twelve
detail modifiers -- so where the source proves Identity it does not execute at
all. Thirteen inheritors instead of twelve. Verified FVoxelOpSample::Sdf really
does initialise to FLT_MAX rather than assuming it; the inheritance inverts into
a hole otherwise.

Deliberately NOT VF_NoCaveOverBox: that helper answers "identity" for a null
Rooms, correct for the twelve modifiers and wrong for an op a future assembly
could place behind a different SDF writer. No room source means we do not know,
which must cost CPU rather than a hole.

And the instrument, because the guess is the thing that cost a build: a
diagnostic that lists candidate causes without measuring them is still a guess
wearing rigour. FVoxelOpStack::ClassifyBoxAttributed reports the index of the
first op that kills each hypothesis -- same loop, same early-out, verdict
identical to ClassifyBox, because a diagnostic that takes a different path than
the thing it explains sends you hunting in the wrong place.
IVoxelDensityOp::DebugName gives them names and touches no cache key, so it
cannot change the world. Check 4 now always prints "AllSolid killed by: <op>
xN", and the zero-proved warning says to read it instead of re-deriving.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:10:51 +02:00
Fr0zkaandClaude Opus 5 03ddcde334 feat(opstack): FRoomGraphSource::EffectOverBox answers spatially -- and AUDIT C2 is fixed
The chain no longer dies at the room source. The criterion is the per-voxel cull
lifted from point to box, which can fail for exactly one reason: Eval starts at
MinSDF = FLT_MAX and only lowers it through a primitive that survives its own
cull, so if no cached primitive survives its cull anywhere in the box, Sdf stays
FLT_MAX across the whole box.

FSdfConvertOp already returned Identity ("la source a repondu pour la paire") and
the twelve detail modifiers already inherited through VF_NoCaveOverBox. One
function learned to answer and fourteen operators became provable -- what the C1
wiring was built for.

Three deliberate choices, all erring toward CPU rather than toward a hole:
- |Perlin3D| <= 2, derived from GradDot + the convex hull of a trilinear lerp,
  instead of the header's observed "~[-1,1]". A verdict resting on an observation
  is the hole this file spends its life avoiding.
- the op pool is passed to BuildChunkCache, not nullptr: the bake reads OpParams
  to place pits and chimneys, so nullptr would under-bound the cache and could
  return Identity over a real pit.
- the search box is wider than Eval's, giving a superset of primitives.

The verdict is memoised per box (all twelve modifiers ask the same question), and
the cache is a SECOND per-worker cache so classification cannot disturb a live
generation's hot cache.

AUDIT C2, confirmed 2026-07-28, is fixed on the switch path in the same breath:
GetDensityWithParams now takes required ParamsFingerprint + LayoutVersion. The
alternative this audit section used to recommend -- add chunk Z to the key -- is
insufficient (Interleaved makes Alpha depend on chunk XY too) and destructive
(chunk XY is deliberately absent so gradient probes don't thrash the box, ARCH
8.10). The CRC is taken once per chunk where the params memo already lives, so
the per-voxel cost is two integer compares. The three test call sites pass it
too, so the oracle stops sharing the defect it tests.

Check 4 of the tunnel test no longer asserts "0 proved" -- that assertion would
now forbid the gain. It brute-forces every proved tile voxel by voxel instead and
reports the count, because a false verdict leaves no geometry and no collision
behind it.

The second debt (per-room ops can raise a modifier's amplitude above the strate
params a box bound reads) turned out to be DORMANT, not live: where the source
proves Identity the modifiers' gate never opens, and where it answers Both it
supplies no MaxCarveOverBox so nothing is provable anyway. It goes live the day
the source gains one. Written at the site.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:48:18 +02:00
Fr0zkaandClaude Opus 5 6a8390f11f feat(opstack): ClassifyTile consumes ClassifyBox for cave archetypes -- the T1.d prize, behind the same opt-in
⚠️ THIS IS THE ONE COMMIT OF THIS RUN WHOSE FAILURE MODE IS A HOLE, NOT A REGRESSION. A non-Mixed
verdict makes the world skip GenerateMesh entirely: no triangles, no collision, invisible until a
player falls through. Read VoxelForge.OpStack.ClassifyTileSoundness before trusting it.

WHAT CHANGED
ClassifyTile used to `return Mixed` without a call for every cave archetype ("archétype cave [...]
pas prouvable en v1"). It now builds that strate's stack and folds ClassifyBox. SurfaceWorld and
bedrock gaps keep their hand-written proofs untouched -- the exact-lattice column test is strictly
better than any box bound, so the stack has nothing to add there.

ONE DEFINITION OF THE STACK MAPPING, and that is the load-bearing part
GetDensityAt's ~90-line build switch is extracted into VF_BuildOpStackForChunk and both callers now
use it. A second copy would be the worst bug available here: a tile skipped on the verdict of a
stack that is not the one producing its density is precisely a hole. A "keep these in sync" comment
would not have been enough; there had to be only one. Params are passed in by pointer, never fetched
inside, because both callers already have them.

SIX GUARDS, ALL FAILING TO MIXED, none giving the benefit of the doubt
1. The strate must actually opt in -- on EVERY chunk coord the box touches, not just the one that
   triggered the attempt. Otherwise the classifier judges a field the mesher will not produce.
2. One cave slot per tile. Two slots means two param sets, and a stack answers only for its strate.
3. No mixed cave/surface/gap tile: the cave stack's box would cover Z belonging to another strate.
4. The params must be BIT-IDENTICAL across every chunk coord the box touches. This is the guard that
   matters, and it exists because of the finding committed earlier today: GetGenerationParams blends
   params inside a strate (Alpha depends on chunk Z for Gradient, and on chunk XY as well for
   Interleaved), so one stack genuinely cannot represent a tile that straddles a transition band.
   Memcmp on POD: differing padding can only produce a false MISMATCH, i.e. one Mixed too many.
5. A 27-chunk-coord cap, so a very wide tile does not pay for the check. We give up the gain, never
   the safety.
6. The disturbances are folded by hand (chasm ⇒ CarveOnly, bridge/ridge ⇒ FillOnly), because
   DECOMPOSITION 10.2 leaves them OUTSIDE the stack -- GetDensityAt applies them after the negate.
   A verdict that ignored them would be wrong exactly where they act. Same inequalities the
   SurfaceWorld branch already uses.

The diff layer needs no new guard: ClassifyTile already returns Mixed for any tile with player mods
in range, before any of this.

TEST: VoxelForge.OpStack.ClassifyTileSoundness -- the same brute-force oracle as the existing
ClassifyTileSoundness, on a world where every strate opted in. It does not check the fold (that is
BoxVerdictFold) or the operators (those are the eight equivalence tests); it checks the WIRING. The
number to read first is the count of tiles actually brute-forced: zero non-Mixed verdicts would mean
the test proved nothing, so that case is an ERROR rather than a quiet pass. Its failure message
lists the four suspects in the order worth checking.

FIXTURE: FTestWorld::Build gains a bUseOperatorStack parameter (default false, so the thirteen
existing tests still exercise the switch), and every test world now gets a PROCESS-UNIQUE
LayoutVersion. That second change fixes a real cross-test hazard that was only ever hidden by an
accident: PassagesVersion is per-instance and starts at 0, so two FTestWorlds both reported 1, and
GetDensityAt's per-chunk caches are keyed on (ChunkCoord, LayoutVersion) -- one world could be
served the previous world's params AND its CP_UseOpStack flag. Invisible while every world agreed
the flag was false. The first world that ticks it removes that coincidence, in both directions.

WHAT THIS BUYS TODAY: Maze, FlatPlain/CrystalChamber, VerticalShafts and FloatingIslands can now
prove tiles in production, which is where the measured skipping (Maze 23/60, slabs 36-40/60) turns
into frames. TunnelNetwork and Underwater still prove nothing: their chain dies at FRoomGraphSource,
which answers Both with unknown amplitude. Making it answer spatially means building the SDF cache
for the queried box -- now worth doing, since a skipped tile saves 30k+ density evaluations, and the
amplitude fold plus the modifiers' Identity inheritance are already in place to receive it. That is
the next piece.

And nothing here changes Jahni's current world: no strate asset has bUseOperatorStack ticked, so
every guard above is unreachable in his project until he ticks one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 04:05:42 +02:00
Fr0zkaandClaude Opus 5 10dbe64b06 feat(opstack C3): TunnelNetwork + Underwater in the ported list -- 8 of 8
UsesOperatorStackForChunk now returns true for both, so the archetype switch has a complete
operator-stack twin: per-strate opt-in, every archetype equivalence-tested bit for bit against its
original function.

This changes nothing by itself. The flag still requires bUseOperatorStack ticked on a strate asset,
which is Jahni's call and was deliberately NOT done. What HAS changed is that the flag is no longer
a no-op anywhere: ticking it on any strate now really switches that strate onto the stack.

Not done, and it is the next real prize: ClassifyTile still uses hand-written guards and does not
consume ClassifyBox. That is where measured tile-skipping becomes frames.

DOCS
- CODEMAP 3.2d: ported list 6 of 8 -> 8 of 8; the BuildTunnelNetworkStack row rewritten (19 ops, one
  builder for two archetypes); six new rows for the detail modifiers, each carrying the thing a
  reader would otherwise have to rediscover -- roughness reads STRATE params, terrace re-queries the
  SDF, the cliff's comment disagrees with its code, columns have no strate parameter at all, and
  LocalParams() is the override whose EffectOverBox is too optimistic on a strate with an op pool.
  Also corrected the stale "never compare the two paths" line: C10 is closed and all eight
  equivalence tests compare bit for bit.
- CODEMAP 3.3 UsesOperatorStackForChunk row: same list, plus the warning that the flag is now a real
  switch rather than a harmless tick.
- OPSTACK-PLAN: status header and the Phase 2 order both updated; the three-stage TunnelNetwork
  breakdown and the calls-not-transcribes rule recorded there rather than only in the code.
- OPSTACK-PROGRESS: the closing entry for this unattended run -- every commit in order, the five
  original-code findings ported as-is, the two decisions that are not reversible by taste, the
  ClassifyBox optimism C1 introduced and that must be fixed before ClassifyTile consumes it, what
  breaks first per group, and the likely compile-error spots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 03:46:17 +02:00
Fr0zkaandClaude Opus 5 ef5bda3d8a feat: TunnelNetwork stage A — the SDF spine, wrapping BuildChunkCache
The last archetype is ~1080 lines with 13 detail modifiers, a two-region
cache and a per-room op override. Porting it whole before anything can be
verified is ~600 unverified lines on top of ~200 — the pattern this
refactor has dodged six times. So: three stages.

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

TunnelNetwork stays OFF in UsesOperatorStackForChunk until stage C.

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:28:06 +02:00
Fr0zkaandClaude Opus 5 96e75abe57 feat: port FloatingIslands — the stack that runs backwards
6 of 8 archetypes ported. This one starts from VOID and FILLS where the
other four start from ROCK and CARVE, which is what it was worth doing:
neither end of the pile needed a new operator, only the opposite sign.

  FConstantRockSource -> FConstantFieldSource(+/-Base)   AllSolid <-> AllAir
  FSdfCarveOp         -> FSdfConvertOp(Sign = +/-1)      carve    <-> fill
  FSdfRoughnessMod                                       4th archetype, unchanged

Only the island blob source is new. Multiplying by +/-1 is exact in
IEEE-754, so the three already-green ports are bit-for-bit untouched.

ClassifyBox can return AllAir for the first time in the plugin, and an
island strate is by construction mostly empty — the test counts AllSolid
and AllAir separately so an aggregate cannot hide whether that fired.

Two bounds that would have been holes if assumed rather than derived:
the island bound is one-sided (a hairline thread of matter hangs below
each island down its axis, so only the TOP may reject), and the domain
warp displaces X and Y independently, so the pad needs WarpAmp*sqrt(2).

Also: AUDIT C1 was NOT closed. The 2026-07-27 sweep matched `SeedF * K`
and this archetype's warp spells it `(float)S * K`, so one site survived
— at seed 2e9 the warp flattens and every island snaps back to a perfect
circle. Fixed in both paths in one pass so the equivalence test stays a
valid oracle. Expect island silhouettes to change at large seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:50:36 +02:00
Fr0zkaandClaude Opus 5 3acb3fbc6b feat: port VerticalShafts — three ops reused from Maze unchanged
The port that tests the thesis rather than the fidelity. Previous ports asked whether
the decomposition reproduces the original; this one asks whether operators actually
get reused across archetypes, which is section 2.5's claim and the only reason to do
this refactor instead of tidying the switch.

ConstantRock, SdfRoughness and SdfCarve are Maze's, reused without a line changed.
In the switch, GetMazeDensity and GetVerticalShaftDensity are two ~100-line functions
with nothing visibly in common; as operators they are the same three ops with a
different source and different tuning (freq 0.1 vs 0.12, window rough+4 vs R+rough+2).

New: FShaftFieldSource (infinite cylinders + hash-gated connectors into the SDF
channel) and FShaftLedgeMod (banded shelves on the +X/+Y half so the shaft stays
climbable).

Deviation from section 6, stated: it suggested splitting the source so the XY-pure
cylinder half could get an exact box verdict. Kept as one op because the connectors
derive from the same 3x3 roll and the ledge mod needs the shaft list anyway, so
splitting means rolling twice or sharing a cache between two ops. Forfeited: the exact
verdict on the cylinder half. Kept: a conservative EffectOverBox testing circles and
connector reach.

FShaftLedgeMod gates on the POST-roughness Sdf as the stack left it; re-deriving it
would use the pre-roughness value and shift every ledge. Reading the channel rather
than recomputing is what the two-channel sample is for.

Compile fix: FCells was declared below the functions returning it. Member bodies are
deferred, return types are not.

Ported: Maze, FlatPlain, CrystalChamber, SurfaceWorld (biomes included),
VerticalShafts — 5 of 8.

UNVERIFIED: not compiled past the FCells fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:11:07 +02:00
Fr0zkaandClaude Opus 5 921a9fb666 feat: height-space operator family — SurfaceWorld step 1, and C10 is solved
Maze and Slab now report BIT-IDENTICAL: FPSemantics = Precise, set for
cross-platform play, dissolved the ULP residue. Hypothesis 3 had the right
mechanism all along — under /fp:fast the compiler transforms by surrounding
context with no isolable axis, which is exactly why five one-variable experiments
all came back negative. Removing the permission removed the difference. Nobody
solved C10; C9 got fixed for an unrelated reason and C10 fell out of it.

SurfaceWorld step 1 forced an architectural decision. DECOMPOSITION section 5 notes
the height ops operate on Z values rather than density, then lists them as children
of FHeightfieldSource. Writing them made the consequence unavoidable: they do not
fit IVoxelDensityOp. No input Z (they produce one), XY-pure per column rather than
per voxel, and they write neither channel. Forcing them in would need a per-voxel
channel for a column property, or one opaque op — section 2.5's failure mode.

So height space gets its own contract: VoxelHeightOp.h (FVoxelHeightSample with
Height + Relief, IVoxelHeightOp, FVoxelHeightStack) and five ops. Relief is the
original's M — produced by the structural source, consumed by the terrace gate.
Section 0.1 found density needed a second channel; this found terrain needs a
second space.

The type system now forbids for free what AUDIT 6.3 warns about: a height stack
cannot hold Z-dependent data because there is no Z in the signature.

Deliberately staged — this touches nothing on the density path. If height space had
not decomposed cleanly, it shows up here for one test rather than after building the
adapter, the column cache integration and the dispatch on top.

The test runs twice; the second pass is load-bearing because the F20 terrain ops are
off by default, so a defaults-only run leaves all four modifiers untested. It also
brute-forces MaxDisplacement, since a false bound would later be a hole.

ComputeSurfaceTerrainZ moved private -> public for the test, same justification as
GetSlabDensity. Old declaration removed.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:06:50 +02:00
Fr0zkaandClaude Opus 5 644339def5 feat: Phase 2 first port — FlatPlain + CrystalChamber collapse into one op
Jahni closed OPSTACK-DECOMPOSITION 3.1: the slab noise Z term was not
intentional character. Phase 1 also closed — the visual A/B on Maze passed.

Two changes, deliberately together, kept attributable by the test:

1. Design: GetSlabDensity's floor and ceiling noise lose their Z terms. A floor
   height no longer depends on the altitude you sample it from. The ceiling keeps
   its + 3000.0f, which is a decorrelation offset, not a Z term. The world
   re-tunes once — a different slice of the noise field, not a worse one.

2. Refactor: the now-XY-pure function ports to FSlabVoidSource + FGridColumnMod
   plus the three structural ops. BuildSlabStack has NO branch on archetype
   because GetSlabDensity never had one — CrystalChamber is FlatPlain with a
   bigger CeilingRoughness. 8 archetypes -> 7.

SlabEquivalence compares against the reference AS IT IS NOW and runs the whole
battery on both slots, so green means the port is a pure refactor and any visual
delta is attributable to the Z-term removal alone. The attribution comes from the
test, not from splitting it across two builds.

The payoff 3.1 was actually about: FSlabVoidSource::ClassifyBox is exact and needs
no sampling. FBM is contractually [-1,1], so both surfaces live in Z bands with
known bounds — a tile below the floor band is provably solid, a tile between the
bands provably air. ClassifyTile proves zero tiles for these archetypes today.
FGridColumnMod answers Identity when no column reaches the box, which is what lets
the source's AllAir verdict survive the fold.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:30:26 +02:00
Fr0zkaandClaude Opus 5 974e795b66 fix: call PrepareChunk and guard the degenerate strate on the wired path
Read the step-3 wiring against the equivalence test before spending a build.
The symbols all line up; the two PATHS did not.

- GetDensityAt never called FVoxelOpStack::PrepareChunk, though the test does.
  All seven concrete bodies are empty today so behaviour is unchanged — which is
  the reason to fix it now: the first op to hoist real per-chunk work would have
  been green in test and silently wrong in game. Builds an FVoxelOpContext in the
  same refetch block (chunk, seed, layout version, strate Z bounds). Step stays 1;
  GetDensityAt does not know the mesher's sampling step (T2.b).

- GetMazeDensity early-outs to air on a degenerate strate (height <= 0) and the
  stack has no such early-out by design. Unguarded that is air on one path and
  spine/seal-of-a-zero-height-band on the other, so the wired path now falls back
  to the switch there — the reference behaviour is the behaviour.

Docs: VoxelDensityOpStack.h's banner still claimed nothing here feeds the game,
and CODEMAP 3.2d repeated it. Both now state what is wired (GetDensityAt) and
what is not (ClassifyTile, hand-written guards, Phase 2), with C10's never-compare
rule at the point of use. CODEMAP gains UsesOperatorStackForChunk and
bUseOperatorStack rows, and BuildMazeStack's degenerate-strate precondition.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:15:59 +02:00
Fr0zkaandClaude Opus 5 8a33bcb42a docs: CODEMAP rows + progress entry for the Phase 1 Maze port
CODEMAP gains 3.2c (VoxelDensityPrimitives), 3.2d (the operator stack and its
factories), FVoxelOpSample under 3.2b, and the two new tests under 3.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:33:00 +02:00
Fr0zkaandClaude Opus 5 831ee2fbf7 docs: Q3 tick the stale PENDING BUILD markers + CODEMAP rows for the new symbols
Q3: fable-idea F20 phases 1/2 and F18, REVIEW_FINDINGS perf pass 2 and batch 3,
and ARCHITECTURE's biome full-param redesign were all still carrying
"CODE-COMPLETE, PENDING BUILD" markers dated 07-04/-06/-08. Jahni confirmed on
2026-07-26 that everything is built and working (AUDIT-2026-07.md §0), so the
documents were misreporting project state. Ticked with the date they were
ticked, not just the date they were built.

Deliberately NOT ticked: ARCHITECTURE's F6 master material graph. Its C++ half
is built, but the material graph itself is editor-side work that is genuinely
still open, and ticking it would recreate the problem this queue item fixes.

CODEMAP discipline for this batch: new §3.2b (the VoxelDensityOp contract), new
§3.12 (Private/Tests), the EVoxelTileClass move into §3.2, and
FChunkBiomeCache::Invalidate under the biome types row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:06:32 +02:00
Fr0zka 69fa73e07e tmp 2026-07-26 02:11:11 +02:00
Fr0zka cb61c8b2e4 potential perf regression 2026-07-04 11:30:43 +02:00
Fr0zka 6875614002 Fix Decoration Placement 2026-06-26 19:15:04 +02:00
Fr0zka e6cd852129 Another pass 2026-06-23 08:30:13 +02:00
Fr0zka db558d9e14 j 2026-06-16 03:39:22 +02:00
Fr0zka f030eec08a Upload of all files, starting point 2026-06-09 20:21:29 +02:00