Skip to content

Phase 1 / Forge / forge_3d orchestration: PhysicsWorld and ECS Transform synchronisation - #72

Merged
guysenpai merged 48 commits into
mainfrom
phase-1/forge/physics-world-orchestration
Aug 24, 2026
Merged

Phase 1 / Forge / forge_3d orchestration: PhysicsWorld and ECS Transform synchronisation#72
guysenpai merged 48 commits into
mainfrom
phase-1/forge/physics-world-orchestration

Conversation

@guysenpai

Copy link
Copy Markdown
Contributor

Brief

briefs/m1.1.15-physics-world-orchestration.md

DRAFT, opened at the first gate as engine-development-workflow.md §4.4 requires: ci.yml fires on pull_request and not on a branch push, so a gate whose exit depends on a matrix cell is unreachable until the PR exists. Gate A's exit is exactly that — the eight committed determinism witnesses byte-identical on the twelve-cell matrix, and the four CHAIN witnesses are x86_64 artifacts that no local corner can verify.

No Witness-regen: trailer appears anywhere in this branch, and none will. A red witness here is a defect to isolate, not an act to declare.

Summary

forge_3d had every mechanism the arena needs and no owner for the tick that runs them. This milestone creates that owner: PhysicsWorld executes the eleven steps of engine-physics-solver.md §1.7 in their frozen order, wires the pieces the body store cannot wire from inside, and connects the solver to the ECS in both directions.

Gate progress

Gate State
APhysicsWorld and the tick delivered, awaiting the matrix reading
B — proxies and body lifetime pending
C — wake composition pending
D — ECS synchronisation pending
E — precision crossing, interface file, false premises pending
F — closure pending

Gate A — what landed

  • The eleven-step cycle MOVED out of tests/solver_test.zig — where its only composition lived, driven by ten suites — into forge_3d/world.zig. Call sequence, arguments and arithmetic unchanged: the reparenting changes who calls, never what is computed.
  • step() reads as the cycle: nine stage methods in order, plus the two code-free anchors as comments where they would run (3 read-only, 5 bis the empty composite seam of §1.7.3). Each stage method opens with its own enter(), which is what binds the recorded order to the executed one.
  • Step 10 bis is now UNCONDITIONAL. The harness gated it on a sensors_on flag defaulting to false; in production that reads "sensors silently do not work". SensorState.update takes a *const BodyManager, so the pass cannot alter a bit of body state and the change is witness-neutral by construction.
  • SolverStats.warm_start_injections — real injections, not loop turns. Documented as NOT part of what §1.8.2 reports; see the brief's Notes, and flagged there for your call.

Gate A — measurements

Determinism instrument, all four local corners green ({Debug, ReleaseSafe} × {f32, f64}, macOS aarch64): self-reproducible, the four discrete traces byte-identical to the committed witnesses, no divergence within K = 60. The four chain witnesses read REPORTED, not gated on this host because level 1 is intra-ISA — that is the matrix's half of the exit.

Counter-factuals, four RUN, and the fourth is a residual rather than a confirmation:

Probe Mutation Measured
A swap the calls of stages 10 and 10 bis 1 failed of 561 — the order test, that adjacency's only guard
B hoist the warm start out of the substep loop cadence test: expected 16, found 4, plus 6 physics tests
D swap the calls of stages 4 and 5 4 failed, 35 crashed — where order matters physically, dozens fire
C swap two stage BODIES, leave each enter() whole suite green — the residual, written into the code

Probe C is the honest limit: a record separated from its work is undetectable by any test here. What bounds it is structural — each enter() is the first statement of the stage method carrying its name — and that is a smaller claim than "the order is verified", stated as such in world.zig and in the test.

Counts, with their denominators. zig build test-forge-3d collects 561 on this branch (560 pass, 1 skip) against 555 on main at e02d27b, measured in a worktree, both macOS aarch64: +6, the six blocks added in tests/world_test.zig. Full suite 1875 collected. The dead-tests declared floor was re-derived from the SUITE (1875), never from the closure's own arithmetic.

