Commit Graph
189 Commits
Author SHA1 Message Date
Fr0zkaandClaude Opus 5 13399529b5 fix(measure): measure the strate, not the band above its floor
The window excluded a whole chunk (32 voxels) at each end to avoid a
4-voxel seal band -- 8x too much. On 4-chunk strates that discarded half
of every strate, floor included, so the pass was measuring the open air
above the ground and calling it the strate.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Test-only. Not built.

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

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

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

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

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

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

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

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

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

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

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

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

Not built -- Jahni builds.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two corrections to my own assertions:

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

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

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

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

Nothing built. Worktrees left in place for review.

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

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

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

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

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

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

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

Not built -- Jahni builds.

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

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

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

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

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

Not built -- Jahni builds.

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

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

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

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

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

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

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

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

Not built -- Jahni builds.

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

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

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

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

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

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

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

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

Comments only -- no code changed.

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

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

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

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

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

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

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

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

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

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

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

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

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

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:50:01 +02:00
Fr0zkaandClaude Opus 5 108982d135 docs(opstack): the proved-bound rule now names VF_PerlinAbsBound and the FBM normalisation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:36:45 +02:00
Fr0zkaandClaude Opus 5 7dbdf51b44 fix(opstack): three ExtraReach box verdicts used sup|FBM| = 1.0; the proved bound is 1.5
CORRECTNESS on a box verdict, not perf. This file derives |Perlin3D| <= 1.5
rigorously, exposes it as PerlinAbsBound, uses it for the tunnel warp, and says
outright that the header's "~[-1,1]" is an observation and that a box verdict
resting on one is a hole. Three ExtraReach formulas in the same file assumed
sup|FBM| <= 1.0 -- and VoxelNoise::FBM normalises (Total / MaxValue), so
sup|FBM| = sup|Perlin3D| exactly. The bound was wrong by 1.5x.

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

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

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

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

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:36:04 +02:00
Fr0zkaandClaude Opus 5 b5294b2d5b docs: close C2, sharpen the column-memo prediction, reassess REVIEW_FINDINGS
Reading session -- no build available, so: verify things instead of writing code.

- Cross-checked the eight stat declarations against their definitions and use
  sites. Identical. The one compile risk I had only eyeballed is now retired.

- REVIEW_FINDINGS: the GetDensityWithParams and BuildChunkCache splits are now
  counter-productive rather than optional -- the op stack is replacing the
  first, and FRoomGraphSource deliberately CALLS the second so a restructure
  forks what was kept unforked. The two passage enums are not duplicates and
  are UMETA-serialised into authored assets; merging them rewrites content for
  tidiness. Judged: leave, and close the item.

- CODEX-TASK-002's acceptance said "20-30% of lookups", a guess wearing a
  number. Replaced with arithmetic from the real grid: 1225 columns/tile over
  35 Z planes, load factor 0.299, ~317 colliding columns, ~12000 vs 1225
  computations = ~9.8x. Plus the reading rule that matters -- the overhang and
  cliff mods re-query the same XY and add HITS only, so compare the ratio of
  the two hypotheses, not an absolute percentage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:14:18 +02:00
Fr0zkaandClaude Opus 5 b426cfcb0d docs(audit): C2 is FULLY closed -- the live-edit half was fixed too
Was about to spec a Codex fix for OC_Chunk / BM_Chunk / FChunkBiomeCache,
which three docs still list as open. Checked the sites first. All of them
already carry a layout-version guard:

  CP_Chunk -> CP_Version      OC_Chunk -> OC_Version
  BM_Chunk -> BM_Version      TC_BiomeCache -> TC_SeenVersion

FChunkBiomeCache::Invalidate() exists precisely because the validity box says
nothing about the FBiomeContext its cells were classified against, and all four
thread_local instances call it on a version change. The only other two
instances in the tree (VoxelContentManager ~445, the height-stack test) are
function-local, constructed per task, so staleness is impossible there.

Seventh time in this project a confident premise reversed on reading. It cost
a doc edit instead of a Codex run and a build cycle.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 16:01:26 +02:00
Fr0zkaandClaude Opus 5 7909c4f2ca docs(opstack): handoff -- 001 chains into 002, and PERF is now aimed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:46:30 +02:00
Fr0zkaandClaude Opus 5 7313201e91 docs: spec CODEX-TASK-002 (column-memo thrash) and aim the PERF work
Three orchestration findings on the parked PERF item, none measured yet:

1. The perf A/B needs no new code -- two Insights traces on a deterministic
   world are a clean before/after. But tile skipping changes how many times
   GenerateMesh runs, so totals conflate "cheaper per tile" with "fewer
   tiles". TilesMeshed from task 001 is the denominator. 001 is therefore a
   PREREQUISITE for the perf work, not a parallel item as the handoff had it.

2. The suspects don't share an archetype: SurfaceWorld is the column memo,
   TunnelNetwork is 19 virtual calls per voxel (16 + 3 structural post).
   One lumped number can't be acted on -- measure one strate at a time.

3. GetColumn is a direct-mapped hashed table where GSurfColCache is a
   direct-indexed box, and the mesher pre-samples Z-OUTERMOST (verified,
   VoxelMarchingCubesMesher.cpp ~226) so every column is revisited ~34x per
   tile. The table's sizing comment reasons about fullness; a direct-mapped
   table evicts on collision. At load factor 0.28 that is still ~25% of
   columns recomputing the full height stack every Z plane -- derived ~9x.

Task 002 measures that and fixes nothing, so the instrument and the fix
cannot land in the same build and make each other unreadable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:45:44 +02:00
Fr0zkaandClaude Opus 5 57002b35cf docs(opstack): log the CODEX-TASK-001 acceptance-bar correction
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:40:48 +02:00
Fr0zkaandClaude Opus 5 6bd5589d53 docs(codex-001): the lumped AllSolid counter cannot prove T1.d -- add the op-stack site
Reviewing the spec against ClassifyTile reversed its acceptance bar. The function
reaches a non-Mixed verdict two independent ways: the hand-written path (a bedrock
gap sets bCanAir=false, VoxelGenerator.cpp ~2835, and the tile resolves AllSolid)
and the operator-stack path (the bAnyCave block). The first fires with NO strate
opted in, so the spec's baseline -- "TilesSkippedAllSolid stays 0 underground" --
was never going to hold, and the before/after would have been unreadable.

Adds site B at the bAnyCave exit (~3009) with TilesOpStackSolid / TilesOpStackAir.
Those are zero BY CONSTRUCTION without an opted-in strate, since the branch returns
Mixed at the UsesOperatorStackForChunk gate -- a stronger baseline than the one it
replaces. Deliverable is now: tick the box, TilesOpStackSolid goes non-zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:40:23 +02:00
Fr0zkaandClaude Opus 5 251a1288e0 docs: spec CODEX-TASK-001 (tile-skip stats) and refresh the handoff for the Codex tandem
Jahni built a world and said "I don't know if it dropped any meshing? but it
looks alright by the eye." That sentence is the honest state of this refactor:
everything proved so far was proved in an automation harness on 40 sampled
tiles, and in the running game tile-skipping is unobservable.

grep INC_DWORD_STAT over Source/ returns nothing -- the plugin has zero stat
counters -- and "skipped correctly" renders identically to "skipped nothing", so
no visual check can separate them. The tests got the "coverage is a number, not
a boolean" discipline this session; the game never did.

CODEX-TASK-001-tile-skip-stats.md specs a stat VoxelForge group with
TilesClassified / TilesSkippedAllSolid / TilesSkippedAllAir / TilesMeshed. Solid
and air are split deliberately: cave archetypes prove AllSolid, so that counter
is the one that says whether the op-stack work did anything real. Its deliverable
is the before/after that constitutes the PRODUCTION proof of T1.d, which does not
exist today.

The spec carries the invariants rather than just the task, which is the point of
a spec here: bTrivialEmpty decides whether a tile has COLLISION, the five-clause
gate is load-bearing, and GenerateTileResult runs on worker threads -- so a plain
static int32++ is a data race while INC_DWORD_STAT is not.

Handoff updated: a "How we work now" section (Codex Model Luna xHigh writes most
code, Claude orchestrates -- hand over the INVARIANT, review against the code and
not the description), first actions split into the Codex task and the pending
e002bd4 build, and the T1.d heading qualified as harness-measured rather than
game-proven. Also records the Insights interim answer and its trap: ~84% of tiles
were already rejected by the hand-written surface/bedrock paths, so surface skips
drown the cave ones unless you are underground in an opted-in strate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:36:29 +02:00
Fr0zkaandClaude Opus 5 e4d1fdd7ea docs(opstack): experimental is pushed and tracked -- correct the flat "never push" rule
The rule said "Never push" without qualification, which was true while
experimental existed only locally and became misleading the moment it did not:
a future session would read it and let origin/experimental drift.

Now: push experimental freely (tracked since 2026-07-29), never push main, which
stays pinned at the known-good commit. Both statements of the rule are updated
(OPSTACK-HANDOFF.md and OPSTACK-PROMPT.md's crash-safe discipline).

Also added at both sites, because it is the failure mode this creates: a pushed
commit is NOT a "verified green" marker. This branch carries unbuilt work by
design, so OPSTACK-PROGRESS.md remains the only record of what was actually
built and measured, and the remote records only what was written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:41:00 +02:00
Fr0zkaandClaude Opus 5 41ba3c34a9 docs(opstack): rewrite the handoff for the T1.d-delivered state
The old handoff's "first action" (confirm the Underwater coverage number) and its
"one task everything is waiting on" (make FRoomGraphSource::EffectOverBox answer
spatially) are both done, so most of it was actively misleading.

What it says now:

- T1.d is real and measured: 11 of 40 tiles proved AllSolid at production
  defaults, 14641 voxels brute-forced, 0 violations; dense fixture correctly 0.
- A boxed invariant near the top, because it is the most dangerous thing in the
  current code: Identity from the room source means Sdf >= T, not FLT_MAX, and
  any new Sdf consumer with a bigger threshold silently produces tiles with no
  geometry and no collision.
- First action is now "build e002bd4 and read ONE line" (VerticalShafts box
  verdicts), including what to check first if it is still 0 -- ExtraReach --
  rather than re-deriving, which is what cost three rounds on TunnelNetwork.
- Both old debts restated with their real status: the per-room-op bound is
  DORMANT (checked, premise reversed), and AUDIT C2 is FIXED on the switch path
  with a note on why the audit's own suggested alternative was the wrong fix.
- The warp squeeze is parked WITH its measured ceiling and a recorded negative
  result, so the obvious next attempt (local Lipschitz warp bound) is not
  repeated: 8.5 * 0.206 = 1.75 exceeds the global range bound of 1.5.

New method lessons, led by the one this session actually paid for: instrument
what you ASSUMED, not just what you changed. Also that a sampler must cover a
period of what it samples, that a fixture tuned for coverage can be antagonistic
to what you are measuring, that diagnostics must report this run rather than
carry narrative, and that you must not assert a number you intend to improve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:36:36 +02:00