§42's rule is short and absolute: exactly one system owns a node's
transform at a time, and "the engine must prevent multiple systems from
silently overwriting the same transform". This module holds the vocabulary
for that ownership (TransformAuthority) and the enforcement primitive
every writing system shares (warnAuthorityConflict); the field itself
lives on Node (§42 writes node.transformAuthority = "physics"), see
Node.transformAuthority.
What enforcement means here (decision, WP-2.3)
§42 says conflicts "should produce development warnings" and that a warning
"fires whenever a system writes a transform it does not own". A warning alone
would still leave the transform overwritten — the very thing §42's first
sentence forbids — so the engine pairs the two halves: the owner keeps the
transform and the non-owner's write is refused. A refused write is loud
(a console.warn) and lossless in the other direction: nothing about the
owner's state is touched, so authority conflicts can never corrupt a
simulation, only stall the system that mis-declared itself.
The check belongs to the writer, not to Transform: transforms are written
through plain math methods on hot paths (transform.position.set(...),
plan D3), and gating every one of those on an ownership test would put a
branch inside the innermost loop in the engine and still be trivially
bypassable. Each system instead tests once per node per step, immediately
before it would write — the shape warnAuthorityConflict is built for.
Deduplication (§42: development warnings)
A conflicting system conflicts every fixed step — 60 identical lines per
second per node is noise that hides the first one. Warnings are therefore
emitted once per node per writing authority and suppressed afterwards,
tracked in a module-level WeakMap keyed by node so the bookkeeping dies
with the node and holds nothing alive. Two different authorities fighting
over one node produce two warnings, which is the point: each names a distinct
mis-configured system.
writer is a TransformAuthority rather than a free-form system name
(decision, WP-2.3) because §42 identifies writers by the authority they claim
— MotionSystem writes as "kinematic", a solver as "physics" — which
keeps the value typo-checked and makes the suppression key the same
vocabulary as the field being violated. Two systems that both claim
"kinematic" over one node therefore share a suppression slot; they are, by
§42's model, the same writer.
The message is unconditional console.warn — not DEV / devWarnOnce.
@fourjs/scene is a §33 simulation package: dev-build-mode.test.ts
forbids any build-flag import here (the same reason a DEV-gated
new Node() warn was reverted). The WeakMap is the once-per-pair
suppress; production prints the first conflict and then stays quiet.
Application writes on a system-owned node are a different contract (2026-10-05):
they land, and a DEV build warns once. That hook lives on
Transform.markDirty and is skipped inside
runOwnedTransformWrite, which every writing system wraps its owned
pose with. The DEV test is inlined (typeof __FOUR_DEV__) so this module
still does not import DEV.
Transform authority (§42).
§42's rule is short and absolute: exactly one system owns a node's transform at a time, and "the engine must prevent multiple systems from silently overwriting the same transform". This module holds the vocabulary for that ownership (TransformAuthority) and the enforcement primitive every writing system shares (warnAuthorityConflict); the field itself lives on
Node(§42 writesnode.transformAuthority = "physics"), seeNode.transformAuthority.What enforcement means here (decision, WP-2.3)
§42 says conflicts "should produce development warnings" and that a warning "fires whenever a system writes a transform it does not own". A warning alone would still leave the transform overwritten — the very thing §42's first sentence forbids — so the engine pairs the two halves: the owner keeps the transform and the non-owner's write is refused. A refused write is loud (a
console.warn) and lossless in the other direction: nothing about the owner's state is touched, so authority conflicts can never corrupt a simulation, only stall the system that mis-declared itself.The check belongs to the writer, not to
Transform: transforms are written through plain math methods on hot paths (transform.position.set(...), plan D3), and gating every one of those on an ownership test would put a branch inside the innermost loop in the engine and still be trivially bypassable. Each system instead tests once per node per step, immediately before it would write — the shape warnAuthorityConflict is built for.Deduplication (§42: development warnings)
A conflicting system conflicts every fixed step — 60 identical lines per second per node is noise that hides the first one. Warnings are therefore emitted once per node per writing authority and suppressed afterwards, tracked in a module-level
WeakMapkeyed by node so the bookkeeping dies with the node and holds nothing alive. Two different authorities fighting over one node produce two warnings, which is the point: each names a distinct mis-configured system.writeris a TransformAuthority rather than a free-form system name (decision, WP-2.3) because §42 identifies writers by the authority they claim —MotionSystemwrites as"kinematic", a solver as"physics"— which keeps the value typo-checked and makes the suppression key the same vocabulary as the field being violated. Two systems that both claim"kinematic"over one node therefore share a suppression slot; they are, by §42's model, the same writer.The message is unconditional
console.warn— notDEV/devWarnOnce.@fourjs/sceneis a §33 simulation package:dev-build-mode.test.tsforbids any build-flag import here (the same reason a DEV-gatednew Node()warn was reverted). The WeakMap is the once-per-pair suppress; production prints the first conflict and then stays quiet.Application writes on a system-owned node are a different contract (2026-10-05): they land, and a DEV build warns once. That hook lives on
Transform.markDirtyand is skipped inside runOwnedTransformWrite, which every writing system wraps its owned pose with. The DEV test is inlined (typeof __FOUR_DEV__) so this module still does not importDEV.