One test assertion removed, declared in the brief's Recorded deviations: expect(s.world.sensors_on) in the determinism scenario had no flag left to read. It is replaced by two assertions obtained by different mechanisms — the scenario's trigger asserted on the body's ROLE, and the unconditional pass asserted on the STATE it produces, in tests/world_test.zig.

Local gates

  • zig build clean
  • zig build test — 1875 collected, 1856 pass, 19 skip
  • zig build test-forge-3d — 561 collected, 560 pass, 1 skip
  • zig fmt --check green over src/ tests/ bench/ tools/ build.zig
  • zig build lint green, dead-tests conservation reconciled at 1875
  • zig build forge-determinism green on the four local corners
  • the twelve-cell matrix — this PR's reason for existing at Gate A

guysenpai and others added 23 commits August 21, 2026 22:59
Moves the eleven-step cycle of `engine-physics-solver.md` §1.7 out of the
acceptance suite that held the only composition of it and into
`forge_3d/world.zig`. The call sequence, the arguments and the arithmetic are
the ones the eight committed witnesses were taken over: the reparenting changes
who calls, never what is computed.

Cycle ownership:
- `PhysicsWorld` holds the shape store, the body store, the broadphase, the
  warm-start cache, the island partition and the per-tick scratches
- `step()` reads as the cycle: nine stage methods in the frozen order, with the
  two code-free anchors (3 read-only, 5 bis empty) as comments where they run
- each stage method begins with its own `enter()`, so a stage cannot be moved
  without its record moving — a recorder wired at the call site would be blind
  to the one mutation it exists to catch
- `tests/solver_test.zig` keeps the name `World` as an alias, so the ten suites
  that drive a world all drive the same owner

Step 10 bis is UNCONDITIONAL:
- the harness gated it on a `sensors_on` flag defaulting to false, which for a
  production world means sensors silently do not work
- `SensorState.update` takes a `*const BodyManager`, so the pass cannot alter a
  bit of body state and making it unconditional leaves every witness identical

Telemetry:
- `SolverStats.warm_start_injections` counts constraint POINTS injected, summed
  over substeps — not part of what §1.8.2 reports, and the only place the
  application half of warm start is observable
- `applyWarmStartRange` returns the count it injected

Tests:
- `tests/world_test.zig`: the step order read as an order, the substep cadence
  with its paired one-substep negative, and step 10 bis on a world that was
  never told about sensors
- the declared per-platform test floor re-derived from the suite: 1869 -> 1875
F1 — probe C claimed "a record moved away from its work is undetectable by
any test here", which its own batch refutes: swapping two stage BODIES
executes the work in the same order as swapping their CALLS, so on a
consequential pair the physics guards fire identically. Measured, not
deduced — probe C' on (build_constraints, island_partition) gives 3 failed,
35 crashed with the order test passing, i.e. probe D minus the order test.
Both pairs are now named, the claim is bounded to the physically harmless
case, and what the test guards ALONE is stated: the 10 / 10 bis adjacency,
where probe A reports 1 failed of 561.

F2 — `warm_start_injections` moves into a named `not_reported` sub-struct of
`SolverStats`. §1.8.2's exclusion was carried by a doc comment, so it rested
on whoever later maps the struct onto the reported telemetry surface; it is
now carried by the type and visible at every read site. The counter stays
where the injection happens.

F3 — the order test is renamed to say what it measures: the nine CODED steps
of an eleven-anchor cycle, not eleven steps.
The counter-factual refuted its own prediction and had to be read further:
under the "tag before publishing" defect BOTH halves of the sleeping test
fail, because the unpublished final velocity stays in the ECS and sync-in
pushes it back as an activating write, waking the sleeper on 29 of 30 ticks.
Closing that channel separates them — with Velocity removed the immobility
half passes while the published pose is off by 1.88e-5 m, which is the value
half doing the work the structural claim credits it with.

