Skip to content

M1.1.12 — Forge 3D: the kinematic character controller - #68

Merged
guysenpai merged 100 commits into
mainfrom
phase-1/forge/character-controller
Aug 10, 2026
Merged

M1.1.12 — Forge 3D: the kinematic character controller#68
guysenpai merged 100 commits into
mainfrom
phase-1/forge/character-controller

Conversation

@guysenpai

@guysenpai guysenpai commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

M1.1.12 — Forge 3D: the kinematic character controller

Fourteenth M1.1 sub-milestone, and the first that is neither a shape nor a solver pass. Seven gates,
each pushed and reviewed before the next opened. Base: main at a4354df (v0.11.11-mesh-shape).

Planned tag v0.11.12-character-controller. Not merged and not tagged here — both are Guy's.

What it is

A virtual character controller carrying no simulated body: it does not participate in the solve,
is not integrated, and is not tracked by the island partition. Settled on the reference, whose
CharacterVirtual has no rigid body.

But it does carry a broadphase presence — an inner body, a .kinematic body holding the
controller's own capsule, mandatory on the module and defaulting to on per character. The argument
is internal to the frozen surface: CharacterDescriptor has carried collision_layer since its
original version, and in Weld's query family the object layer is how an object declares itself
visible to other callers' queries (§1.11.5) — so either the character has a presence, or that field
has no observable effect. That inverts the reference's default, because the failure mode of
default-off is a character nobody can query, found late.

  • moveCharacter — sweep, slide along planes and creases, climb, stick to the floor on descent,
    depenetrate, push what it touches, and wake it
  • resizeCharacter — atomic, feet-anchored, keeps the presence BodyId, three outcomes
  • setCharacterPosition — teleport without resolving, verdict invalidated to .in_air
  • getCharacterInnerBody / destroyCharacter / the ground verdict
  • collideShapeBody — the seventh body-level adapter, the manifold fallback the sweep needs when
    its normal is unusable

Decisions worth reading before the diff

Position is the BASE of the capsule, never the centre of its shape, and the offset lives in
exactly one named place. The reference parameterises that anchor through mShapeOffset; Weld fixes
it. The base/centre distinction was the source of three separate defects in this milestone.

Ground determination is a bounded downward sweep, not manifolds at the current pose.
collideOrdered returns null on a separated pair and a resting character stands padding above its
floor, so a manifold-only reading answers .in_air for a standing character.

The slide is constrained by slope, and §1.12 did not say so: measured, a character climbed any
face up to 90°−ε by walking into it — 0.583 m of rise in one call against a 50° face under a 45°
limit, with the verdict correctly reporting .on_steep_ground throughout. The rule caps the
projected motion's up component at max(up_before, 0) and not at up_before; capping at
up_before drives into the plane on an inclined face and annuls the physically correct answer, a
body sliding down a 50° slope descending more slowly than in free fall.

Depenetration pushes out and never through. It reverts to the entry pose the moment it finds a
contact whose plane the base has crossed since entry. Without it, the exit side of an unresolvable
squeeze was the parity of max_depenetration_iterations: at 3 and 5 the base landed 0.800000 below
the ground plane, and nothing in the suite moved at any value — an odd count would have shipped in
silence.

PackedId.dead reserves the all-ones no-handle bit pattern. ground_body's default of 0 was a
live handle to slot 0 generation 0, so no bit pattern of that field meant absence — a coupling the C
ABI cannot express.

The discriminant for an error channel is whether an entry returns a value, not whether it writes.
setCharacterPosition was made fallible mid-milestone and reverted on that rule.

It ships one known, measured, pinned, UNFIXED functional defect

A character whose base is placed exactly tangent to a surface serves no horizontal motion —
permanently, and silently. At exact tangency the sweep reports a contact at distance zero,
paddedAdvance returns max(0, 0 − padding) = 0, and the slide returns a horizontal motion projected
on a horizontal plane unchanged; all four slide iterations are consumed and the remainder is dropped.

Recorded here rather than left for a reader to find. Three reasons it ships, and none is "it is small":

  • Not reachable from play — measured, every move leaves the character padding above its floor, so
    the state does not reproduce itself. Only authoring reaches it, position = (0,0,0) over a floor at
    y = 0 being the natural thing to write.
  • Pinned at seven heights in both directions — only exactly zero fails; 0.005 through 0.05 serve
    in full. The fix is testable in advance and the regression is guarded.
  • The deferral forces no refactoringsweepNearest is private to character.zig, no published
    surface sees it, its five call sites are in that one file.

Mitigated by a documented precondition on CharacterDescriptor.position. Two candidate fixes and two
already-refuted forms are recorded in CLAUDE.md, owned by the next milestone that opens
character.zig. The refuted ones were closed by measurement, not argument: not counting the iteration
leaves a bit-identical state, hence an infinite loop; serving the remainder tunnels, measured through a
wall whose face stands at 0.8, since the sweep reports only the plane at distance zero.

Two rounds of external review after the internal gates closed

Seven findings, every one verified against source before being acted on, every one confirmed. The push
applied before a publication that can still fail. The broadphase proxy outliving the character, whose
own comment counted three released resources where there are four. ground_velocity fed the
penetration midpoint instead of the body's surface, on a path no test reached. step_height
unvalidated where every other stored physical parameter is. setShape maintaining two of its four
consequences. The union-published proxy box, wrong twice — permanently fat after a teleport, and
guarding a failure mode that does not exist since Broadphase.update already reserves before mutating.
And, in the second round, the deferral of the push having doubled the force ceiling: a ceiling one can
exceed by being touched twice is not a ceiling, closed by summing per body and capping the sum once.

Seven rounds of external review, and the last four were spent on the wrong level

The tangency defect — a character whose base sits exactly on its floor serving no motion — was closed
four times and reappeared four times, because each fix closed one ARRIVAL PATH and arrival at tangency is
a float-resolution phenomenon with as many paths as one likes. What is finite is the CONSEQUENCE, in one
place: a zero advance against a surface that does not oppose the travel direction obstructs nothing, yet
burns the slide budget and drops the remainder. That contact is now set aside for one retry that does not
spend the budget — 48 lines, no epsilon, bounded at one slot.

The round-2 estimate that this needed threading through five call sites was made without reading
SweepCollector, which already carried an exclusion. Same defect on both sides of the review: prescribing
from an imagined mechanism instead of a read one.

What it ships open, declared rather than discovered

An f32 collider-extent envelope, with its real cause and not "large collider": at a 1 km box
slideNormal returns null and the slide takes its documented stop rather than guess a direction exit.
Serving it would mean inventing a direction the narrowphase declines to supply. Measured boundary — a
scale, not a distance, since gjk.zig's coordScale is relative: 100 m half-extent serves every call,
300 m stalls on the third, 500 m and above from the second, and there is no stall at 50 km from the
origin. f64 is clean to 2 km. It needs a spec addition, not a code change: §1.11.4 bis characterises the
far field by distance alone and carries no extent axis.

Eight green assertions proved nothing, and the eighth exposed a blind spot

Seven were found by the probe table — disable a mechanism, require a test to fail. The eighth was found
by external review, and could not have been found by the table: it exercised no mechanism at all. The
counter-measure is a MUTATION probe — restore the previous implementation and require the test to fail —
which is how the scoped stand-off is now pinned, @max breaking a test where it previously broke none.

A domain table, and an exigence for the next descriptor

Every descriptor parameter at its legal bounds, measured and asserted. No gate of this milestone
enumerated those bounds and asked what the code does at each: gate F decided which values to REJECT and
never asked the other question, and both ends of padding fell into that hole. Cheap at the gate that
declares the fields, expensive afterwards.

Nine of this milestone's findings were in the measuring apparatus

Three tests that asserted nothing at gate E alone. A proxy-freshness test that passed with the proxy
update removed — a broadphase box is only a conservative filter, so a stale fat box the ray still
crosses yields the correct distance and the query is right for the wrong reason. A bit-exact
comparison passing the same literal at two precisions, hence comparing two different inputs. A
tie-break test whose insertion order made both rules agree. A closed form derived at 1.95 and
measured at 0.688. And three harness defects of one class: an exit code taken from a trailing
echo, a regex broken by an apostrophe, four probes reporting failure on compile errors.

Every one was found by disabling the mechanism, never by rereading the test. The standing
formulation: an assertion that exercises a path does not thereby test the mechanism that path uses.

Closure measurements

The eleven inherited envelope quantities, re-measured against main through a worktree with the same
probe compiled in both trees, both precisions: zero movement, digit for digit, on all eleven. All
seventeen inherited forge test files are byte-identical to the tag, solver_test.zig included.

The NGS energy-injection watch CLAUDE.md names this milestone to re-read is answered by
measurement: the retained speed is 5.0000005 at f32 — exactly one ULP above 5 — and exactly 5 at
f64. A solver that adds energy adds it at both precisions; one that rounds does not.

Bench reported, not gated, interleaved across eight reps (ReleaseFast): plane 212.0 ns, stairs
2235.5, wall 1764.5, mesh floor 7979.0, resize 203.0 ns — the worst still leaving 2089 calls per
16.67 ms frame.

Validation

Corner Result
test-forge-3d, Debug, f32 / f64 484/484 ×2
test-forge-3d, ReleaseSafe, f32 / f64 484/484 ×2
zig build test, Debug / ReleaseSafe 264/264 steps, 1656/1673 (17 skipped) ×2

zig fmt --check src/ bench/ tests/ clean; tree-wide zig build lint exit 0. Zero French in code or
brief prose, audited in Python (a byte-wise grep class silently matches nothing here).

Ten consignations, each with its owner, in the brief's closing notes — four of them added to
CLAUDE.md's open decisions.

🤖 Generated with Claude Code

The slide's projection is capped on `up` at its pre-projection component whenever the contact
plane fails the slope test (§1.12.6), on the plane branch AND on the crease branch. Without it a
character climbed any face up to 90°−ε by walking into it: 0.583 m of rise in one call against a
50° face under a 45° limit, with the verdict correctly reporting `.on_steep_ground` throughout.
The crease needed the same cap, measured at 0.0186 m per call through a rotated box offering two
normals, and the probe table shows the two branches are not redundant.

`resizeCharacter` is atomic and feet-anchored: build, test the target volume with the character's
own presence excluded, then commit. A refusal changes nothing. The presence's shape is swapped
before the proxy sync so the broadphase box reflects the new size, not only the new centre.

`pushBody` is unilateral and force-limited; `setCharacterPosition` teleports without resolving and
invalidates the reported verdict to `.in_air`. Its stale-handle answer is a typed error like every
other entry of the store: a destroy is idempotent, so ignoring a dead handle is its natural
answer, but a teleport that goes nowhere is a write the caller has to know did not happen.
Ten blocks. The slope constraint in four cases, one of which carries a correction: the rule's case 3
does not hold as stated for a steep but SLOPED face — a pure gravity step against a 50° normal
projects to an up component GREATER than the one it came from, so the cap fires and restores the
requested rate. The prediction that a descent is already below and hence untouched holds only for a
VERTICAL face. Computed before the test was written.

The unguarded squeeze mode is measured rather than guarded: a capsule needing 1.8 m under a ceiling
offering 1.0 m is PINNED at base exactly 0, bit-identical on the second call, and never driven
through the floor. That direction is safe only because `max_depenetration_iterations` is EVEN — at 3
and at 5 the base lands 0.8 m below the ground plane, and no test would have caught it — so the
parity is pinned with its measurement.

The step's FORWARD sweep now pins its padding stand-off at 1.93. The counterfactual was derived as a
lost stand-off and the measurement refuted that: dropping it gives 0.688, because the unpadded
advance leaves the capsule flush against the wall, the landing sweep then reports that wall at
distance zero instead of the plateau below, and the whole climb is refused. The cost is not 0.02 m of
stand-off but 1.24 m of a legitimate move never served. The LIFT's padding is unobservable and that
is confirmed by probe, not argued: it cancels against the landing drop in every geometry.
`syncPresence` updated the broadphase proxy AFTER the record had been written, on all three write
paths. `Broadphase.update` reserves a slot on its layer's moved log, so it allocates and can fail:
on the two pose paths the record and the body had moved while the proxy had not, so the stored box
no longer contained the body; in `resizeCharacter` the record and the presence body already pointed
at the new shape, which the `errdefer` then destroyed.

The entry now takes its TARGET instead of reading the record, so the one fallible call precedes
every mutation, and publishes the UNION of the old and new boxes so a failure leaves a proxy that
still contains the body. The commit that follows is infallible. Reserve-then-mutate, M1.1.1-HF1
D3/D4.

The resize's other fallible step is interceptable and a FailingAllocator test pins the full claim
there. The `bp.update` reservation could NOT be made to fail from a test — measured: a 5 m move on a
fresh character reports zero allocations seen, 40 teleports of 5 m report exactly one, and the
reversed ordering breaks nothing at exit 0. So the ordering rests on a structural argument, and the
use-after-free it forecloses is recorded as a latent hazard rather than claimed as demonstrated.
Three corrections after the gate F verdict.

The slope cap's bound was the pre-projection `up` component and had to be `max(before, 0)`. Capping
at `before` PENETRATES the plane on a sloped face: on a 50° normal a pure gravity step projects to an
up component of −0.5866, which exceeds the −1 it came from, so the cap bit and the output had a dot of
−0.2657 with the normal. And the projection was the physically right answer — a body sliding down a
50° slope descends more slowly than in free fall. Measured at contact, the old bound removed the
motion entirely: dy = 0.00000, the character frozen on the cliff. Same correction on the crease
branch, where no scene of five separated the two bounds, recorded as undiscriminated rather than
proven.

`depenetrate` carries an invariant instead of relying on its iteration count's parity: it never moves
the character to the far side of a contact plane it was on the good side of at the entry of the call.
The base and not the centre, because the centre of a 1.8 m capsule is still above a plane its feet
passed 0.80 m below. Failure direction, now sayable: an unresolvable squeeze keeps its entry pose with
a residual overlap and does not tunnel — verified identical at counts of 3, 4, 5 and 8.

`setCharacterPosition` returns to a no-op on a dead handle. The discriminant is whether the entry
RETURNS A VALUE, not writing versus destroying: the four entries that return something have no honest
answer for a dead handle, while this one and `destroyCharacter` return nothing. The setter-fallibility
convention goes to M1.1.15, where it covers the whole surface at once.
Two blocks, each the direction whose absence let a defect ship.

Gravity on a steep face: a 60° face so the trigonometry closes, the capsule's surface starting 0.12 m
clear, a pure 0.5 m downward request. The padding comes off the TRAVEL and not off the normal — the
first derivation here got that wrong, the same class of error already corrected at gate D — so contact
is at 0.24, the padded advance 0.22, the remaining 0.28 projects, and dy = −0.43 with
dx = −0.28·(√3/4). Both to six digits. Discriminating: capping at `before` gives −0.281645 and
−0.026694, and at exact contact on a 50° face it gives no motion at all.

A doorway narrower than the character: three widths against a 0.60 m capsule, the resolved offset
exactly `radius − width/2`, always inside the doorway, base unmoved and the request unserved. NOT a
second instance of the ceiling's tunnelling and that was measured — the walls are symmetric about the
entry pose so the alternation stays bounded, identical at counts of 3, 4 and 5 because it is per call
and not per iteration.

The squeeze test now expects the entry base of 0.02 rather than a flush 0, which is the invariant's
stated failure direction, and the parity assertion is demoted to a record that carries no correction.
The active-edge flags baked at M1.1.11.1 are NOT unconsumed: internalEdgeNormal
consults them and is called on the contact path. What bypassed it was the
character's cast path, which calls collideOrdered directly — so a capsule deep
under a flat quad received the normal of the quad's INTERNAL DIAGONAL, measured
horizontal and opposite on the two triangles sharing it, and verified by
arithmetic against that edge's horizontal perpendicular. The slide read the pair
as a crease, a third plane as a corner, and stopped.

The edge is recovered from the contact POINT's distance to the three segments,
which is what internalEdgeNormal already does. feature_id cannot serve: decoded
on the faulty contact it carries a reference face on the SEGMENT (per-core-type
ids, segment 6 and triangle 8) and incident winding VERTICES, class_edge never
fires, and half the faulty contacts are single-point manifolds with two FACE
halves and no edge information at all. So this is a reuse and not a mechanism.

It closes ONE of the three residual cells and not the others: at f32, 26 of 28
converge and the two left are 0.506016 at 90 deg base 0.05 and 0.459151 at
315 deg base 0.05 — both at the lifted base where the closed one was the tangent
base. At f64 nothing moved, the correction's noise gate being far tighter there.

So the convergence condition is NOT met: no common expectation is pinned, the
no-freeze property stays the pin, and the consignation stands.
Rewritten because the entry now describes something else: two thirds of the
yaw dependence are closed and their parts are named. The severe half went with
the retained contact's normal reaching the slide; one further cause was the
internal diagonal of a flat quad returning its own normal in deep overlap,
horizontal and anti-parallel between the two triangles, which the plane
accumulator read as a sharp edge. The remedy was already in the module and
merely bypassed.

What remains is unnamed, and the entry keeps the one quantitative constraint we
have on it: f64 does not move a digit where f32 gains a cell, and its noise
guard is tighter by 2^29, so the cause sits above that guard at f32 and below it
at f64. Both previously named candidates are recorded as traced and eliminated
rather than merely doubted.
The magnitude bound does not discriminate it: that cell read 0.459 before the
correction reached the character's cast path and ~1.700 after, and along > 0.1
accepts both, so removing the internalEdgeNormal call outright left the suite
green. A bound is the right shape for the no-freeze property and the wrong one
for a fix whose whole effect is WHICH value is served.

Pinned to the measured stop at both precisions, 1.699976100 at f32 and
1.699999988 at f64. The 2.4e-5 gap between them IS the stand-off floor,
64 * floatEps(Real) * coordScale, which vanishes at f64 — so the split is by
precision with a named cause and not by platform.

Mutation probe against 36c6866: fails with actual 0.45921806 against expected
1.6999761.
A horizontal triangle's normal does not survive a quaternion yaw exactly:
transported through 90 degrees it reads (7.4e-16, 1, 1.1e-15) at f64 and
(3.97e-7, 1, 3.97e-7) at f32. Its dot with a horizontal sweep is then a small
NEGATIVE residue where the geometry has an exact zero, so an exact >= 0 test
admitted a FLOOR as an obstacle to horizontal travel. Advance zero, iteration
spent, plane slot spent; the slide against that near-vertical normal injected a
vertical residue into the motion, which admitted the CEILING the same way; and
two near-anti-parallel planes formed a crease whose axis is their cross product,
which is pure noise. At f64 that axis re-found the floor and the character froze
at exactly zero every call, at f32 it happened to find a real contact. A coin
toss arbitrated by rounding.

opposing_noise_k = 16, as k * floatEps(Real) and nothing else: n . d is a
product of two UNIT vectors, hence dimensionless, so no coordinate scale belongs
in it — the difference from contactMargin, whose operand is a distance. The band
reads NON-OPPOSING and discards: discarding a contact that opposed by a hair
advances by a hair into a surface and the next call's depenetration recovers it,
while reading it as opposing freezes, and a freeze recovers from nothing.
Measured residues are 3.3 and 5 floatEps, so k = 16 is a 3x margin, and the
angle it costs is asin(16 * floatEps) — 1.1e-4 degrees at f32. Governed by
1.11.2, unlike max_slope, padding and predictive_contact_distance. One predicate
shared by all four arms so they cannot drift.

All twenty-eight cells now converge at BOTH precisions: f32 in
[1.699976300, 1.699986600], f64 in [1.699999905, 1.700000054]. So the common
expectation replaces the magnitude bound, the Real == f32 guard is gone, the
title is true, and the fifth open decision is deleted rather than rewritten.
Mutation probe against 4438a4e: actual -0 against expected 1.7.
The kernels normalise the sweep direction themselves, so a caller is entitled to
hand this path a direction of any length — but `opposes` compared `n . d`
against an ABSOLUTE threshold, so the same geometry with the direction handed
over twice as long changed sides. No geometry justifies that; it was introduced
with the noise band one commit ago.

The threshold is now scaled by the direction's length, which is exactly
normalising it without paying a division, and it is guarded at TRUE zero by
construction: at zero length the dot is zero too and `0 < 0` is false, so a null
direction opposes nothing and no epsilon is invented. `n` is unit at all four
call sites by the narrowphase's own contract, so only `d` needed the scaling.

489/489 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The band closed all twenty-eight cells of the insoluble-squeeze grid at both
precisions and still had to go: what it bought in an authoring-fault
configuration it paid for on the DEFAULT path. Discarding a grazing contact lets
the capsule advance INTO the surface by distance x |n.d| — measured 3.05e-5 over
32 m and unbounded in the distance. A traversable geometry is worse than a
partial serve.

And no band on that quantity can work, which is the durable result of the thread
and is written at the site so nobody retries it. The two populations are a
TRANSPORT RESIDUE tracking floatEps(Real) — it is the rounding of a quaternion
rotation — and a REAL GRAZING INCIDENCE that is geometric and precision-free.
Nine orders of separation at f64, a factor of 2.4 at f32, so the inseparability
is structurally an f32 phenomenon. The length-dimensioned form escapes nothing:
coordScale ~ distance makes it this test with both sides multiplied by the
distance.

The predicate returns to an exact zero. P2's norm-independence is KEPT but its
mechanism is now structural rather than arranged: scaling d by a positive factor
scales the dot by the same factor and cannot change its sign. The ||d|| term is
not merely inert at zero — 0 * inf is NaN and x < NaN is false — so writing it
would introduce a path where an infinite direction opposes nothing.

What comes back is PINNED and not suffered: 0.506016400 at f32 for yaw 90 from a
base of 0.05, and an exact zero at f64 for the same yaw from a tangent base,
which nothing watched before. Those two assertions are the pin of the revocation
itself and fail against a2e7493. The title stops claiming the precisions agree,
because they do not.
The fifth open decision returns, carrying what the previous version could not:
the named cause, the remedy that was tried and revoked, and the algebraic
demonstration that no band on n.d separates a transport residue from a real
grazing incidence at f32.

And a tooling fact the revocation itself produced: 94ee517 carried a remedy and
every expectation that remedy made possible in one commit, so undoing it could
not be a revert — it was a hand reconstruction of three expectations, two
re-measured cells and a re-authored consignation. Third instance of the class in
this milestone. The cost is invisible going forward and paid going backward,
which is when one is least willing to pay it.

Separated from the revocation for exactly that reason.
The predicate conditioned the direction itself instead of taking what the kernel
already computes, and got it wrong twice from that one root: an absolute
threshold made the verdict depend on the direction's length, then a bare sign
test UNDERFLOWED. For a denormal direction a product n_i * d_i with |n_i| < 0.5
flushes to exactly zero, the sign is destroyed, and a real wall reads
non-opposing and is discarded — contradicting the contract shapecast_test.zig
already pins, that every non-zero direction including a denormal is served.

