811c4b7b172654c64e90395a693452b090cff74d
62
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
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 |
||
|
|
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 -- |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
3128852d4e |
chore: track the design docs in git (.gitignore !*.md)
AUDIT P1: every markdown design doc except CODEMAP.md was untracked, so ARCHITECTURE / AUDIT / OPSTACK-PLAN / fable-idea / REVIEW_FINDINGS lived only on disk. Replaces the single !CODEMAP.md exception with !*.md. Also makes OPSTACK-PROGRESS.md commits actually record something, which the unattended crash-safety discipline depends on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |