f2044135ea11455ca76b2941a2b306cd7d5d460d
204
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
271bbdd034 |
docs(composer): record what measuring the world actually found
The notes described Tier 1 as done and had no record of today. Adds §13 (Tier 2 built, and the three artifacts that looked exactly like findings), §14 (the VerticalShafts bug end to end -- three bugs wearing one coat), §15 (working notes that cost something), and corrects the Tier 1/Tier 2 entries in §7. The rule worth keeping, because it held three times out of three: when a measurement reports something alarming, SUSPECT THE MEASUREMENT FIRST. "FlatPlain is 99.95% void", "5 of 15 endpoints disconnected" and "7 of 16 seeds fail" were all method, not world. Also recorded: the two guard directions are not symmetric (a false positive drops a player through the floor; a false negative merely sends you fixing healthy code), percolation is a phase transition and not a knob, and Tier 1 shipped half built in a way only a connectivity measurement could reveal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
8cd5face9b |
fix(strate): aim at the room's centre, not beside it
Commit
|
||
|
|
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 |
||
|
|
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 -- |
||
|
|
a60138b19c |
docs(composer): split the transcript -- notes become generator-only
CLAUDE.md says each doc has one job and they must not duplicate. After GDD.md
landed, COMPOSER-NOTES.md and it both held the death loop, the city tiers and
the economy -- and duplicated documents drift until neither is authoritative.
So the notes are now the engineering document and nothing else: the vision and
its correction, what the game *requires* of the generator (constraints only,
pointing at the GDD for why), the composer itself, the manifest, naming, every
code finding and hazard, the build order, and the corrections nobody should
re-derive. 1561 lines down to 527, with the game design replaced by pointers.
Two sections earn their place on their own. Section 6 is the verified code
findings, including the one that blocks free composition (EffectOverBox answers
for the source+carve pair, so an op's box verdict is only correct in the stack
it shipped in) and the primordial law's live hole (GeneratePassages never
consults the destination strate's layout, so a passage can open into a sealed
pocket). Section 8 is the list of corrections that cost real time: Phase 3 is
reopened, Jahni does not want to author strates either, peer determinism never
required matching the old system.
The full 1561-line transcript remains in git at
|
||
|
|
b781e3bab2 |
docs: a game design document, distilled from the transcript
COMPOSER-NOTES.md is a conversation: chronological, with my wrong turns kept on purpose so nobody re-derives them. That is the right shape for us reading it back and the wrong shape for anyone else, including Codex. GDD.md is the other half -- the decided state without the journey. Seventeen sections covering the premise, the core loop, the city, the depths, a strate, descending, the economy, death, housing, the verbs, the season, the community layer and the opening. Every section separates what Jahni decided from what is still open, and design that is mine rather than his says so, so nothing reads as settled that isn't. Written deliberately plainer than the notes: prose rather than dense tables, because a design document that only its authors can read has failed at the one thing it exists for. Also records why the file is in the wrong folder. This is a document about the game, and most of it -- the city, combat, the economy, seasons -- is nothing to do with VoxelForge. It lives in the plugin because the plugin is the only part of the project under version control. It should move to the project root once that becomes a repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
b7cf12af4c |
docs(composer): the depths regenerate, the city does not -- game design session
Second design session. Started on the composer, ended up with the whole game's economic loop, and one terminology fix that was overdue. The loop, closed: the surface is a sky island or an unclimbable mountain, so resources must come from below; the depths supply them; the resources build the settlement; the settlement enables deeper descent. The settlement is communal, identical for everyone, and permanent -- a season advances it one tier (camp, village, town, city). So the wipe means something beyond freshness: the world is impermanent, the civilization is not. We lost our maps, but we built a town. I had that arc backwards and it is marked as such in the file, along with the vocabulary that caused it: "the world" was being used for two different things. The depths are what VoxelForge generates and what regenerates each season; the city is a plain Unreal level, permanent, and entirely outside this plugin. That distinction is now pinned at the top of the notes. Also settled: no bottom, boss strates every ~5 as intermediary gates, gear stats capped around depth 30 with deeper loot buying better rolls and unique materials rather than bigger numbers, materials tagged by difficulty tier rather than depth so their location is rediscovered every season, and death leaving a possessed corpse that copies your weapons without taking them -- a mirror match that self-balances at every tier forever, provided the copied gear is never lootable. Two design records worth keeping: the manifest is two things (a tiny stored spine and an infinite hash-addressable field, where the ID encodes the position so resolving it needs no table), and the opening cinematic -- civilization emerged from the depths, humans crystallized in the first artificial strate -- justifies the authored/generated seam in fiction. A player who senses strate 1 feels different from strate 2 is reading the story correctly, not spotting the join. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
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
|
||
|
|
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 |
||
|
|
eae7e68caf |
docs(composer): the composer runs once per season, offline -- so it can take all night
Second half of the design session. The realisation that resets the whole cost budget: everyone shares one seed per season, so there is exactly one world per season and the composer never runs on a player's machine. It runs once, on a build box. Not "fast enough for a loading screen" -- "runs overnight". It can generate a thousand candidates, keep eight, and let Jahni veto the season before it publishes. What ships is the seed plus the chosen parameter vectors. The measurement pass itself is one coarse grid and one flood fill, producing one feature vector that serves validation, danger and rarity at once. Noted the one place coarse sampling can lie -- a wall thinner than the sample spacing makes sealed spaces look connected, which would pass the primordial-law check on an impassable strate. Coarse to find the route, full resolution to confirm it. And the answer to "how do we ever judge what's good": the judge is the corpus, not a model. Good never gets defined, only collected -- strate-level thumbs up/down, player behaviour, and best of all where players choose to snapshot their home, which is an ungameable "I love this place" that costs nothing to collect. Known risk written down: promote only what you already liked and the system converges on a comfortable middle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
d8f7dabee8 |
docs(composer): the system should invent worlds -- design session notes
A long design conversation, written down as it happened. The headline: the op stack's "Phase 3" is REOPENED. Asked what the world was missing, Jahni described exactly it -- the system assembling worlds from data assets and a seed -- so the 2026-08-16 handoff's "do NOT start Phase 3" is superseded. Then a correction that moved the whole thing a level deeper: he does not want to author strates either. The system must INVENT them. He authors raw content (operators, meshes, materials, sounds, creatures) and tags it; everything above that is composed and measured. Decided along the way: worlds are wiped every 6-12 months, which dissolves the "generation is frozen forever once public" problem -- generation is frozen only within a season. Shared player builds are blueprint structures, vote-ranked. Housing is an instanced level built from a snapshot of land the player chose. Danger is depth x a per-strate multiplier, not a sort. Every strate has a findable entrance and exit -- built first, generated around. The range problem turned out cheap: don't roll 73 parameters independently (that is what makes mush), blend known-good ones with FStrateGenerationParams::Lerp, which already exists and already blends the whole field set. Ranges come from the spread of the existing corpus, not from authored numbers. Verified against the code: the X-macro is in sync with the struct, only Alpha differs. Recurring mechanism, arrived at three separate times: measure, don't declare -- and promote what measures well back into the corpus, so the system's vocabulary grows from its own history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
451ebcb776 |
docs: close the operator stack -- handoff rewritten in plain language
Jahni built the last two commits and the game looks fine. Closing the thread. OPSTACK-HANDOFF.md fully rewritten. The old one had become a private dialect -- "you've started using very unique terminology, i have NO idea what you've been doing" -- which is a failure of the document, not the reader. The new version leads with what the op stack was for, says plainly that Phase 3 was never built and is not being built, carries the honest three-week ledger (120 commits, every feat: a port, the world unchanged by design), and translates the jargon. Final state: 8/8 archetypes live and bit-identical, 14/14 tests green through |
||
|
|
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> |
||
|
|
4d33321bfa | feat(classify): fold cave boundary categories safely | ||
|
|
2303442d8f |
docs: T1.d fires in production -- 39% of tiles skipped, measured in game
Classified 1.88 / Meshed 1.15 / SkippedAllAir 0.73 (1.15 + 0.73 = 1.88, exact). Was Classified 1.90 / Meshed 1.90 / nothing skipped. Cave Bail No Layout went 1.58 -> gone. 39% of classified tiles now skipped against 0% this morning, each avoiding ~43000 density evaluations plus marching cubes. First observation of tile-skipping in the running game rather than an automation harness. Not yet firing, stated plainly: TilesSkippedAllSolid and the two op-stack counters remain absent, so the cave branch still produces no verdicts in game -- its attempts land in Cave Bail Mixed Content (0.77) and Stack Verdict (0.32). The win here is the open-air class proved by the classifier's own Z categories, not by ClassifyBox. That is the next thread and the counters now name it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
05986bd875 |
docs: T1.d blocker FOUND -- out-of-layout chunks misroute into the cave branch
In-game with Sol's four-way split: NotOpStackNoLayout = 1.58 of 1.90 classified
(83%), while SoleSlot, BoundaryTile and MixedContent never fire. So the assets
ARE ticked -- Jahni was right -- and the neighbour theory is dead. The split
earned itself on its first flight; the old lumped counter would have sent us at
the assets again.
Mechanism: GetGeneratorTypeForChunk returns TunnelNetwork for any chunk outside
the strate stack ("fallback produces solid rock anyway"), while IsGapChunk
returns false for above-stack chunks ("open air, NOT a gap"). So every chunk
above TopChunkZ = 0 -- all the open air over the world -- looks to ClassifyTile
like a cave archetype, enters the cave branch, finds no layout slot, and bails.
T1.d was never failing. It was unreachable, behind a routing mistake in the
archetype lookup that has nothing to do with the operator stack, the box
verdicts, or the flags -- all of which are correct and tested.
Not fixed: the two comments disagree about whether out-of-layout is solid rock
or open air, and the right verdict depends on which. Read GetDensityAt's
no-slot path before choosing; guessing there writes a hole.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
b06c39c077 |
docs: suite green after the verbosity fix; FIELD digest proves nothing moved
14/14 pass with no StrateManager warnings. Every reported number is identical to the pre-change run, including the CrossPlatform FIELD digest (0xF62F3D355B1C0BDB) -- a bit-for-bit hash over 115,000 samples of the whole field. That digest is the strongest evidence available: the six-box LRU, the DensityCacheOwnerId key, the bail re-attribution and the scope fix did not move a single voxel between them. Four changes to caching, keying and diagnostics, and the world is byte-for-byte identical -- exactly the contract each claimed, now tested rather than asserted. Still unmeasured: the performance benefit of those caching changes. "Quite a fraction of what it used to be" is a real observation, not a same-seed/ same-route number. Still open: read `Operator-stack opt-in` in the GAME log. That line is the ground truth for T1.d and supersedes both the bail-counter inference and the recorded belief that the assets have the switch on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6e7ea7038a |
docs(opstack): record unbuilt experimental landing
Append the exact commits, Approach A memory decision, cave-only warning caveat, diagnostic semantics, CP owner scope, compile watchpoints, deliberately open measurements, and recoverable worktree cleanup. No build or runtime result is claimed. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
588f9e0294 |
docs: Sol investigation -- refutes my boundary-tile hypothesis, corrects my false accusation
Sol High investigated with Luna xHigh sub-agents; two approaches built in isolated worktrees, main tree untouched. Two corrections to my own assertions: 1. "One un-ticked neighbouring strate kills boundary tiles" is WRONG. UsesOperatorStackForChunk depends only on ChunkCoord.Z, and ClassifyTile's earlier Z loop already required the predicate for every sampled cave Z within one slot -- so the later XYZ sweep is redundant, not over-strict. Removing it unlocks nothing; boundary tiles still fail the one-slot/type/params guards, correctly. 2. I falsely wrote that VF-03's fixture citation was fabricated. It exists (VoxelForgeTestFixture.h ~134/146, explicit about CP_UseOpStack contamination). I had read only the 30-line header and asserted a negative from a partial read. Header corrected. VF-03's core claim is now CONFIRMED: GetDensityAt keys thread_local CP_* by (ChunkCoord, LayoutVersion) with no world identity, and every manager's version starts equal. Also: the Cave Bail Not Op Stack counter I specced is ambiguous -- it fires before the different-slot attribution, so it cannot distinguish "flown slot un-ticked" from "boundary tile met an un-ticked slot first". My 80% reading was over-confident. Same defect I fixed for TilesSkippedAllSolid, reintroduced one layer down. Approach A recommended (explicit opt-in + six-box LRU) over B (code-enforced cutover): the code does not justify weakening ClassifyTile, and B would delete the flag, which is the only same-route A/B lever available to prove the refactor. Narrow A's warning to cave archetypes before adopting. Nothing built. Worktrees left in place for review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ba53f2ede |
docs: T1.d is blocked by CONFIG not code (Cave Bail Not Op Stack = 80%); retract memo claim
Part B paid for itself on its first flight. Over the caves, Tiles Classified == Tiles Meshed (nothing skipped at all) and CaveBailNotOpStack accounts for ~80% of cave-branch attempts. That counter has exactly one cause: UsesOperatorStackForChunk returned false -- so either the strate flown over has not ticked bUseOperatorStack, or a chunk in the tile's box belongs to an adjacent strate that has not, since the per-chunk sweep requires every chunk to agree. CaveBailStackVerdict being non-zero (0.32) corroborates: where the flag did hold for the whole box, the stack built fine and merely returned Mixed. The machinery works; it is mostly not being reached. RETRACTION: I called the column-memo port a regression after comparing 14.7% from one flight against 23.3% from another. Invalid -- the miss rate is strongly location-dependent (~10-30%) and was in that band before the port. Comparing single-flight averages from different routes measures the route. A real before/after needs the same seed and route; parked until then, and not urgent. Kept while diagnosing: GSurfColCache is a 6-box LRU, not one box. My spec said "a direct-indexed box" and Codex implemented one faithfully, so any recentre now wipes all 6561 cells where the old table lost one entry. Plausible thrash source, unmeasured. Port the LRU if the memo is revisited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
bc0bf83c64 |
docs: VF-01 confirmed (worker race on rebuild) + VF-10 triaged; hold further code
VF-01 CONFIRMED and it is the most serious find of the day. Initialize does StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and reallocating both -- with no lock, barrier or drain anywhere in that file, while worker threads read the same arrays through AnyPassageNearBox (:460), EvaluateModifierSDF and FindSlotIndexForChunkZ, reached from GetDensityAt/ClassifyTile. RegenerateAllChunks bumps the epoch AFTER the mutation, so previous-epoch workers are live during it; the epoch rejects a finished result, it cannot make a read of a freed allocation safe. Same class already fixed once here: DiffLayer.ChunkMods got ModsLock after a carve-vs-stream AV. The dangerous call site is OnObjectModifiedInEditor, which fires automatically on a strate asset edit while streaming -- routine here. VF-10 confirmed real (74 fields, per near-surface sample) but Sol missed the conclusion: the copy is INHERITED from GetDensityWithParams, so both paths pay it and it does NOT explain the op-stack regression. The op stack improved it by memoising across eleven detail ops. Future optimisation, not the answer. Deliberately NOT fixing VF-01 now. Four code changes are stacked unbuilt and three have "nothing should move" as their acceptance signal; a fifth change to streaming lifecycle would make an odd build result unattributable. The three fix options (drain / snapshot / RWLock) trade a possible crash for an editor hitch or hot-path lock traffic -- a product decision, not an agent's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
8ef4d7dc09 |
docs: CORRECTION -- bUseOperatorStack is ON in the game's data assets
The handoff said "No strate asset has the box ticked -- that is my call and I still haven't made it." Stale, and I reasoned from it all session. Jahni: "the data assets in game have the switch on." Everything downstream flips. The operator stack is the PRODUCTION density path, so the measured perf regression is one players feel, and any unsound EffectOverBox is a live hole rather than a latent one. The two correctness fixes committed today ( |
||
|
|
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> |