unitOf becomes pub and is re-exported by the narrowphase facade. It reduces by
the largest absolute component before normalising, so it neither squares a
denormal into zero nor overflows a huge one, and returns null at exact zero and
nowhere else. There are no longer two answers to what direction the cast travels.

The regression is at the BODY level, which is the gap that let both forms
through: the kernel's own denormal test passed the whole time because it never
goes near this predicate.

Scope measured, not assumed. Triangle soup is excluded because the PLAIN cast
returns null there too, so the two agree and the filter is not implicated — the
direction is lost upstream in the swept traversal. Half-space is excluded
because plane.zig asserts the direction is unit where shapecast.zig conditions
it: two kernels, one parameter, two contracts, with the entry normalising once.
Both are recorded rather than papered over.

The tilt is measured, not derived: an axis-aligned wall proves nothing, since
-1 * floatTrueMin is representable, and a first version of this test passed
against the pre-fix commit. Swept over ten yaws the raw product survives at
20-50 and 70-85 degrees and is exactly zero at 60 and 65, where the contact
normal's X component reads -0.4999997 and -0.4226. 65 is taken, and the test
fails at BOTH precisions against f0ba370.

490/490 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
castShapeBody had a contract per SHAPE CLASS that nobody had written down, which
is why the same defect arrived one cell at a time for four rounds. Measured over
three classes and six properties at both precisions, four cells diverged and all
four were the DIRECTION: plane.zig asserted the vector was unit and conditioned
nothing, so a non-unit, denormal or zero direction was a domain violation on
that arm and an ordinary answer on the other two; and the mesh arm additionally
lost a denormal in its swept traversal. Conditioning once at the entry took all
four together, the mesh traversal included.

The conditioning skips a direction that is ALREADY unit, and that is not an
optimisation. A first form normalised unconditionally and shifted every query
answer by an ULP, caught by mesh_test's bit-exact brute-force oracle: the query
entry already normalises, so an unconditional line here was a SECOND
conditioning of one vector — the class this pass exists to collapse,
re-introduced by the collapse. The gate is the module's existing unit predicate
and no answer depends on which branch is taken.

opposes now ASSERTS its precondition instead of re-establishing it, for the same
reason. And unitOf reverts to private: it duplicates Vec.normalizeScaled line
for line, so publishing it would have been a second answer as well.

Two cells were measured NOT to diverge and are recorded as such rather than
uniformised: hit.normal at an initial overlap differs in value because 1.11.11
fixes -direction as the FALLBACK and the half-space has no terminal simplex to
offer instead, while all three keep the guarantee the contract actually makes,
normal . direction <= 0; and the manifold is consulted on all three arms.

The parameterised test is what was missing from the start: one scene per class,
the same assertion for all three, so a future arm breaks it here rather than in
a character scene three milestones later. It aborts against 0dc9137 on
plane.zig:297.

491/491 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The pass had uniformised the three shape classes by the PERMISSIVE, making the
adapter condition its own direction. The map of every production caller says
nobody needs the permission: query/root.zig conditions once for the whole query
family and guards zero there, calling a zero direction a legal query with an
empty answer; the ground probe and the step sweeps pass the exact constants up
and up.neg(); the slide normalises its own remainder. So it uniformises by the
STRICT instead.

castShapeBody is an INTERNAL adapter of BodyManager, not a public entry:
requiring a unit direction of it is legitimate where requiring it of
query.shapeCast would not be, and the public contract does not move.

And conditioning at the entry was not free of the defect it closed. The query
entry already normalises, so an unconditional line was a SECOND conditioning of
one vector; skipping it inside a tolerance left the two arms disagreeing INSIDE
that tolerance, since plane.zig uses the raw norm in t = sep / -closing while
shapecast.zig renormalises. Measured, a conditioned direction lands 0 to 1 ULP
from unit, so that branch was taken on every query. An assert has no inside.

The controller's remaining.scale(1 / distance) becomes normalizeScaled, the same
shared form the query family uses. Stated plainly: nothing in the suite
discriminates that line today — the denormal end is masked by the len_sq == 0
guard and the huge end aborts earlier on a separate, pre-existing defect where
an infinite max_distance reaches shapecast.zig:193. It is an alignment guarded
by the new assert, not a fix with a pin.

Two table cells therefore move from served to precondition, and the property is
pinned one tier up where it IS a contract: a second parameterised test asserts
that query.shapeCast serves a non-unit and a denormal direction and answers null
on zero, on all three classes. That table pins a long-standing contract and does
not discriminate this change; the assert, running on every call in Debug and
ReleaseSafe across the suite, is what pins the change.

P3: three comments contradicted the code and are replaced rather than amended —
the opposes doc still described the entry conditioning, and C4 claimed the
manifold is consulted on all three arms where the half-space transports its
stored plane normal and consults nothing.

491/491 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The slide asked three separate questions of the same vector and got three
different domains. lengthSq() == 0 for emptiness UNDERFLOWS, so a denormal
remainder read as nothing and the displacement was silently dropped;
sqrt(lengthSq()) for the distance OVERFLOWS, so an infinite max_distance reached
the kernel and tripped its finiteness assert. The reduction by the largest
absolute component that normalizeScaled already performs internally has neither
failure, so Vec gains unitAndLength, which returns both quantities from that one
reduction and is null at exact zero — the emptiness test the caller needs,
without a threshold that could disagree with the direction of the same call.
normalizeScaled delegates to it, same operations in the same order.

A SECOND site of the same class in the same function: the step attempt derived
its own sqrt(remaining.lengthSq()), and that is the one the huge case actually
aborted on. Both now take the same reduction.

Pinned at both ends of the range, and the test aborts against 77e49c2 on
shapecast.zig:193, which is the infinite max_distance the second site produced.

492/492 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
An assert BOUNDS a direction, it does not canonicalise it. (1 + 4eps, 0, 0)
passes every unit assert in the module, and inside that band the two arms of
castShapeBody consumed different vectors: the convex kernel reconditioned its
copy through unitOf while plane.zig used the raw norm in t = sep / -closing.
Measured against ed3a8cf, one query answered 3.7 on one arm and 3.6999984 on the
other, at BOTH precisions.

Two forms could close it and the choice was MEASURED against the bit-exact
oracles, which is the only criterion that has decided correctly on this column.
Having the half-space condition like the convex breaks TWO plane tests, and they
are a CONTRACT: normal.eql(d.neg()) at an initial overlap is 1.11.11's identity
across the four kernels, and conditioning inside the plane kernel returns
-normalizeScaled(d) instead of -d. Having the kernel not recondition breaks ONE
test, and it is an ORACLE ARTEFACT. So: form one.

castShapeUnit is ADDITIVE. castShape and castShapeBounded keep conditioning,
keep their zero-direction guard and keep their signatures, so no existing caller
moves; the shared body becomes a private impl taking a comptime flag. Only
castShapeBody takes the new entry, its direction being a precondition it
asserts — and that assert stays, no longer a band masking a divergence but the
precondition of an entry that reconditions nothing behind it.

The mesh_test line is an ORACLE CORRECTION and it is written at the site: the
test measures that the accelerated path agrees with brute force, and two sides
calling different kernel entries compare two things instead of one. Aligned, it
measures the acceleration again.

Codex's reproducer is pinned, bit-exactly and not approximately — the defect is
one ULP and any comfortable tolerance would hide it — with a bound placed
strictly between the two answers a divergent pair gives, where one ULP separates
hit from miss. It fails against ed3a8cf at both precisions.

493/493 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
unitAndLength protected the intermediate square and not the final product: two
components at 0.75 * floatMax have a norm near 1.06 * floatMax, which no float
here holds, so largest * |reduced| left the range and the function answered inf.
That inf became a sweep bound and tripped a kernel's finiteness assert one call
later. Treated AT THE SITE that produces the value, as asked: length becomes
optional, because a DIRECTION always exists where a LENGTH need not, and the
direction is scale-free and unaffected. Not an assumption each caller re-derives.

The controller TREATS it rather than leaving it consigned, so the consignation
shrinks by that much: an unrepresentable norm is still a request, served to
floatMax, which is the largest distance this arithmetic can express — the
world's colliders bound the sweep long before it, and a caller asking for more
is asking for more than a POSITION can represent either. Third site of the class
swept two rounds ago, and one tier below the two others.

The denormal assertion was VACUOUS: x >= 0 also passes for the old behaviour,
where the displacement was dropped and x was exactly zero. It is bit-exactly the
displacement at both precisions and now says so — an inequality where an exact
equality was available admitted the defect it was written against.

And the mutation probe did not catch that, which is the round's method finding:
it had been run, and it FIRED — on the huge case, whose abort ends the body
before any later case runs. A probe on a multi-case body proves that AT LEAST
ONE case discriminates, never that each does. The three cases are now three
tests, each with its own verdict, and each fails against the commit that
precedes its own fix.

Exposed and NOT chased, because it is a different limit: at 0.75 * floatMax from
the origin a broadphase node's (min + max) * 0.5 overflows, so the ray origin it
derives is infinite and a different assert fires. The test closes its scene with
walls to keep the question on unitAndLength.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The saturation was the module's only exception to its own written rule, and the
rule sits three lines above the error set it belongs to: silently clamping makes
a caller's mistake look like a modelling choice and leaves no diagnostic.
Serving an unrepresentable norm would also mean segmenting the move, turning
this entry into a multi-segment integrator for a request no POSITION can
represent — 0.75 * floatMax is outside any expressible world, and the
broadphase's own node arithmetic overflows there, which is why the test scene
has to close its walls near the origin.

So CharacterError gains InvalidDisplacement and moveCharacter uses the error
channel gate A gave it. THE CALL PARAMETER NOW HAS A DOMAIN: finite components
and a representable norm, checked at the entry like every descriptor guard, and
written at the site. Every other guard in this module belongs to the descriptor
and is checked once at creation; displacement is checked per call and was
checked nowhere, which is what let an infinite bound reach the sweep.

Both open ends of that domain stay open and are asserted: EXACT zero is a legal
no-op, and a DENORMAL displacement is still served — the norm is unrepresentable
only by overflow, never by underflow, the direction being scale-free.

Downstream the length is representable by INVARIANT rather than by hope: the
entry refused otherwise and remaining only shrinks, both slide forms being
projections, so the two sites take it with a checked unwrap that states it.

Recorded rather than assumed: I did not reproduce a traversed plane from the
saturation in my own scene, which ends at (1.686, 0.02, 2.18), inside its walls.
What I did find is that the assertions I had written there were ONE-SIDED — x < 2
and z < 3 — so they could not have detected a traversal in the other direction
anyway. The refusal removes the path either way.

And there was no consignation in the repo to withdraw: that class lived in the
conversation and was never written into the brief or CLAUDE.md.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe. The test
aborts against 2413585, where the same call saturated instead of refusing.
The domain tested representability in Real, which made it a function of a BUILD
FLAG: the same public vector was refused at f32 and accepted under
-Dphysics_f64, where its norm is an ordinary number. The interface takes a Vec3
in f32, so the discriminant is the quantity's ORIGIN and not the precision that
handles it afterwards — the tolerance class this milestone made normative,
applied to a DOMAIN instead of to a comparison. The bound is floatMax(f32),
evaluated at the boundary, and the same vector is now refused at both.

The test passes ONE input at both precisions instead of two, which is what the
Real-scaled literal was hiding. And the HUGE test was the same defect: it read
, so the two builds compared two things. It
now uses 1e30 at both, and states plainly that the mechanism it exercises is
live at f32 alone — 1e60 overflows there and is an ordinary number in f64 — so
the guard is load-bearing at one precision and inert at the other, rather than
dressing the f64 leg up as testing the same thing.