Also records the review's 157-line correction with its instrument, so the
figure and the recipe above it stop naming different byte strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gate D's count was copied from Gate C and read "unchanged" while the true
value had moved: Gate C bis added the moveKinematic test and journaled no
count, so the comparison ran against a baseline one gate stale. Measured in
a worktree at each of the four heads, the series is 561 / 564 / 570 / 571 /
571, and the missing Gate C bis line is added — leaving a gate without its
count is the mechanism, not the arithmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`World.getMut` stamps `changed_tick` unconditionally and the `Changed<T>`
filter is built on that stamp, so publishing a bit-identical pose or velocity
reported a change that never happened, every tick, for every awake body. An
immobile kinematic platform held awake by a character standing on it
republished a constant zero forever, and `Velocity` being replicated with a
rollback strategy, the false delta left on the wire.

Sync-out now reads before it writes, symmetrically with sync-in, which already
had the rule for its own motive. The guard asserts `changed_tick` against the
world's current tick and never the value: under the defect the value is
already correct, and only the signal lies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four private helpers of identical semantics under two names -- widen in
mesh.zig, convVec3 in body_manager.zig and again in character.zig, convQuat --
collapse into forge/api/precision.zig. Two of the three vector copies had
already diverged: one short-circuited when the scalars coincided and the others
did not, which is what two writings of one conversion do.

The boundary is written against a named world scalar and never a literal f32,
so the day large_world lands the edit is one declaration and not a hunt through
call sites. The ECS sync seam is routed through it too: its fourteen narrowing
sites were a second boundary in all but name.

Verification is the deliverable, not the intention. A new lint rule flags any
narrowing in a forge production file outside the boundary, and the widening
half -- which has no token to flag -- is caught by the type system on the six
f64 cells, where Vec(3,f32) and Vec(3,f64) are distinct types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
src/interfaces/PhysicsModule.zig is the first file of that directory. It is
NOT frozen -- the freeze is M1.1.26 -- and it attests that absence rather than
leaving it to be read from a silence: a test fails the day
WELD_PHYSICS_PROTOCOL_VERSION appears.

The comptime assert block is deliberately absent. Surface guards belong to the
freeze, a guard covering three of twenty-seven entries would pass an
implementation missing the other twenty-four, and the block's first entry needs
ModuleContext, which this repository does not declare anywhere -- measured, the
name appears in three comments and no declaration.

What does land is the contract of the three body pose and velocity entries,
moved out of forge/api/types.zig, which had named this file as its destination.
Moved and not copied, with a pointer left where it stood. The signatures are
written at the world scalar, which makes this file the first consumer of the
alias gate E introduced; dt stays f32, carrying no length dimension.

Also corrects two more instances of the false struct_size premise than the two
the scope named: the class had four members in one file, and leaving two
standing beside two corrections is the motif this repository already named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scope named two false struct_size premises; measurement found four in one
file, so the class was swept rather than its two named members. The interface
file is created on the narrow reading of its scope line, with both readings and
the two measured facts that decided it recorded for review: surface guards
belong to the freeze, and ModuleContext is declared nowhere in this repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The state table was stale on three lines: PR #71 is merged and
v0.11.14-determinism is posted, so the last released tag, the active branch
and the current milestone all move. The test floor is re-derived from the
suite at 1906 / 1904 and says so, since re-deriving it from the closure is
what the dead-tests guard refused once during this milestone.

Two open decisions are added as preconditions of the M1.1.26 freeze --
ModuleContext, declared nowhere while the interface types init with it, and
the pose setters that cannot stay void now that bodies carry proxies -- and
the M1.1.9 precision entry is re-pointed onto the rewritten section 1.11.8.

Closing audits: zero French function words and zero accented tokens on the
full branch diff and on the brief; the drift sweep found three orphaned
references to deleted symbols and patched them in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@guysenpai
guysenpai marked this pull request as ready for review August 23, 2026 06:06
guysenpai and others added 6 commits August 23, 2026 08:31
The rule's diagnostic states that section 1.11.8 makes the precision boundary
unique, which is a claim about the engine, from a control that looked at one
module. Section 1.11.8 places that boundary at the interface tier, and
src/interfaces is where M1.1.26 writes the adapter -- conversion code by
definition, in a directory the rule ignored. It is governed from the day the
directory exists rather than the day the freeze finds the gap. The test that
locked the old restriction is rewritten onto the real perimeter, not deleted.

The escape marker exempted a site with nobody counting. A guard whose
exclusions are invisible converges to zero coverage through additions each of
which is locally justified, which is the failure dead_tests answers in this
same binary with a declared list and a bilateral control. The marker now takes
effect only for a file declared with a reason and an owner; an undeclared
marker is reported at its site and a declaration nobody used is reported as
stale. The list is empty, which is the cheapest moment to install it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records both findings with their tree-level counter-factuals: a narrowing in
the interface tier is now refused where it was invisible, and the escape
marker's three states -- undeclared, declared, stale -- each measured on the
tree rather than only in unit tests.

Also records a defect of my own caught before it shipped: the first stale-half
test re-implemented the production body over a fixture, which is the scratch
copy motif this repository named at M1.1.11.1. Split into reportStale so the
test drives the shipped function instead of a mirror of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six confirmed findings, treated as three subjects.

The class first: a property held by one entry and not by its twin. addBody was
not transactional where createCharacter was, so a failing insert left a body
with no proxy and a failing append left both; destroyCharacter skipped W4 where
removeBody applies it in its first instruction; and both pose writers committed
before the fallible proxy refresh, leaving the broadphase describing a body
that had moved -- and moveKinematic's retry then derived its velocity from
target minus target and published zero for a move that happened.

A character presence carries its character's own entity, so the sync seam
walked one entity as two bodies: outward it published the presence's constant
zero velocity over the entity's, inward a gameplay Transform write teleported
the presence past the sweep and depenetration that make a controller one. The
distinction now lives on the registration record.

The synchronisation had no caller outside its own tests. Three systems are
registered across three phases, because the DAG is forward dataflow and two
writers of one component in a phase conflict by construction, so only the phase
order can sequence them. The publication predicate becomes tagged AND still
asleep so the structural marker changes can defer to the phase flush.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The escape declaration is written in POSIX spelling and the runner passes
backslash-separated paths on Windows, so the suffix match could never fire
there: the site would report an undeclared marker and the tree would go red on
one platform only. The awareness already existed one function below, where
isTest handles both spellings, and this lookup had lost it.

The first counter-factual for the fix measured NOTHING and said so: the list is
empty, so the lookup never iterates, and the only test on it drove the helper
directly -- proving the helper correct without proving the call site used it.
Split into declaredIndexIn so a fixture drives the shipped lookup, and the
replayed probe now fails exactly that test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the six confirmed findings with what each counter-factual measured,
the twin-pair review that covers six pairs rather than the three reported
sites, and the one finding refused on the corpus with its reason.

Also records three things that were mine: two probes that measured nothing and
said so before any failure list was read, a line number quoted in the Gate F
signal that was an injected mutation rather than a discovered site, and a third
identical loss to git checkout -- on the very pass whose notes make probing
against a committed tree a standing rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A gameplay Velocity written from an update system was lost: the phases run
pre_update, fixed_update, update, post_update, so a write in update landed
after sync-in had read it and a publication in post_update overwrote it before
the next frame. C1.1 requires that an Etch system can write Velocity and the
solver apply it. An intra-phase constraint cannot fix it -- the scheduler has
no runs_before and the only intra-phase order is a Writes-to-Reads edge, which
would mean giving the tick a declared access it does not use, and ARCH-030
makes the declared set the type of the view a system receives. The tick and the
publication become one fixed_update system, so the order is code.

A trigger body has no manifold, no constraint, no impulse and no island entry,
so it has no resolved fact to publish. Two bodies on one entity is the normal
shape rather than the exotic one, so excluding only the character presence
covered the instance and not the class. Triggers are excluded from publication
and kept in reception.

The resolver no longer registers its handle type on the frame allocator, the
registration preflights both names before touching the scheduler, and the
linter's aggregate control runs only on a full scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
guysenpai and others added 19 commits August 23, 2026 11:55
Records the six findings with what each counter-factual measured, including
one that aborts rather than fails -- the testing allocator catching retained
memory is a stronger signal than the assertion asked for.

Also records two corrections of mine: an owner placed behind the gate it makes
fail, since M1.1.25 executes after the M1.1.26 that closes C1.1, and a
reception assertion whose first form assumed simultaneity where sync-in reads
the pose the ECS held going into the tick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A trigger has no manifold, constraint, impulse or island entry, and none of
that says anything about integration: integration.zig filters on body type
alone, so a dynamic trigger falls under gravity and its pose IS a fact the
solver resolved. Excluded from publication it drifted away from its entity in
silence, taking its detection volume with it.

The rule is now concurrency: a trigger yields only when another body on the
same entity publishes for it. Alone it publishes per its type, which covers the
four cases without restricting any of them, and reception stays governed by
type as it always was.

Both systems now declare the resource they mutate through the pointer, the
linter recognises a complete root set rather than only an empty argument list,
and a test records the future write-conflict with TransformSystem as a measured
fact instead of an invisible incompatibility.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A `&.{ ... }` literal in the descriptor passed to registerSystem is a temporary
whose lifetime ends with the enclosing block, and SystemScheduler stores the
descriptor with that slice. Once registerSystems returned, every type_name in
it pointed at dead stack: reading them printed an empty string, then faulted.

Nothing crashed before because the DAG edges are computed at registration and
no later path reads the accesses -- the first thing that did was the test
asserting they are declared, which is how the defect surfaced at all. The
tree's other call sites survive by accident, registering inside the function
that consumes the scheduler, so the API invites this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the trigger rule corrected from exclusion by nature to exclusion by
concurrency, the future TransformSystem conflict written down as a measured
wall, and the defect the P2-3 guard found on the way: the declared access
slices were temporaries the scheduler kept after registration returned.

Also records two of mine: a counter-factual that measured nothing because no
test asserted the declared set, and a fourth loss to git checkout -- which the
standing rule did not prevent because it named an intention rather than a
mechanism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exclusion by concurrency only arbitrated solid against trigger: two triggers on
one entity did not exclude each other and two solids never entered the
arbitration, so both were silent last-write-wins on configurations the
descriptor admits. The publisher is now elected -- the non-trigger of smallest
identity, else the trigger of smallest identity, a presence never -- and
identity is the complete handle, so the election does not depend on insertion
order.

Sync-in was undoing moveKinematic: the entry writes pose and velocities into
the solver while gameplay owns a kinematic Transform, so a value comparison
read a difference and pushed the stale ECS pose back before the step. It now
pushes only what gameplay actually wrote, measured on the component's change
stamp against a watermark -- the mirror of the rule sync-out already applies.

Also preflights both descriptors rather than both names, and normalises a path
before judging whether a scan covered every lint root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the election replacing an incomplete concurrency rule, the watermark
that stops sync-in from undoing moveKinematic, and the boundary case that made
the watermark inclusive rather than strict -- a write in update carries the same
tick number as the sync-in of its own frame.

Also records the divergence carried to the corpus rather than patched in code:
section 1.13.7's deduction covers islands formed by pairs, while a singleton
island comes from the enumeration of dynamics, so a dynamic trigger needs one
to ever stop being integrated. island_manager.zig is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The inward direction leaves this milestone for M1.1.26 with the Etch service.
The ECS tick says WHEN a write happened and never WHO produced it, and no
comparison of change stamps can create that information; a solver-side
provenance would not close it either, since moveKinematic moves a kinematic
whose pose the publication deliberately does not write, so the ECS copy would
stay stale and the entity would render at the old pose. Authority by BodyType
cannot tell a kinematic driven by its component from one driven by the API, and
both are legal. The Tier 1 service is the only place that sees both paths.

One registered system remains: the tick with its publication. The election that
decides which body speaks for an entity now runs as a single pre-pass, sorted on
a total key with no hashed container -- the per-body form was 3N squared, 363
million comparisons at the C1.1 target, paid even by a scene with no trigger.

Also corrects a normative comment onto the amended distinction between a
constraint island and an integration singleton, and stops an absolute path from
counting as lint-root coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Traces the frozen-Scope amendment as a recorded deviation with the property
that forced it, and writes a deferral entry naming exactly what exists, what it
cannot do, and that M1.1.26 owns it -- the watermark's exact predicate
included, since the inclusive comparison is load-bearing and was measured.

Also records a counter-factual that measured nothing: removing the election
criterion's second level left every test green, because in each of them the
solid happened to be created first and identity alone already elected it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A declaration is stale when its file was READ and carried no marker; a file
nobody opened says nothing either way. That removes the question the previous
two forms tried to answer -- whether a scan covered everything -- along with
both wrong verdicts they traded between them, and it holds whatever spelling a
caller uses for its paths. The one residual reasoning leaves, a declaration
whose file was deleted and is therefore never visited, closes with an existence
test on the declared path.

The claim that canonicalising paths was impossible at Zig 0.16 was FALSE:
std.process.currentPathAlloc and std.Io.Dir.realPathFileAbsoluteAlloc both
exist, verified in the standard library rather than inferred from a neighbouring
removal. A control was switched off on that inference, which is the part worth
recording.

Also sweeps every text still claiming an inward direction this milestone no
longer carries, and retracts a promise the election never made: it is
reproducible for a given sequence of operations, not invariant under a
permutation of creation order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the error rather than glossing it: I concluded an API was unavailable
by analogy with a neighbouring removal, and both greps that were supposed to
check it were themselves aimed wrong -- one at the wrong path, the other at a
function name instead of a capability. A control was switched off on that.

Also records that the correct design asks no such question, the four
combinations the new control is measured on, and a promise retracted rather
than honoured: the election is reproducible for a given sequence of
operations, not invariant under a permutation of creation order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The publication adds and removes the Sleeping marker without declaring it, so
a second writer of that component in fixed_update passed the preflight with no
conflict -- and ARCH-030 will make the declared set the type of the view a
system receives, where a body mutating an undeclared component stops compiling.

removeBody accepted a character presence, and the sequence needs nothing
illegal: getCharacterInnerBody is a public entry, its BodyId released the
proxy and the registration, and destroyCharacter then released a proxy the
broadphase had already freed. It is a no-op, because the frozen signature
returns void and the repository already answers an unhandleable handle that
way, and the filter sits before every mutation -- a refused call that had still
woken the scene would be a side effect with no operation behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The texts that say what is DELIVERED still described a bidirectional scope, a
sync-in before the tick and two registered systems. They now say the one
system, step plus publication. Three further mentions were checked and kept,
because they name the marker's two transitions, the note recording where the
reception half went, and TransformSystem's two writers.

Also corrects a delta narrative that did not reconstruct its own figure: the
sixth pass wrote "three go, one arrives" for a move of minus three, where the
measurement is five removed and two added. The count was right and the story
reached minus two, which leaves a reader looking for a loss that is not there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"With one system there is no partial state to leave behind" was false: moving
to one system removed the residue BETWEEN two calls and nothing inside one.
registerSystem appends edges and tracker entries before an allocation can fail
and its errdefers do not undo all of them, so a retry can report a conflict
against a count of zero. What replaces it says what the preflight covers --
name and write conflict -- and that after OOM the scheduler is unusable. The
rollback is Tier 0 and already recorded there with its owner.

The presence-removal guard moves next to removeBody with the half it was
missing: a proxy count measures that nothing was released and says nothing
about the wake, so a sleeping partner retained with the presence is asserted
still asleep, with a legitimate W4 producer in the same scene to prove the
reading discriminates.

And the tests the re-scope touched are swept on one criterion -- does the name
still describe what the body measures. One measured nothing in its inward half,
two carried assertions true for every body, and three named systems in the
plural. The absence they half-measured is now one test that says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reports the negatives as well as the changes: nineteen tests read against one
criterion, three renamed, one stripped of a half that measured nothing, two
stripped of assertions true for every body, thirteen read and left alone. The
absence those halves were half-measuring is now one test that names it.

Also records an unplanned demonstration: the counter-factual for that absence
test injected a narrowing outside the boundary, and the precision lint rule
failed the production tree alongside the intended test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
publishPhysicsWorld writes raw pointers, PhysicsWorld.deinit frees and poisons,
and nothing cleared the resource: hasPhysicsWorld kept answering true and the
next dispatch dereferenced a dead address. That the runtime's ordinary order is
safe changes nothing -- a lifetime contract nothing enforces is not a contract,
and it is the same shape as removeBody accepting a character presence.

unpublishPhysicsWorld is the symmetric half and the caller that published is the
one that withdraws, so PhysicsWorld still knows nothing of the ECS. The handle
is zeroed rather than the resource removed: resolve already reads zero as
absence, and nothing has to be freed on a path a caller may run at teardown.

The absence test now drives the registered path rather than the direct
composition -- an inward read reintroduced inside the system would have left it
green -- and writes every field the departed sync-in acted on, rotation and
angular velocity included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the use-after-free as the removeBody family it belongs to -- two
operations each correct whose sequence breaks -- and records that the absence
test written during the previous pass carried the very defect that pass had
corrected elsewhere: a name claiming a wider set than the body visits.

Also corrects the Closing-notes precondition onto the right one of the three
Tier 0 debts, and marks the superseded reading at the earlier journal entry
rather than rewriting it there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A withdrawal that cleared whatever it found let a late teardown erase a world
published after it: publish A, publish B, run A's deferred cleanup, and every
frame afterwards is a silent no-op with nobody having done anything wrong. It
now clears only the caller's own handle, and finding someone else's is a no-op
rather than a raise on a teardown path.

Publication refuses to replace a live one, which is the same defect seen from
the other side: a silent replacement makes the first world disappear without
anyone having withdrawn it. The signature is not frozen, so the error channel
costs nothing a later milestone has to live with.

The accessor answers with the identity rather than a boolean, because the tests
that guard this need to tell "B is still published" from "B was erased and
something else answers" -- and both new tests assert that identity, not a
presence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records why the path returned a finding three passes running -- it was being
closed one side at a time, and each half looked complete on its own -- and that
both halves ship together.

Also records that each half is guarded separately: clearing blindly again fails
the late-withdrawal test alone, replacing silently again fails the refusal test
alone, so the two guards are independent rather than one counted twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither finding of this round is a wrong behaviour or a hollow test: an
address-reuse ABA needs a world destroyed and a second allocated at the same
address, which no path here produces, and a refused publication marks a
resource nothing observes. What was wrong was a sentence -- the withdrawal
promised to be harmless however late, and its check is on the address. The doc
now says what it separates and what it does not, with the token that would
close it recorded in Closing notes under M1.1.26.

Closing audits replayed on the full branch diff: zero French function words and
zero accented tokens, and a drift sweep over every symbol removed or renamed
found one orphan comment, patched. CLAUDE.md called the PR a draft in one row
while two others said otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@guysenpai
guysenpai merged commit 02a2407 into main Aug 24, 2026
19 checks passed
@guysenpai
guysenpai deleted the phase-1/forge/physics-world-orchestration branch August 24, 2026 18:00
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