Phase 1 / Forge / forge_3d orchestration: PhysicsWorld and ECS Transform synchronisation - #72
Merged
Merged
Conversation
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
marked this pull request as ready for review
August 23, 2026 06:06
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brief
briefs/m1.1.15-physics-world-orchestration.mdDRAFT, opened at the first gate as
engine-development-workflow.md§4.4 requires:ci.ymlfires onpull_requestand 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_3dhad every mechanism the arena needs and no owner for the tick that runs them. This milestone creates that owner:PhysicsWorldexecutes the eleven steps ofengine-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
PhysicsWorldand the tickGate A — what landed
tests/solver_test.zig— where its only composition lived, driven by ten suites — intoforge_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 ownenter(), which is what binds the recorded order to the executed one.sensors_onflag defaulting tofalse; in production that reads "sensors silently do not work".SensorState.updatetakes 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 withinK = 60. The four chain witnesses readREPORTED, not gatedon 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:
1 failedof 561 — the order test, that adjacency's only guardexpected 16, found 4, plus 6 physics tests4 failed, 35 crashed— where order matters physically, dozens fireenter()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 inworld.zigand in the test.Counts, with their denominators.
zig build test-forge-3dcollects 561 on this branch (560 pass, 1 skip) against 555 onmainate02d27b, measured in a worktree, both macOS aarch64:+6, the six blocks added intests/world_test.zig. Full suite 1875 collected. Thedead-testsdeclared 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, intests/world_test.zig.Local gates
zig buildcleanzig build test— 1875 collected, 1856 pass, 19 skipzig build test-forge-3d— 561 collected, 560 pass, 1 skipzig fmt --checkgreen oversrc/ tests/ bench/ tools/ build.zigzig build lintgreen,dead-testsconservation reconciled at 1875zig build forge-determinismgreen on the four local corners