moveCharacter's error documentation carries InvalidDisplacement with its reason.

Mutation probe: against e92b476 the f64 leg accepts what f32 refuses, and the
test fails there.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Both control witnesses verified on the text before touching anything, not on
the files being present. engine-tier-interfaces.md carries "Le canal porte AUSSI
le domaine du DEPLACEMENT", the f32 boundary evaluated before widening, and the
prohibition on saturating with its measure — a saturated bound leaves the TAIL
of the displacement unswept, so a plane sitting there is traversed.
engine-physics-forge.md 1.12.6 carries "le premier parametre d'APPEL du module a
en avoir un" and why the domain table missed it: it tabulated fields, not
parameters.

The site text predated both and therefore paraphrased what is now normative
elsewhere. Removed rather than amended: a second formulation of a normative rule
is a second source, which is the rule of two sources applied to prose. What
stays is what the code does and what only the site knows — the two open ends of
the domain, and the two distinct refusals that share it.

That measure also reconciles a disagreement I reported: I could not reproduce a
traversed plane from the saturation in my own scene, and the reason is now
named — the traversal comes from the unswept tail, and my scene's walls sat
inside the saturated bound, so it could not exhibit it.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The list on moveCharacter still announced StaleCharacter and the allocation
failures alone, which the previous patch was meant to correct and did not. It
points at 1.12.6 for the domain rather than restating it.

P1 is NOT in this commit: the prescription and its stated outcome are not
compatible, measured, and the fork is reported instead of picked.
The bound was floatMax(f32) and the norm was still computed at Real, so the
domain went on depending on the build through the ROUNDING instead of through
the bound. Codex's vector shows it: (floatMax(f32), 0.0002 * floatMax(f32), 0)
was refused at f64 and accepted at f32.

Evaluating in f32 does not fix it, and that is measured rather than argued: the
reduced square 1 + 4e-8 falls below the half-ULP of 1.0 in f32, rounds to
exactly 1.0, and the norm comes back as exactly floatMax(f32) — f32 cannot see
its own overflow, so it would accept a vector whose true norm leaves the range.
The prescription and its stated outcome were not compatible, which is why the
fork was reported instead of picked.

So the contract is stated in its own terms: THE DOMAIN IS THE f32 RANGE, AND IT
IS EVALUATED IN AN ARITHMETIC THAT CAN DECIDE THAT RANGE WITHOUT ROUNDING. The
width is the consequence and not the rule. Widening the components is EXACT, so
the verdict is a pure function of the public input and identical under either
-Dphysics_f64 setting. The declaration cites 1.11.4 bis, which measured the same
phenomenon on the ray normals — the information is present in the f32 inputs and
only a wider arithmetic extracts it — so a reader does not read the width as a
precision picked at random.

No reduction by the largest component is needed at this one site, and the reason
is written there: the widening makes the square safe for every f32-origin input,
and a square that overflows f64 can only come from a component already outside
the f32 range.

The test passes that exact vector at both precisions, formed in f32 and widened.
Mutation probe against 8cab4b0: it FAILS at f32 and passes at f64, which is the
asymmetry this removes.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Both witnesses verified on the text: engine-physics-forge.md 1.12.6 carries "La
regle dit quel domaine s'applique, pas quelle arithmetique l'evalue", with the
reproducer as its measurement and the 1.11.4 bis reference; the moveCharacter
entry carries "LE DOMAINE EST LA PLAGE f32, et il est EVALUE dans une
arithmetique capable de trancher cette plage SANS ARRONDI".

Reading 1.12.6 in full, it now also owns the two ends the domain leaves served —
exact zero and the denormal — which the site still restated. So the declaration
was paraphrase end to end and collapses to a pointer. Sixteen lines removed
rather than amended.

What stays is what only the site knows: that no reduction by the largest
component is needed at this one place, and why. The guard comment also stops
presenting f64 as a local choice — it is the arithmetic 1.12.6 requires.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
floatTrueMin(Real) is an f32 denormal in one build and an f64 denormal in the
other, so the two legs compared two different vectors while the domain under
test belongs to the public entry. floatTrueMin(f32) widened, and the underflow
assertion conditional as in the HUGE case — the mechanism is live at f32 alone,
and saying so beats a second literal that would make the f64 leg look like it
tested the same thing.

Third instance of the class in this file. P2-1 is not in this commit: measured
exactly, its premise does not hold, and the measurement is reported first.
The one method lesson this round produced, and the only external finding of the
milestone that did not hold. A first oracle answered with hypot in f64 and
carried exactly the rounding it was judging, so it agreed with the guard and
reported nothing — which reads as "no defect" for the wrong reason. Rewritten
over exact rationals against the true f32 overflow boundary, the answer
inverted: the vector is IN domain and accepting it is correct, and the
exact-component predicate proposed in its place is worse in both directions.

P3 was already delivered in 30b900a: floatTrueMin(f32) widened, underflow
assertion conditional as in the HUGE case.
Both witnesses verified on the text: 1.12.6 carries "Une borne enoncable vaut
mieux qu'une borne exacte quand l'ecart entre les deux n'a aucun habitant", with
the true threshold 2^128 - 2^103 named and the band measured; the moveCharacter
entry carries "norme AU PLUS floatMax(f32)".

The site diverged on one real point and it was not a nuance: it said "a norm
outside the f32 range", which is wider than the bound the guard applies and
contradicts what the spec now establishes — the refused band is INSIDE the f32
range, every real below 2^128 - 2^103 rounding to a finite f32, and the bound is
deliberately conservative. Replaced by the bound itself, and three further
mentions of "the f32 range" and "representable" in the guard and the loop are
re-expressed against the bound for the same reason.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Witnesses verified on the text: 1.12.6 carries "norme CALCULEE au plus
floatMax(f32)" with "et non la norme mathematique", and "Le domaine se declare
donc sur ce qui est decidable"; the moveCharacter entry carries the same
computed-versus-mathematical distinction. The "aucun habitant" phrasing has
disappeared from both, counted.

Two divergences at the site, and the second was not on the list. The summary
line stated the bound as if it applied to the true norm — corrected to the
computed one, with the arithmetic named. And the guard comment claimed that
arithmetic "can decide that bound WITHOUT ROUNDING", which the new text
explicitly refutes: no fixed-width arithmetic decides the mathematical form
exactly, the squares of the widened components being exact while their sum is
not. That sentence would have contradicted the spec it points at, so it is
replaced rather than softened.

No code change. 495/495 green at f32 and -Dphysics_f64=true, Debug and
ReleaseSafe.
Absence verified as much as presence, which is what this round is about: 1.12.6
carries "Elargir deplace le seuil de l'arrondi, il ne le supprime pas", and
"sans arrondi" now has ZERO occurrences in either spec — counted, not assumed.
The repo is swept for the same residue and has none either.

The site diverged by carrying its own formulation of what the source now states
better: "no fixed-width arithmetic decides the mathematical form exactly" was a
second statement of a normative fact. Replaced by the source's framing at the
pointer, and the guard comment keeps only what the site owns — that f64 is the
arithmetic 1.12.6 fixes, not a choice made locally.

No code change. 495/495 green at f32 and -Dphysics_f64=true, Debug and
ReleaseSafe.
Witnesses verified, presence and absence both. 1.12.6 carries "L'expression est
NORMATIVE, parce qu'un domaine declare sur ce qui est calcule n'est defini que
si le calcul l'est" and the transcribed predicate block; the moveCharacter entry
carries "L'EXPRESSION est normative". And "sans arrondi" stays at zero in both
specs and in src, checked rather than assumed — the residue check is now part of
every spec alignment.

The suite tested only the refused side, so a finer predicate — hypot, a wider
arithmetic, a different summation order — would have refused (M, 1e-9*M, 0) and
the suite would have called that an improvement. That vector is now asserted
served. Its true norm is M * (1 + 5e-19), above floatMax(f32) and far below the
real overflow threshold, so it is a deliberate consequence of the computed form.

Its second component points DOWN, and the reason is written at the site: with it
pointing up, the vertical part carries the pose to 3.4e29 and the run aborts in
the BROADPHASE on the far-field limit this file already records. The sign leaves
the norm, hence the verdict, untouched.

The guard comment stops describing the expression now that 1.12.6 transcribes
it, and says instead what only the site can: the expression IS the domain, so a
line changed here changes what the engine accepts and changes 1.12.6 first.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Your arithmetic verified independently before writing. (M, 7e-9*M, 7e-9*M) is
accepted in x,y,z order and REFUSED if y and z are summed first: each square
alone falls under the half-ULP of M squared and is absorbed, their sum does not.
At 1e-9 both orders agree and at 9e-9 both refuse, so it sits in the band where
the absorption flips and nowhere else.

THE HYPOT CLAIM IS PROVEN RATHER THAN REMOVED, and the case that proves it is
new. You were right that the shipped vector does not distinguish hypot — both
accept it, measured. But a band exists where they part: the ordered sum loses
the term once a squared falls under 2^-53, while hypot only refuses once the
true norm exceeds M by half an ULP OF M, so for a in [8e-9, 1.05e-8] the sum
refuses and hypot accepts. (M, 9e-9*M, 0) is asserted refused. Your conditional
said to keep the claim if such a case exists, so it is kept and locked; if you
prefer the two-property framing, that one case reverts alone.

The comment now names the three properties and the one case that moves for each,
rather than listing mutations it does not lock.

The order case needed a -Z wall, and the reason is at the site: its third
component is otherwise unblocked and carries the pose to z = -2.4e30, where the
run aborts on the broadphase far-field limit — a different limit from the one
under test, and the same isolation the refused side already uses.

495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Two assertions removed. One pinned 0.506016400 at 90 deg / base 0.05 and was
refuted by ubuntu-24.04 / Debug reading 1.6999805; one pinned the f64 cell frozen
at exactly zero and was never contradicted only because NO CI corner runs
-Dphysics_f64 — verified absent from .github/ by grep, so its silence was not a
confirmation. Both were measurements of one machine dressed as properties of a
quantity the consignation itself calls decided by rounding.

The surviving property lost its Real == f32 guard, was RUN unguarded, and the
answer is that it fails at f64: yaw 90 / base 0 returns -0e0 here. So the scope
is now written as a measured FACT rather than carried by a condition — the claim
is made at f32, where four platform-and-mode combinations exercise it, and the
f64 failure is recorded in CLAUDE.md instead of hidden. The two sides do not have
the same standing and the comment says which.

CLAUDE.md gains what the red taught: the residue varies by ARCHITECTURE and by
COMPILATION MODE and not only by precision, which is one more constraint on its
unnamed cause. And a sixth open decision, because the inventory turned it up: the
f64 leg has never run anywhere but one machine, so every f64 claim of this
milestone rests on a single sample — not charged to M1.1.12, owner named as the
next milestone touching CI.

495/495 green at all four local corners, lint 0. The CI matrix is the only
instrument for this class and it is read next.
The row dated from 2026-08-05 and predated twenty rounds of closure: it named
neither castShapeBody's contract table nor the displacement domain, nor the four
tunnelling defects, nor the methods the milestone leaves. Replaced whole, and the
footer date follows.

One formatting necessity, not a content change: the absolute-value bars in
`distance × |n·d|` are escaped as `\|`, since a bare bar would end the table cell
— the same escaping the earlier rows use for `\|omega\|` and `\|Delta pos\|`.
@guysenpai
guysenpai merged commit cbb02b3 into main Aug 10, 2026
9 of 10 checks passed
@guysenpai
guysenpai deleted the phase-1/forge/character-controller branch August 10, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant