WRL Core 0.1.2 — Frozen Family Extract
This document commits the meaning families of WallRiderLang that the TRVM runtime and the Forge semantic graph have grounded. It freezes which kinds of things exist and what each kind means — not the detailed surface rules, which remain a design draft and may still change.
WRL_UNSUPPORTED_FEATURE. Part II exists so that the
direction is stated in the spec's own language and can be argued with, rather
than living only in somebody's head. Where the two parts appear to disagree,
Part I wins.Status and errata
Status: frozen (families only). Extracted from the Complete Design Draft per the WRL ruling, Phase 1. This is the living Core-0.1.x document; the current revision is 0.1.2.
When the design draft and this document conflict about what is settled, this document wins. The draft remains authoritative for full rationale and forward design.
Errata 0.1 → 0.1.1
Before freezing Forge Semantic IR v1, five corrections were required so that the frozen core reflects the ADMIT and persistent-world results rather than the older actor execution model:
- The period cycle is OBSERVE→ACCEPT→MAP→COMMIT→REACT→FILM, not Collect→Order→Reduce→Commit→Record.
- "Conflicts dissolve" is narrowed — single ownership stops generic cell races, but claim and control conflicts need explicit policies.
- Canonical ordering is policy-pinned, not a fixed hash formula.
- History splits into WorldFrame / EventLedger / BuildFilm.
- The grounding language distinguishes grounded from reserved and partial.
Errata 0.1.1 → 0.1.2
Four corrections, none of which move a frozen family. They make the grounding claims honest and add the two structural facts the implementation established after 0.1.1 was written:
- Mailbox is
partial, notreserved. The mailbox declaration is grounded in the canonical IR and the runtime, but the~~route construct is not surface-emittable and has no structural edge declaration. Mailbox is therefore not a sixth surface-grounded role — it is IR/runtime-grounded and surface-partial. §14 is corrected; §14b is new. *is narrowed. Only the replication-by-position meaning is implemented; wildcard matching remains reserved with no lowering. §4 is corrected.- The document boundary is normative (new §15). A WRL world document and
a scenario are different documents.
periodsand[epoch:N]claims are run inputs and are deliberately outside theSemanticArtifactID. The strict world parser is normative; the combined-document parser is an explicitly-named migration bridge. - The Core 0.2 promotion order is recorded (new §16), together with the conditions each construct must meet before it may be promoted.
Part I · Normative — the frozen family extract
§0Architectural rulefrozen
WRL denotes the canonical Forge semantic graph. It is not lowered by compiling surface syntax directly to hand-written interaction-calculus text. The only sanctioned path is:
Forge Semantic IR v1 is a separate gated deliverable. Until it is frozen, no lowering target is normative.
§1The ten construct kindsfrozen · closed set
Every WRL construct is exactly one of these ten kinds. The set is closed.
| Kind | Role (frozen) |
|---|---|
| Function | calculates — pure local computation |
| Actor | persists — durable identity + owned state |
| Route | communicates — a directed, textured transition edge |
| Wall (boundary) | authorizes — gate / commit / seal on propagation |
| Period | orders — logical-time step under which events settle |
| Fragment | carries code — quoted graph as movable data |
| Stencil | constructs graphs — parameterized fragment producer |
| Derive | transforms typed graphs |
| Film | proves what occurred — append-only replayable log |
| Hash | identifies what was built — content address |
Of these, Actor, Route, Wall, Period, Film, Hash are additionally grounded by TRVM today (§14). Function, Fragment, Stencil, Derive are frozen as kinds but their detailed semantics remain draft.
§2The four reading lawsfrozen
- Shape says what a thing is. Bracket shape identifies the kind of object.
- Line texture says how it moves. Route style identifies the operational guarantee.
- The colon introduces; the slash bounds. Both families cap at three strokes, ordered by permanence: transient · committed · sealed.
- Marks for architecture, words for computation. Process notation owns punctuation; expression notation defaults to words.
§3Container / shape-as-kind familyfrozen · 3 members
| Shape | Kind (frozen) |
|---|---|
[x] | durable identity (actor, region, mailbox, named artifact) |
(x) | state — current, mutable-by-replacement cell |
{x} | wiring — permitted topology / capabilities / ports |
Frozen invariant: (state) is single-owner and
mutable by replacement; {…} is permitted, static
structure, distinct from current state. Exact field grammars are draft.
§4Identity / addressing familyfrozen · 4 members
| Symbol | Meaning-role (frozen) |
|---|---|
# | canonical identity / content hash |
@ | address / placement / stamp |
? | pattern variable introduced by matching |
* | wildcard match, or replication-by-position |
Frozen invariant: executable identity is a hash; human aliases are words and never semantic.
Implementation status of * (0.1.2). The
symbol's family role stays frozen as written, but the two meanings are at
different maturities and the spec must not imply otherwise:
| Meaning | Status |
|---|---|
replication-by-position ([relay:r*3]) | implemented — surface sugar, bounded, identity-equivalent (§17) |
| wildcard match | reserved — no lowering exists |
§5Route / texture familyfrozen · 4 core textures
The reduction relation is defined over exactly these four irreducible textures. All surface sugar canonicalizes down to one of them plus attributes.
| Texture | Guarantee (frozen) |
|---|---|
--x--> solid | deterministic local transition; settles within the period |
~~x~~> async | asynchronous message; appended to a mailbox; observable next period |
==x==> verified | evidence-backed / committed transition under a named policy |
!!x!!> fault | crash / cancel / reject / interrupt; engages supervision |
Frozen: the set of core textures and their guarantee-roles. Not frozen: label, payload and guard grammar; the sugar table; branch and lane rules.
Surface status (0.1.2). Freezing a texture's meaning-role is not
the same as grounding its construct. Only -- is
surface-grounded today:
| Texture | Surface status | See |
|---|---|---|
-- solid | surface-grounded — declarable, emittable, round-trips | §14 |
~~ async | partial — IR/runtime-grounded, not surface-emittable, no structural edge declaration | §14b, §16 |
== verified | partial — acceptance machinery exists; the construct does not | §16.1 |
!! fault | partial — fault state exists; route and supervision do not | §16 |
§6Time familyfrozen
- Logical time is superdense: every event carries
(period t, microstep m). - A period is the settle-and-commit unit; same-period events are ordered by microstep.
- Wall-clock enters only through an explicit boundary and is recorded as a signed fact; wall-clock is never replayable state, logical time is.
Frozen: the period/microstep model and the wall-clock-as-fact discipline. Not frozen: the tick surface syntax details.
§7Memory-kind familyfrozen · 5 members
| Shape | Memory kind (frozen) | Lifetime rule |
|---|---|---|
(state) | volatile cell | mutable by replacement; single-owner |
{facts} | monotonic knowledge | grow-only; merges by lattice union; never retracts |
[archive] | durable store | persistent addressable actor; survives restart |
:: fragment // | code memory | quoted graph as portable data |
#hash | sealed identity | immutable, content-addressed |
{facts} (monotone union); shared identity
is [archive]. This single-owner discipline is what
makes execution deterministic without a runtime conflict resolver.§8Execution-model family — the deterministic reactive floorfrozen
Grounded directly by TRVM's deterministic reduction, ADMIT reducer, persistent fold, and replayable films.
Configuration (frozen shape): the persistent runtime state — world state (owned state cells), monotone claim facts, immutable acceptance receipts, capacity-fault latches, clock state, poses, rotors, numeric faults; plus static topology and policies. Mailboxes, supervisor and behavior tables are Experimental and are not part of the frozen floor.
Two frozen invariants (enforced statically)
- Single-owner cells — no shared mutable cell.
- Disjoint deterministic guards — at most one deterministic rule enabled per actor-state; genuine choice must be an explicit scored branch.
Period cycle frozen · 6 phases
| Phase | Definition |
|---|---|
| OBSERVE | canonicalize and insert distinct claims |
| ACCEPT | create missing receipts per the pinned acceptance policy |
| MAP | newly-accepted successful ops → controls |
| COMMIT | apply control writes and fault resets to owned cells |
| REACT | deterministic within-period token cascade to fixpoint; latch current overflow |
| FILM | record the entries needed to replay the epoch |
For claim-free profiles the first three phases (OBSERVE/ACCEPT/MAP) degenerate to identity but must not disappear from the semantic model: the OBSERVE/ACCEPT/MAP distinction is what determines which claims were seen, which candidate was accepted, whether a retransmission retries an effect, which controls were committed, and whether a same-epoch configuration affects that epoch's reaction.
Canonical ordering is policy-pinned frozen as a family, not a formula
Events and accepted operations are ordered by a total canonical key whose schema and policy identity are part of the semantic artifact. Worker arrival order is never semantic. The current ADMIT profile pins the CandidateKey and the canonical accepted-event ordering; a future mailbox profile may pin a different key. The existence and policy-pinning of the key is frozen; the specific formula is not.
Conflicts narrowed in 0.1.1
Cell-write conflicts are structurally constrained; claim and control conflicts are resolved by explicit, policy-pinned recognition, acceptance and canonical application laws. Concretely:
- single-owner cells remove generic write-write races;
- append and fact-union structures merge deterministically;
- but equivocation, conflicting accepted operations, claim retransmission, receipt divergence, configuration ordering, fault-reset-vs-new-overflow, and capacity failure are not dissolved by ownership — each is resolved by an explicit pinned policy.
| Concern | Pinned law |
|---|---|
| recognition | distinct-CandidateKey count |
| acceptance | MIN CandidateKey / first receipt |
| application | canonical event order |
| fault | COMMIT clears, then REACT ORs |
| capacity | atomic latch; no partial |
Not frozen: the podium/ranking ceiling, scored-branch scoring functions, distributed termination detection.
§8bHistory family — WorldFrame / EventLedger / BuildFilmfrozen · 3 distinct artifacts
"Film" is not one undifferentiated object. Three conceptual artifacts are frozen distinct; their current serialization is the Film v0.6/v0.7 family.
| Artifact | Frozen role |
|---|---|
| WorldFrame | authoritative observable state sufficient for future behavior — the physical world snapshot (Film v0.6 bytes) |
| EventLedger | accepted events, receipts, effects, outcomes — the claim-aware layer (Film v0.7 adds this over v0.6) |
| BuildFilm | compiler / build provenance proposed |
A replay package is frozen as:
ReplayBundle { initial_artifact, initial_state, event_ledger, frames, policy_ids }
Frozen: the three-way distinction and the ReplayBundle shape. Not frozen: byte layouts — they are the Film serialization, a backend concern.
§9Fact-merge familyfrozen · union law only
Replicated {facts} merge by a join-semilattice
union that is commutative, associative and idempotent. This is the frozen
distributed floor and matches TRVM's proven claim-fact set union.
§10Boundary (wall) familyfrozen · gate / commit / seal
| Boundary | Reduction effect (frozen role) |
|---|---|
/gate | require capability; emit an effect-request node (no ambient I/O) |
//commit | canonicalize the fragment-so-far; assign a content id; freeze it |
///seal | commit, then register the artifact under a content hash |
Frozen: the three-tier ordering by permanence, and the no-ambient-authority rule — effects cross named walls only. Not frozen: exact policy grammars.
§11Canonicalization & content-identity familyfrozen
- Meaning is normalized before content addressing. Comments, spacing, alignment, decorative line and route length, and presentation color never affect the semantic hash.
- Identity is content-addressed (
#hash); the same normalized graph yields the same hash on every host. - Builds and expansion are hermetic and host-blind: same source, same canonical bytes.
This matches TRVM's content-addressed objects and the digest / candidate-key discipline used in the ADMIT core.
§12Conformance families frozen
These observable properties are frozen as families of guarantee; their exact test matrices remain draft.
| Family | Guarantee |
|---|---|
| Formatting invariance | presentation never changes the hash |
| Replay exactness | a live run and its replay share all observables |
| Build invariance | same inputs → identical film + hashes across hosts |
| Merge laws | fact union is commutative / associative / idempotent |
| Boundary safety | undeclared effects cannot cross walls |
| Scheduler invariance | worker ordering never changes the committed film |
| World round-trip 0.1.2 | a format→parse cycle preserves the complete canonical world projection, defined by exclusion (§15) |
| Identity round-trip 0.1.2 | a format→parse cycle preserves the canonical artifact bytes and the SemanticArtifactID |
| Document separation 0.1.2 | a world document excludes run inputs both semantically and lexically (§15) |
§13What Core 0.1 does NOT freeze
Deferred to later frozen revisions:
- Experimental: the entire expression notation (values, functions, types, generics, numerics), effects / capabilities / entropy, the supervision error ladder, streams and backpressure, actor behavior blocks, evolution and migration, podium / ranking, the static-check catalog, execution profiles.
- Proposed: metaprogramming (fragments, stencils, derives, reflection, sealed compiler tools) beyond their kind-role; modules and builds; build films; foreign functions; and all distributed settlement above fact-union.
0.1.2: "mailboxes" was removed from the Experimental list — the mailbox is no longer merely experimental, it is IR/runtime-grounded and surface-partial (§14b). It is still not promoted.
Promotion path: a family moves from Experimental → Core only after it is surface-grounded (§14b) in a running Forge/TRVM implementation, at which point this file is revised. The steered order for 0.2 is recorded in §16.
Everything in Part II is, by definition, in neither list: it is not Experimental (nothing has been tried) and not Proposed (nothing has been designed to the point of a construct). Part II records the direction these deferrals are being resolved toward, so that a promotion can be judged against something other than convenience.
§14TRVM groundingcorrected 0.1.1
Grounding is stated honestly: grounded = an isomorphic TRVM lowering exists; partial / reserved = the concept is grounded but the WRL language construct is not, or the construct is reserved with no lowering.
0.1.2: the word "grounded" was doing two jobs. §14b now splits it into IR/runtime-grounded and surface-grounded. Every entry in the Grounded table below is surface-grounded.
Grounded
| Family | TRVM grounding |
|---|---|
| The five surface-grounded world roles (Pulser / Relay / Door / Spinner / Orb) — Mailbox is not a sixth | fixture node kinds; lowered by the compiler |
| State + wiring | state schema (counters, wires, rotor-as-state, pose, sticky fault); sig-wire + socket edges |
Deterministic signal route (--) | sig-wire delivery; within-period REACT fixpoint |
| Periods | epoch-by-epoch persistent fold |
| WorldFrame / EventLedger (§8b) | Film v0.6 (physical) / v0.7 (claim-aware); replay exactness proven |
| Content identity (§11) | content-addressed rulepack hash; digest / CandidateKey packing |
| Fact union (§9) | monotone claim-fact set union; immutable receipts kept separate |
| ADMIT acceptance core (§8) | OBSERVE→ACCEPT→MAP reducer, single-epoch + persistent fold, reference == native |
| Commit / react sequencing (§8, §10) | COMMIT (control writes + fault resets) → REACT → latch overflow |
Reserved or partial — NOT a complete grounding
| Family | Status |
|---|---|
Async route / mailbox (~~) | partial — the mailbox declaration is grounded in the canonical IR and the runtime, but the ~~ route construct is not surface-emittable and has no structural edge declaration (§14b) |
Verified route (==) | partial — acceptance machinery grounded, but the route construct is not a complete lowering (§16.1) |
Fault route + supervision (!!) | partial — numeric-fault state grounded; the route/supervision construct is not, and no supervision floor exists yet |
Capability gate (/gate) | reserved — no capability-gate node kind yet |
Seal / artifact registry (///seal) | partial — commit grounded; seal registry is Proposed |
| General actor behaviors | reserved — Experimental; no arbitrary-behavior runtime exists |
Wildcard * | reserved — only replication-by-position is implemented (§4) |
§14bSurface-grounded vs IR-groundedfrozen distinction · new in 0.1.2
0.1.1 used a single word, grounded, for two genuinely different achievements. Conflating them let the Mailbox work read as if it had promoted a route texture, which it had not. 0.1.2 splits the term:
| Term | Means |
|---|---|
| IR/runtime-grounded | a canonical IR declaration exists, the runtime executes it, and it is covered by a battery |
| surface-grounded | additionally, WRL source can declare it, the formatter can emit it, and it round-trips through the canonical bytes |
A construct is only eligible for promotion into a frozen Core family when it is surface-grounded. IR/runtime grounding alone is necessary but not sufficient.
Mailbox is the worked example. The mailbox declaration is
IR/runtime-grounded: it exists in the canonical IR, the runtime honours it, and
the networking battery covers it. It is not surface-grounded: there is no
~~ surface form to write, the formatter cannot emit
one, and — decisively — there is no structural edge declaration for it.
Mailbox is not a sixth surface-grounded role. The five world roles (Pulser / Relay / Door / Spinner / Orb) are surface-grounded. Mailbox is IR/runtime-grounded and surface-partial.
The networking track is correct to call Mailbox a sixth IR role — that is exactly what it added, additively, without moving any pre-existing identity. What must not be inferred from it is that WRL now has six surface roles, or that the async route texture was promoted. It did not, and it was not.
-- does. Promoting
~~ therefore requires a canonical logical route
declaration that is distinct from the structural edge declaration, not a new
edge kind.§15The document boundarynormative · new in 0.1.2
A world and a run of that world are different documents.
| Document | Carries | Enters SemanticArtifactID? |
|---|---|---|
| World document | profile, objects, edges, static config, wiring | yes |
| ScenarioV1 | periods, [epoch:N] claims, run inputs | no |
Frozen consequences
- Run inputs are not world content.
periodsand[epoch:N]claims are run inputs. Changing them does not move theSemanticArtifactID; the same world can be run by many scenarios. - A world formatter emits a world document. It must exclude scenario
syntax both semantically (reparsing yields no run inputs) and
lexically (the emitted text does not contain scenario syntax at all). The
lexical half is load-bearing: a formatter emitting a literal
periods 0would satisfy the semantic half while still writing scenario syntax into a world document. - Round-trip laws split in two. The world round-trip preserves the
complete canonical world projection; the identity round-trip preserves the
canonical artifact bytes and the
SemanticArtifactID. Neither subsumes the other.
The world projection is defined by exclusion — every canonical
field except the run inputs — and never as a fixed tuple such as
(profile, nodes, edges). A fixed tuple silently stops
covering any field a later slice adds; §16 adds route semantics, which such a
tuple would omit.
15.1 · The two parser mouths
The strict world parser is normative. Parameterizing the parser is sanctioned only as a migration bridge, and only through an unmistakable API — never a boolean flag on one function:
| Entry point | Accepts | Status |
|---|---|---|
parse_wrl_core(text) | world source only; rejects run inputs with a typed WRL_WORLD_SOURCE_HAS_SCENARIO | normative |
parse_wrl_legacy_document(text) | a pre-boundary combined document | explicit compatibility path |
The legacy path's destination is to split a combined document into a world document plus a scenario, rather than to keep parsing it forever.
CanvasGraphV1 was never migrated
across this boundary: it still carries top-level
periods / batches, making
it the structural parallel of the legacy parser. This is not a live defect,
because the production canvas path is CanvasLayoutV1,
which is world-only by construction. Whether
CanvasGraphV1 gets the same strict/legacy split or is
retired is unruled.§16Core 0.2 promotion orderrecorded · new in 0.1.2
The order below is steered, not merely preferred — each step supplies something the next one needs, so reordering them produces constructs that cannot be grounded honestly.
| # | Step | Gate it must pass |
|---|---|---|
| 1 | L-0 closure | the round-trip and document-boundary laws restated and green — complete |
| 2 | ~~ async route | needs a canonical logical route declaration distinct from the structural edge declaration (§14b) |
| 3 | == verified route | see §16.1 |
| 4 | # references and & composition | — |
| 5 | //commit and ///seal | — |
| 6 | !! fault route | may only follow a minimal supervision floor actually existing |
| 7 | /gate capability gate | last |
16.1 · == is not "almost free"
It is tempting to read the verified route as nearly grounded, because ADMIT already accepts claims. That reading confuses two different things:
- the declaration
==would introduce is authorization structure; - the frozen texture
==denotes an evidence-backed transition.
== is grounded only when
ADMIT enforces all four of: the claimant, the target, the
operation family, and a named policy. Anything less is a route that
looks verified and is not.16.2 · Permission / instance splitapproved
| Layer | Carries |
|---|---|
| World document | declares a verified channel and its schema |
| ScenarioV1 | carries the claim instances |
| Receipts | prove acceptance through that channel |
Constraint. Do not introduce a principal-shaped role such
as [worker:w1] as part of this work. How principals
and writers exist in the role system is a separate question requiring its own
sanction; smuggling one in as a side effect of the verified route would freeze
a role-system decision that was never ruled on.
§17Surface sugar tierstatus · new in 0.1.2
Sugar is a source-to-source pre-pass that runs in front of the untouched canonical parser.
Status:
IMPLEMENTED — CURRENT BATTERY GREEN / IDENTITY-EQUIVALENT /
NOT CLOSURE-PROVEN / NOT FROZEN.
The law: no sugar-specific identity
Stated precisely, because the looser phrasing is false:
- A sugared spelling and its explicit twin desugar to the same canonical
bytes and therefore seal to the same
SemanticArtifactID. Sugar introduces no identity of its own and no privileged path to the seal. - It is not the claim that editing sugar cannot move an identity.
sp*3→sp*4is a different program and moves the identity, exactly as the explicit spelling would.
Two required properties, gated by battery
- Bounded expansion. A one-to-many form must reject an absurd count with
a typed diagnostic before allocating the expansion.
sp*1000000000is a diagnostic, not an out-of-memory event. - Original-source mapping. A one-to-many expansion must preserve the authored spans, so that diagnostics, completion, semantic diff and editor operations all refer to what the author wrote rather than to generated text.
Generated names are judged by the ordinary seal — a collision between a
generated name and an explicit one is a normal
WRL_DUPLICATE_ID, not a special sugar rule.
End of Part I. The next gated
deliverable is §16 step 2 — the ~~ async route —
which requires a canonical logical route declaration distinct from the structural
edge declaration.
Part II · Topology extrapolation — non-normative
§D0Status of Part IInon-normative · new in 0.1.2
Part I answers what is settled. Part II answers a different question: what is this language for, such that the settled part is worth having? A frozen core with no stated destination gets extended by whoever asks loudest; writing the destination down turns that into an argument that can be lost.
profile line, so they would go on being rejected for
a reason unrelated to the capability. The trip-wire is the registry row, where a
person must act; that is the intended failure mode, and it is stated exactly in
the promotion note.D0.1 · The reframe that produced Part II
WRL has been evaluated as an actor language — process, mailbox, behaviour loop, supervision tree — and by that measure it is weak: it has no runtime-spawnable process, no user-written behaviour, no supervision floor, and no surface mailbox (§14, §14b). Part I does not hide this.
That measure is not, however, the one this language was built for. What the implemented core actually delivers is unusual and hard: a content-addressed, replayable, statically-checked description of a network of identities, in which the description is a hash and the run is a film. Part II states the destination that follows from taking that seriously:
WallRiderLang is an executable topology language: a notation in which physical, computational, communication, authority, fault, and economic networks can be described over the same durable identities, checked before they run, and replayed exactly after they do.
The prose case for this framing, with the honest scorecard and the ordered list of what is missing, is on the Direction page. What follows here is only the part that has spec consequences.
§D1Multilayer topology over one identitydraft
A layer is a set of connections among the world's identities that answers one question about them: who is next to whom in space, who can message whom, who commands whom, who powers whom, who is blamed when whom fails.
The multilayer-network literature [K14] distinguishes two arrangements, and the distinction is the whole of this section:
| Arrangement | An entity in n layers is | Consequence |
|---|---|---|
| Replica-based (general case) | n node-layer tuples, tied together by explicit inter-layer coupling edges | identity is a property of the coupling; the couplings can be wrong, missing, or asymmetric |
| Node-aligned proposed for WRL | one node, present in every layer by construction | identity is structural; there is nothing to keep in sync |
Draft rule D1. A WRL world is node-aligned. Layers are aspects of
one identity, not replicas of it. There is therefore no inter-layer coupling
construct, because there is no pair to couple: d7 in
the road layer and d7 in the comms layer are the same
d7.
What node-alignment costs. It rules out the honest modelling of an entity that genuinely is several things in different layers — a consortium that is one legal person in the authority layer and three independently-failing datacentres in the fault layer. The draft's answer is that such a thing is three identities and a wall (§10), not one identity in three replicas. If that answer turns out to be unworkable in practice, D1 is wrong; see §D7.
§D2Three connection kindsdraft
Part I recognises exactly one way for two identities to be connected: a route from a writer to a reader, in which the writer's output is delivered to the reader's input and no cell has two writers (§0, §5). That rule is load-bearing and correct for signals. It is not correct for everything a layer might mean.
Two established precedents show what the frozen rule is a special case of:
| Precedent | Rule | Why it exists |
|---|---|---|
| VHDL [V] | std_ulogic is unresolved: two drivers is a compile error. std_logic is resolved: a resolution function reduces many drivers to one value. | a three-state bus has many drivers and is not a bug |
| Modelica [M] | at a connection point, potential variables are set equal and flow variables sum to zero. Connections are acausal — no direction is chosen. | a wire has no writer; Kirchhoff's laws do not name a source |
Draft rule D2. Single-writer-per-cell is not repealed. It is reclassified as a property of a port, and two further port kinds are proposed alongside it:
| Port kind | Admits | Meaning of a connection | Status |
|---|---|---|---|
| Owned | at most one writer | the writer's value becomes the reader's | frozen — this is today's rule, unchanged (§0) |
| Resolved | many drivers plus a named resolution function | all drivers contribute; the function determines the value | draft |
| Acausal | many participants, no drivers | potentials equal, flows sum to zero across the connection | draft |
What this buys. Power buses, shared physical media, contention on a road segment, and summed loads all stop needing to be faked as chains of relays. What it costs. The within-period REACT fixpoint (§8) is currently a single-pass settle over owned cells. Resolved ports still settle in one pass, provided the resolution obeys the laws in §D2.1 below. Acausal connections do not settle by passing values at all: they require solving a system. That is a different execution model, which is what §D4 is for.
Three resolver contracts, and only one of them is a join
An earlier version of the paragraph above said resolved ports settle in one pass "provided the resolution function is associative, commutative and idempotent — the same three laws §9 already demands of fact-merge, which is not a coincidence". The three-law phrasing was borrowed from the CRDT literature because it was nearby, and the borrowing hid a distinction. It is a coincidence, at least in the third law, and reusing §9's laws here quietly forbids resolutions that are correct.
Idempotence exists to make redelivery harmless. That is its entire job.
In §9's evidence merge, a replica cannot know how many times a fact will reach it
— once, or twice by two paths, or again after a partition heals — so the merge
must not be able to tell, and x ⊔ x = x is what
guarantees it. Driver-set resolution has no such problem. The set of drivers
reaching a terminal is fixed by the topology, the topology is in the sealed
artifact, and §D9 makes even a changed topology a deterministic function of the
ledger prefix. The multiplicity is known exactly. A law that pays for an
unknown you do not have is not free — it is paid for in expressiveness.
The cost of the borrowed law is concrete rather than theoretical. An
idempotent resolution cannot distinguish one driver from two — that is
precisely what x ⊔ x = x says. So it can never
express "two enabled drivers on this bus is a fault", which is an ordinary
thing a hardware profile wants to say and a real bus-contention rule.
Dropping idempotence was the right correction and not the whole of it. The first version of this section kept associativity and commutativity, on the grounds that they let the canonical order "be chosen without changing the answer" — and that sentence refutes itself, because §11 already chose the order. If the input is one normatively-ordered map, there is nothing left for those two laws to protect either. They were retained by the same reflex that supplied idempotence: the laws came as a set, so they were kept as a set. Below they are separated by what each one actually buys.
| Contract | Input | Required properties | What it buys |
|---|---|---|---|
| Canonical-map resolver | one map, source terminal → drive value, in §11's existing terminal order | pure, total, deterministic, plus a declared value for the empty map | the semantic contract. Sufficient for a defined, replayable result. Admits positional resolutions — "the strongest driver wins, ties are a fault" — that no fold expresses |
| Parallel-fold resolver | the same map, folded in any order, partitioned, or resolved incrementally | associative, commutative, total, plus a declared value for the empty map | an implementation permit, not a meaning. A profile pays these two laws to buy the freedom to split the driver set across workers or reuse a partial result |
| Join resolver | values arriving in unknown order and unknown multiplicity | associative, commutative, idempotent, with an identity | redelivery safety. This is §9's evidence merge and §D9's grow-only fact set, and it is frozen |
A profile declares which one it is writing —
resolver_kind = canonical_map or
parallel_fold — and that declaration is checkable
against a finite table by §D6.1's validators rather
than trusted. The two are not ranked: a parallel_fold
is the stronger claim and the weaker expression, and a profile that will never
partition a bus of four drivers has no reason to make it.
std_logic answers 'Z';
another profile may answer absence, and another may answer that it is a
fault. All three are legitimate and they are not interchangeable, so the value is
a required declaration on the table, not a default the runtime picks. A
table that omits it is rejected at profile-load, not at the first quiet
period.std_logic resolution [V]
is idempotent — resolve('1','1') = '1' — and
remains a perfectly good member of the wider class, and it satisfies the
parallel_fold contract as well. The change is that
these become properties a particular table may have and may trade
on, rather than admission requirements every table must satisfy. And because
§D6.1 makes these tables finite and enumerated, whether a given one is
associative, commutative, total or idempotent is decided by counting over the
type, not by proof — so a profile claiming
parallel_fold can be held to it mechanically,
which is what makes the split worth having rather than merely tidy.§D3Route axes are not route texturesdraft
The four frozen textures (§5) are guarantee classes:
-- settles this period,
~~ does not,
== requires evidence,
!! carries failure. That is a small closed set, which
is why glyphs work for it.
A recurring proposal is to add textures for domains — a road texture, a radio texture, a money texture. Draft rule D3 refuses this, on the following grounds.
ROS 2 [R] settles a similar question for message transport by exposing seven independent policies — reliability, durability, history, depth, deadline, lifespan, liveliness — any combination of which is meaningful. The general shape of a connection has at least these orthogonal axes:
| Axis | Range |
|---|---|
| Domain | spatial, informational, authority, energy, economic, evidential … |
| Directionality | directed · bidirectional · acausal |
| Cardinality | 1:1 · 1:n · n:1 · n:m |
| Timing | settles this period · settles later · continuous |
| Capacity & cost | unbounded · metered · priced |
| Reliability | guaranteed · lossy · adversarial |
| Geometry | abstract · metric · embedded in a coordinate frame |
| Schema | untyped · typed payload · verified payload |
Eight axes of two to six values each is thousands of combinations. No glyph
vocabulary survives that, and the attempt would either explode the textures or —
worse — quietly overload them, so that -- means
"synchronous" on one page and "physical" on another.
Draft rule D3. Textures stay four and stay timing-and-evidence only. Everything else about a connection is carried by attributes on the relation, validated against the world's profile (§D6). A connection is therefore a named relation with attributes, of which the texture selects only the settlement discipline.
§D4Walls are where the model of computation changesdraft
§D2 ended with a problem: acausal connections need a solver, not a settle pass, and §8's period cycle is a settle pass. Introducing a solver into the period cycle would change the execution model of every world, including the ones that do not want it.
Ptolemy II [P] answers exactly this question. In Ptolemy a director implements a model of computation, and an opaque composite actor carries its own local director which need not match the director of the model containing it. Hierarchy is therefore the mechanism by which one model can contain a differently-scheduled model — discrete event outside, continuous time inside — without either one contaminating the other.
Draft rule D4. WRL already has that construct, and it is the wall (§10). A wall may enclose a sub-world evaluated under a different profile, and the wall is the only place where the model of computation may change. Within a wall, one discipline; across a wall, a declared boundary with declared effects.
Consequence for §10. The boundary-safety law ("undeclared effects cannot cross walls", §12) becomes strictly more load-bearing: it is what stops a solver inside a wall from writing cells outside it. If walls are to carry profiles, boundary safety has to be enforced per profile pair, not globally — an open problem, not a solved one.
§D5Derives seal the answer, not the searchdraft
A topology language that describes a road network will be asked to route vehicles on it, and routing is an optimisation problem. WRL should not contain an optimiser. §13 already lists derives as Proposed; §D5 constrains what a derive is allowed to promise when it wraps one.
The two obvious external engines behave differently, and the difference is the rule:
| Engine | Determinism |
|---|---|
| SUMO [S] | deterministic by default; the RNG is seeded (default 23423) and the seed is an input. Same inputs → same run. |
| OR-Tools routing [O] | not reliably reproducible: solution quality varies run to run, and the order in which variables and constraints are created can change the result. |
Draft rule D5. A derive over an external engine seals its inputs, its solver profile, and its result. It does not seal the search. The sealed result must be accompanied by a deterministic checker that re-validates it against the world it came from. The solver is an untrusted oracle; the checker is the trusted part, and only the checker's verdict enters the artifact.
This is why the rule is stated here rather than left to the derive design: it determines what a derive is. A derive whose output cannot be checked more cheaply than it was produced is not admissible under D5, and several optimisation problems have no such checker. Those problems stay outside the language.
§D6Profiles carry domains, not construct kindsdraft
Every world already opens with a profile line (§4), and today exactly one profile exists. The draft's answer to "how does WRL model drones, or circuits, or supply chains" is: a profile, and nothing else.
A profile fixes the vocabulary that the frozen kinds are instantiated with: which roles exist, which relation names are legal, which attributes each relation carries and what they mean, which resolution rules are available, and which model of computation applies inside a wall that declares it. §D6.1 narrows the third of those from functions to finite tables, for a reason that turns out to matter to the whole promotion order.
The sketch below is what a mobility profile might look like. It is not valid WRL, and the conformance suite asserts that it is rejected:
profile wrl.mobility.v1
; one identity, present in five layers by construction (D1)
[drone:d7](battery=82, payload=4){pose, telemetry, command}
[segment:a12](capacity=3){occupancy}
[station:west](range=900){uplink}
[cell:c3](watts=400){supply}
[policy:safety](mode=halt){fault_in}
; road layer — attributed relation, not a new texture (D3)
[d7] --occupies(cost=1)--> [a12]
; comms layer — asynchronous, does not settle this period
[d7] ~~telemetry~~> [west]
; energy layer — acausal: no writer, flows sum to zero (D2)
[c3] --power--> [d7]
; fault layer — requires a supervision floor first (§16 step 6)
[d7] !!fault!!> [safety]
Every line of that sketch is currently a diagnostic. The profile name is
unknown, the five roles are unknown, ~~ and
!! are unpromoted, and relation attributes have no
grammar. Reading it as a promise would be a mistake; reading it as a
specification of what §16 has to deliver, in order, is the intent.
~~ (§16 step 2), attributed relations (§D3),
== (§16 step 3), and a supervision floor (§16 step 6).
Building the profile first would force each of those decisions to be made
implicitly, in a hurry, to satisfy a demo — which is the same failure §16.2
already refuses for [worker:w1].The profile mechanism cannot wait for expressions
The paragraph above lists what a profile declares, and one item in it is a problem: which resolution functions are available. A function needs a notation to be written in, and the expression notation is ladder step 5, while the profile mechanism is step 3. Stated plainly, that is a cycle in a document whose central claim is that its promotion order is a dependency order. Either step 3 secretly needs step 5 — in which case the ladder is wrong — or step 3 needs less than it says.
ProfileSchemaV1, a restricted,
purely declarative profile language containing no user-written
expressions and no user-written functions. It admits exactly:
1. primitive and enumerated types; 2. unit declarations and the dimensional relations between them; 3. relation signatures — domain, kind, orientation, endpoint arity, admissible roles; 4. endpoint-role constraints (which object roles may occupy which endpoint role of which relation kind); 5. bounded-resource annotations naming an attribute as a capacity subject to §D9's escrow class; 6. finite resolution tables over enumerated types — enumerated, not computed; 7. a closed set of built-in validators (range, unit compatibility, cardinality, uniqueness, non-negativity), referenced by name and implemented by the toolchain; 8. canonical default values, so that an omitted attribute has one serialisation and not two.
Everything in that list is data. It canonicalises and hashes by §11's existing rules with nothing added, and it is total and terminating by construction rather than by analysis — there is no program in it to analyse.
Why a table is enough, and not a compromise
The instinct is that a table is the weak version of a function, accepted
because the strong version is not ready. For the case that actually motivates
resolution, it is the other way round. VHDL's
[V] std_logic
resolution — the canonical example, and the one
§D8's sketch is written against — resolves a
nine-valued enumerated type, and its resolution is a 9 × 9 table.
Hardware engineers did not settle on a table because they lacked a language to
write a function in. They settled on it because for a finite type it is the
complete, inspectable, obviously-terminating specification, and a function would
be a less direct way of denoting the same object.
| Property | Finite table | User-written function |
|---|---|---|
| Canonical bytes | the sorted entries — §11 already hashes exactly this shape | needs a canonical form for syntax, which is a new and contestable artifact |
| Totality | checked by counting: |T|n entries present | undecidable in general; requires a totality checker before any profile can be trusted |
| Termination | nothing executes | requires the language to be restricted anyway, at which point the restriction is the design |
| Associativity, commutativity | verified exhaustively over the type | proved, assumed, or checked by sampling — and §D8's one-pass settlement depends on it |
| Reviewable by a domain expert | yes — it is the table already in the standard | only by one who also reads the expression notation |
The last row of that table is the argument in one line: a resolution rule over a finite type has to be exhaustively checkable, and exhaustive checking over an enumeration is counting. Expressions do not make this easier; they make it a proof obligation instead of a loop.
What the kernel cannot do, said before someone finds out
A restriction that is only advertised by its benefits is a sales pitch. The kernel genuinely cannot express:
- Resolution over an unbounded type. Summing currents on a
real-valued acausal net is not a table. That is correctly outside the kernel and lands with the solver wall (§D4), which is ladder step 9 — a solver was always going to be needed there, and routing it through a user-written resolution function would have put a numeric method inside a profile where §D4 cannot see it. - Derived or computed attributes —
cost = length ÷ speed. These wait for expressions at step 5, and until then an attribute is declared and supplied, never computed. - Conditional constraints — "capacity must exceed 2 when the segment is bidirectional". The built-in validators are unconditional. A profile needing more says so and waits.
Each of those arrives as a strictly additive extension once expressions exist, because the kernel's obligations are the ones an extension must preserve rather than assumptions it may violate: a computed attribute still has to canonicalise, a conditional constraint still has to be total, and a real-valued resolution still has to be replayable. Building the declarative floor first is what makes those requirements explicit before there is a notation tempting enough to skip them.
ProfileSchemaV1 needs a step-5 notation, so
§D8's attributed relations at step 4 have the type system they
require and the cycle is gone. It also sharpens condition
4 — resolution functions cannot
be expressed in the document and hashed with it — from an open worry into a
split verdict: for finite types it is answered, because a table is data
and §11 already hashes data. For unbounded types it stands, and it is now
precisely located at the solver wall rather than distributed over the whole idea
of resolution.§D7Falsification conditionsdraft
A design draft that cannot be wrong is not a design draft. Part II is abandoned or substantially rewritten if any of the following turns out to be true:
id="falsify-…" so that expansion can cite it, and the
conformance suite requires every citing heading to link here and refuses any
condition published there that is not declared in this table.| # | Condition | What it would falsify |
|---|---|---|
| 1 | Real domains require per-layer identity — the same entity genuinely needing distinct, independently-connected existence in two layers — often enough that "three identities and a wall" is unworkable | §D1. Node-alignment would have to be relaxed to replica-based layers, and identity would stop being structural |
| 2 | The results worth deriving have no checker cheaper than the search | §D5. Derives would either seal unverifiable claims or become useless, and optimisation stays permanently outside the language |
| 3 | Profiles need a central registry to interoperate — that is, two independently-written profiles cannot be composed without an authority arbitrating names | §D6. The "profiles, not construct kinds" answer would collapse into a governance problem, which a language cannot solve |
| 4 | Resolved-port resolution functions cannot be expressed in the document and hashed with it. Now split: §D6.1 answers it for finite enumerated types, where resolution is a table and §11 already hashes tables. It stands for unbounded types, and is located at the solver wall | §D2. Resolved ports would break §11 content identity and must not ship |
| 5 | Per-profile boundary safety across walls proves undecidable in practice | §D4. Heterogeneous execution would have to be dropped, leaving acausal connections nowhere to live |
| 6 | A broadened relation cannot serialise a plain directed route to today's exact bytes, so generalising the entity moves every published sem- id | §D8. Relation would become a 0.2 breaking change rather than an 0.1.x extension, and the ordering of the entire ladder would change |
| 7 | Admission decisions cannot be made from a sealed rule and recorded evidence alone — real federation turns out to require consulting live external state that no film can replay | §D9. The admission answer collapses back into the dilemma it replaced, and a world has to choose between a closed roster and a replayable film |
| 8 | The invariants worth enforcing on a changing topology cannot be escrowed — every useful bound needs global coordination at the moment it is checked, not an allowance divided in advance | §D9. Topology change would be safe only in a single-writer deployment, and the replication story would quietly be a single-node story |
Conditions 1 and 2 are the load-bearing ones. Both are answerable by building the mobility profile for real, which is why the first genuine test of Part II is a single fleet described across road, communications, dispatch, energy and fault layers, run on TRVM, with routing supplied by an external solver and checked on the way back in.
§D8Relation, not Edgedraft
Sections §D2 and §D3 have been quietly accumulating a debt, and this section pays it. The authored design draft (draft §19) closes the runtime ontology at exactly six entity types — Actor, Value, Edge, Boundary, Fragment, Stencil — and gives the third of them this shape:
Edge { source, destination, label, texture, payload, policy, provenance }
That is a binary directed pair. Every proposal in Part II so far needs something it cannot hold:
| Needed by | What it requires | Why Edge cannot carry it |
|---|---|---|
| §D3 attributes | capacity, cost, units, geometry on the connection itself | Edge has payload (what crossed) and policy (how it crosses), but no field for what the connection is |
| §D2 resolved ports | many drivers meeting at one terminal and settling by a named function | n separate edges into one destination is exactly the multiple-writer error §0 forbids; the terminal, not the edge, is the object with the resolution rule |
| §D2 acausal connections | three or more participants, no source and no destination, potentials equal and flows summing to zero | a source/destination pair asserts a direction that physically does not exist |
| §D1 multilayer | an occupancy relation between one drone and one segment that means something different from a message | label is a name, not a typed domain |
The three ways out, and the one taken
| Option | Verdict | |
|---|---|---|
| A | Broaden the entity. Rename Edge to Relation and generalise its shape. A directed binary route becomes the simplest specialisation of it. | taken |
| B | Add a seventh entity type alongside Edge. | rejected. §19's claim is "not five, not seven". An ontology that grows a member per unmet need is not an ontology, it is a changelog. Worse, two entities for one idea forces every later rule — canonicalisation, film schema, wall crossing — to be written twice |
| C | Encode nets as actors: a bus becomes an actor with n ports and a behaviour that resolves. | rejected. It works, and that is the problem — it works by making the connection into a participant, so a description of a circuit stops looking like a circuit. It also puts a solver inside an actor, where §D4 has no way to see it |
Draft rule D8. The runtime ontology keeps six entity types. The third is renamed and broadened:
RelationAllocation ; a tagged union — §D8.1 defines it
RelationRevision { domain, kind, endpoints, orientation,
texture?, attributes, policy }
Endpoint { terminal, role }
{ world_id, issuer, nonce }, and went on doing so
after §D8.1 replaced that flat triple with a
tagged union. It was found by looking at the rendered page, not by any check
— which is the third time in this section's history that one record has been
declared in two shapes and the second time the surviving copy was the retired
one. The defect is not the wrong fields; it is that a type had two definitions at
all. Below, the field list is gone and the reference stands in its place, and the
suite now takes a census of every record
declaration so that a second shape cannot appear quietly again.An allocation names the relation; a revision is its value. The
split is the subject of §D8 identity below, and the
revision is what replaces the old Edge. There is no
stored record joining the two: the current revision of a relation is
derived from the ledger, for the same reason
cardinality is derived from the endpoints.
texture is written texture?
because it is genuinely optional — the table's own row says it is absent
for an acausal relation, and a record that prints a field it does not always
carry is a record that will be implemented wrongly.
| Field | Range | Replaces |
|---|---|---|
domain | a profile-declared namespace — signal, digital, mobility, electrical (§D6) | — (new) |
kind | the predicate within that domain — SignalWire, drive, occupies | label, promoted from a bare name to a domain-scoped kind |
endpoints | an unordered set of Endpoint, size ≥ 2 | source, destination |
orientation | directed · symmetric · acausal | — (was implicit and always directed) |
texture | the four frozen textures (§5). Required for directed; profile-defined for symmetric; absent for acausal, where a solver wall governs settlement instead (§D4) | texture |
attributes | typed, unit-bearing values validated by the profile | — (new; this is §D3's requirement) |
policy | unchanged | policy |
(no provenance) | removed from the value and carried by the ledger event instead — §D8.3 | provenance |
payload leaves the relation deliberately. What
crossed a connection during a period is an event, and events already live
in the event ledger and the film (§10). A relation is the standing fact that a
crossing is possible; the payload is the record that one happened. Merging the
two is what made Edge simultaneously a topology
object and a message envelope.
Two fields, because a name is not a namespace
An earlier draft of this section carried domain
alone and let it absorb the old label. That was
wrong, and the sketch below shows why in its own syntax: a relation written
--drive(domain=digital)--> is carrying two
ideas, not one. digital is the namespace that says
which profile validates the attributes and what the units mean;
drive is the predicate that says what this particular
relation asserts. A road relation needs both in the same way —
domain=mobility,
kind=occupies — and they are not interchangeable.
Collapsing them means capacity on a road and
capacity on a bus are the same word validated by
nothing.
The surface spelling follows the split: a qualified relation is
--digital.drive-->, and today's
--sig--> is the unqualified form, resolved against
the profile's default domain to signal.SignalWire.
That is how the frozen syntax survives the generalisation unchanged.
Endpoints carry roles, because a hyperedge without them is ambiguous
A set of participants plus a 2:3 label does not
say which two of the five are the two. This is a solved problem: a
directed hyperedge in the standard formulation
[H] is an ordered pair of disjoint vertex
sets — a tail and a head — and the role-aware line of work on
n-ary knowledge representation exists precisely because extending binary
methods to n-ary relations without recording each entity's role loses
information that cannot be recovered. So the role goes on the endpoint, where it
is unambiguous, rather than being inferred from a position in a list.
| Role | Admitted by | Meaning |
|---|---|---|
source | directed | a member of the tail: it writes |
target | directed | a member of the head: it reads |
peer | symmetric | neither side distinguished; the profile says what the pairing obliges |
terminal | acausal | a member of one connection set — potentials equal, flows sum to zero |
Four roles, and two that were considered and left out. There is no
driver role: a driver is a
source whose target terminal happens to declare a
resolution function, and giving it a role of its own would let the relation and
the terminal disagree about whether resolution is in force. There is no
resolved role or orientation either, for the same
reason and with a precedent behind it — VHDL declares resolution on the
resolved subtype of the signal, never on the drivers
[V]. Resolution is a property of the terminal
being written to. Exactly one object owns that rule.
Endpoint order is presentation, not meaning
endpoints is a set, and this is
load-bearing rather than pedantic. The acausal connection in the sketch below
joins n1.pin, vcc.pin and
r1.pin. There are six ways to write those three
terminals down and they are the same circuit. If endpoint order were semantic,
two authors describing one physical net would seal two different
sem- ids, and the identity would be measuring
typing habits instead of topology.
So canonicalisation sorts endpoints by role, then terminal, with roles in the declared enumeration order above. This is the same discipline §11 already applies to sorted-key JSON and to omitted empty port groups — the canonical form decides an order so that the author does not have to have one.
For that sort key to be total, terminals have to be unique within a relation. An earlier draft said "at most once per role", which is the weaker rule and lets a contradiction through:
| Rule | Admits | Verdict |
|---|---|---|
| once per role | {(x, source), (x, target)} | the same terminal is simultaneously in the tail and the head |
| once per relation | rejected | correct |
The hypergraph precedent [H] settles it: a hyperarc is an ordered pair of disjoint vertex sets. Tail and head do not overlap, so the rule is:
source and target
terminal sets disjoint by construction, matching the hyperarc definition the
roles were taken from — and it makes the canonical sort key total, since role
alone can no longer be the tiebreaker.This does not outlaw an identity relating to itself, which is an ordinary
thing an actor does. A self-loop uses different terminals on the same
object — a.out as source and
a.in as target — and remains legal, because those
are two terminals. What is now impossible is one terminal holding two
contradictory positions in a single relation. If a profile has a genuine use for
a reflexive terminal, it should declare that as an explicit feature with
its own meaning, rather than receive it as an accident of a rule about
uniqueness.
The canonical form is a rule, because prose was not enoughdraft · added after a review found the implementation disagreeing with this section
Everything above this heading was stated as prose and tables, and an
implementation read it and got the central record backwards. The
relation identity kernel shipped endpoints spelled
{ terminal: "p0", role: "sig_out" } — the object in
the terminal slot and the port name in the role slot. It round-tripped
every V1 edge exactly, it passed every check on the page, and it had implemented
none of this section: no source, no
target, endpoint order still semantic, terminals not
unique, and an undirected orientation appearing in no
table here.
What makes that worth a numbered rule rather than a bug fix is why nothing caught it. The pending register holds model-layer debt at zero, and it was at zero and green the whole time — because a ratchet enforces zero debt among the properties it knows about, and none of these were registered. The obligations below are the missing rows. They restate what the paragraphs above already say, in the form the rest of this page requires of a claim: something a check can falsify.
RelationRevision has exactly one canonical form, and
a conforming implementation reaches it by:
- Port-qualified terminals. A terminal identifies a port, not an
object, and it carries that structure in the record:
{ object_id: "p0", port: "sig_out" }. The spellingp0.sig_outused elsewhere on this page is display — a rendering of that record, never its encoding. Where a legacy V1 edge leaves the port implicit, the import lifts it out of the edge kind's frozen port pair into the two fields; a consumer of a revision never parses a terminal, because a terminal was never packed. This is the same rule as derived, not asserted, read one level down: recovering structure by splitting a string is a sign the structure was discarded on the way in. - Semantic roles. Every endpoint's role is one of
source,target,peer,terminal. A port name is never a role. - Role legality. Each orientation admits only its own roles —
directed: source and target;symmetric: peer;acausal: terminal. The orientation vocabulary is those three and no others. - Role completeness. A
directedrelation carries at least onesourceand at least onetarget. A hyperarc is an ordered pair of vertex sets; neither may be empty. - Terminal uniqueness. A terminal appears at most once per relation, regardless of role — the normative rule stated above.
- No texture on acausal. An acausal connection has no writer, so a solver wall (§D4) governs settlement and a texture would be a second, disagreeing account of it.
- A texture on directed. The other half of the same row, and it is
enforced on the same table. A
directedrelation states a texture, because §5's textures are guarantee classes and a relation that names none makes no statement about whether it settles within the period. - Order-independent hashing. Endpoints sort by role in the enumeration
order declared above, then by terminal; the hash is taken only after
normalisation. Reordering a source's endpoints must not move
revision_id. - Absent optionals absent. A missing
textureis a missing key, never a null.
port
field against the kind rather than assuming it.That second texture rule was, for one version of this section, deliberately
not in the list — filed instead as a V2 obligation
on the argument that a frozen artifact's edge records no texture field, so
inferring one from -- being the only texture V1
lowers would invent a value the artifact does not carry. The argument was
careful and it was wrong, in a way worth leaving on the page: a V1 edge does not
omit a texture, it elides one, because §5 gives V1 nothing else to
mean. That is the same relationship the artifact has to
orientation, which the adapter had been restoring
throughout without anyone calling it authorship. What the mistake cost is the
instructive part — a rule this page states was left with no
implementation, so §D8's texture row shipped as one enforced half and one
commented half, and a table with a commented row has stopped being a table.
The adapter's field map records the correction
at the point where it was actually made.
A relation needs a name that survives its value
Everything above describes what a relation is. It does not say how to refer to one, and until D9 that omission was invisible, because a fixed topology never needs to name a relation twice. The moment relations can be attached, revised and retired at runtime, the omission becomes the load-bearing defect in the pair of sections.
The tempting answer is the one the rest of this language already uses: address a relation by the hash of its content, as §11 addresses a world. Applied here it fails twice, and both failures are ordinary rather than exotic.
| Failure | Case | What breaks |
|---|---|---|
| Distinct relations collide | two physical cables between the same pair of terminals; a redundant link deliberately provisioned as a second path; two lanes of one road segment carrying the same mobility.occupies kind | content-identical relations are the same relation under a content address, so the model cannot say there are two, cannot fail one and keep the other, and cannot report which one a fault was on. §D8 claims WRL supports multigraph relations; a content address quietly withdraws the claim |
| Revision destroys the name | operation 5, replace relation attributes — a road segment's capacity drops from 3 to 2 after a lane closure | the attributes are the content the address was computed over, so changing them changes the name. The ledger fact then cannot say "this relation was revised"; it can only say one relation vanished and an unrelated one appeared, which is exactly the history a replay needs to not lose |
The second failure is the sharper one, because it is self-inflicted: nothing external went wrong, an authorised operation simply mutated the bytes being used as the identity. That is a category error, not a bug, and it has a standard resolution — separate the thing that persists from the thing that changes.
relation_id, minted once from an
allocation and never recomputed, and a succession of
content-addressed RelationRevisions. The
relation_id is the lifecycle handle: it is what a
ledger fact, a diagnostic, a fault report and a film row name. The revision id is
the value: it is what canonicalisation hashes and what a merge compares. Two
parallel cables have two relation_ids and may share
one revision id, and that is the correct description of two identical
cables.Where the name comes from, without a cycle
The first draft of this rule left the minting mechanism open and offered two options, one of which was "the creating event's canonical id". That option is not viable, and the reason is worth keeping rather than deleting, because it is a cycle that reads as a reasonable sentence:
relation_id = H(attach_event)
attach_event_id = H(operation, arguments…) ; arguments include relation_id
Canonical event identity is a content address over the operation and its arguments (§D9), and the id being minted is one of those arguments. Neither hash can be computed first. An identity scheme needs an allocation source that is prior to the event, not derived from it:
RelationAllocation =
InitialRelationAllocation ; present at period 0
| GrantedAllocation { grant_id, local_counter } ; attached at runtime
InitialRelationAllocation =
NamedInitialAllocation { world_id, relation_name } ; IR 2.0, authored
| LegacyEdgeAllocation { world_id, kind, src, dst } ; V1, from the seal
relation_id = H("WRL_RELATION" ‖ "named-initial" ‖ world_id ‖ relation_name)
relation_id = H("WRL_RELATION" ‖ "legacy-edge" ‖ world_id ‖ kind ‖ src ‖ dst)
relation_id = H("WRL_RELATION" ‖ "granted" ‖ grant_id ‖ local_counter)
{ world_id, issuer, nonce }
triple, with issuer meaning "the world itself" at
period 0 and a claimant at runtime. That is a tagged union wearing a flat record,
and it hides the fact that the cases have different preconditions: an
authored name is unique by parse, a V1 edge key is unique by seal-time validation,
a granted counter is unique by §D8.4. Writing the
tag makes the variant part of the preimage, so the families cannot collide even if
a world_id and a grant_id
ever coincided as bytes. world_id is absent from the
granted form because grant_id
already covers it — restating it would be the same fact in two places,
one line apart, in a section about not doing that.AuthoredAllocation, whose uniqueness clause read the
authored relation name, where the surface provides one; where it does not, the
triple — a disjunction inside a variant of a tagged union, which is precisely
the shape the paragraph above congratulates itself for having removed one level
up. The consequence was not cosmetic. A V1 artifact records only
{ kind, src, dst }; it has no field for an authored
name. Two sources naming the same edge differently would seal to identical bytes
under one sem- id and still mint different
relation_ids — a sealed world failing to determine its
own future semantics, which is the one thing the seal exists to do.(kind, src, dst) — the only relation-distinguishing
material the sealed artifact actually contains.
NamedInitialAllocation is stated here so the eventual
surface has a preimage waiting rather than an improvisation, and it is
registered as awaiting that surface. Nothing may emit
it until a relation name is something an author can write and a seal can carry.
The two are separate variants rather than one parameterised case because they
have different availability, and a union that hides an availability
difference is how a name gets minted from a field that is not there.- Which variants exist. All three. The equations above are total: given
a well-formed allocation of any variant,
relation_idis defined, and it is defined the same way for everyone. - Which a trusted importer may construct. Only
legacy-edge, because that is the only preimage a frozen artifact supplies every field of. - Which an authoring surface may emit. None, today — there is no syntax for a relation name and no machinery for a grant.
kind ‖ src ‖ dst cannot tell
("ab", "c", …) from
("a", "bc", …), and the legacy-edge variant is the
first preimage on this page with three adjacent free-form strings in it. Every
‖ above denotes an unambiguous canonical
serialization of the tagged record — the same
§11 canonicalization the artifact itself is hashed under, with
the variant tag as a field. Read as literal concatenation these equations would be
collision-prone by construction, and the collision would be between two relations
in one world, which is the exact failure the tag was added to prevent.The cycle is broken because relation_id is a
function of the allocation, and the allocation is an ordinary argument of the
event. The event id may still be taken over all its arguments with no
exception carved out, and the derived id adds no information to that preimage
because it is recoverable from one of them. The alternative — defining the event
id over a preimage that deliberately excludes the derived
relation_id — is also sound, but it makes the
sentence "event identity hashes the operation and its arguments" false, and a
rule with one silent exception is a rule that will acquire a second.
An allocation that exists before the world runs and one that is handed out while it runs do not arrive by the same route, so they are not the same event:
InitialRelationDeclared carries an InitialRelationAllocation ; sealed, period 0
RelationAttached carries a GrantedAllocation ; proposed, at runtime
; both are declared once, in §D8.3's ledger below
RelationAttached carry an
InitialRelationAllocation would have given the
attachment record a mode in which its
provenance and
proposal envelope are meaningless, and a record with
a mode where half its fields do not apply is a union that was not written down.
Splitting them also makes the milestone honest: a V1 implementation can emit
InitialRelationDeclared today and cannot emit
RelationAttached at all, and no field of either has to
be left blank to say so.RelationAttached carries the
allocation, not both ids. Carrying relation_id
alongside the allocation it is computed from would put one fact in two places —
the defect §D8 rejects for cardinality and
again below for revision history. A validator
computes the id from the allocation; there is nothing to disagree with. Neither
line above is a record: they name which variant each event admits, and the one
place both are declared is below. Showing
part of a record is fine; showing two different wholes under one name is how a
reader ends up implementing whichever they reached first.| Relation | Variant | Where uniqueness comes from |
|---|---|---|
| Present at period 0 — named in the sealed world | NamedInitialAllocation | the authored relation name, unique by parse. The surface that writes one is §D8.12, and it arrives with the ir 2.0 encoding: [clock_feed]: [p0] --sig--> [r0]. A world at IR 1.x has no field a name could live in, so this variant appears only under IR 2.0 — §D8.11 is the authority on which encodings may emit it |
| Present at period 0 — V1, derived from the seal | LegacyEdgeAllocation | the edge key (kind, src, dst), which is all the artifact records. Legal only where that key is unique within the world, which is the same precondition the V1 projection already depends on — one condition, stated once, doing both jobs. Checked at seal time, by the validator that already walks the artifact |
| Attached at runtime — §D9 operation 3 | GrantedAllocation | a counter drawn from a grant's range. Two grants cannot collide whatever their ranges, because grant_id is in the preimage; the obligation is only that a grant is issued and never restored — §D8.4 |
A monotonic counter is not an allocation source
The first version of the runtime row said "a claimant-owned monotonic sequence — uniqueness is then local and needs no coordination". That is the standard move, and it is wrong here for the standard reason: a counter is unique per owner, and a claimant identity does not name an owner, it names a role that a failure can leave two processes playing at once.
claimant C, counter at 42, durably recorded
partition; C is restored from that record on two hosts
host A allocates (world, C, 43) → relation_id X ; a power line
host B allocates (world, C, 43) → relation_id X ; a different power line
Two distinct relations, one id. This is not an exotic failure — it is stale restoration, which is an ordinary consequence of the partitions §D9 is explicitly written to survive, and it destroys the exact property D8.1 exists to provide. The counter must be scoped by something that cannot be duplicated by restoring a snapshot.
AllocationLease { issuer, namespace_prefix, valid_range,
authority_receipt } — and authority_receipt is
a checker's conclusion that the lease may exist, which is precisely the
thing §D8.3 removes from a value one section earlier. In the same commit. The
consequences are the same ones D8.3 lists: two authorities granting the same
range would produce different lease identities; re-signing the evidence would
change the lease; and a receipt that names the lease it justifies is another
hash cycle of the kind the mint section exists to
break. A rule is not learned until it is applied to the case that was not in
front of you when you wrote it.AllocationLease, with fields
lease_id, a LeaseGranted
event and a LeasedAllocation variant tagged
"leased". The name is retired, and not for tidiness.
A lease is a time-bounded exclusive hold, and a reader who sees the word
correctly expects the machinery that goes with one: a fence token, an agreed
clock, a rule for in-flight work at expiry, an answer for the zombie holder. This
record delivers none of that and — as the failure model below shows — needs none
of it, because expiry here is not load-bearing for safety. Calling it a lease
promised a mechanism and then omitted it, which is the defect this section was
already written to prosecute in someone else's design. It is a
grant: issued once, never restored, unforgeable, and exhausted rather than
expired.AllocationGrant { world_id, allocator_incarnation, namespace_prefix,
counter_range, validity, granting_policy_id }
grant_id = H(canonical AllocationGrant)
GrantIssued { grant_id, provenance, offered_evidence } ; the ledger event
GrantedAllocation { grant_id, local_counter } ; local_counter ∈ counter_range
Every field left in the grant affects what future allocations are legal
— which is §D8.3's own test for what belongs in a value rather than on an event.
counter_range is spelled out rather than left as
valid_range beside a validity
field, because two fields whose names are near-synonyms for different things is
how a reader ends up checking the wrong one.
granting_policy_id stays in the value for the reason
D8.3 gives about semantics wearing a provenance label: the governing policy is
not a record of how the grant was obtained, it is a constraint on what the grant
permits.
D8.4-a, the world law. No two distinct transition intents may consume the same
(grant_id, local_counter) pair.D8.4-b, the issuer obligation. An
allocator_incarnation is issued — never
derived, never recovered. A grant is never part of restorable state. Restoring an
allocator therefore means obtaining a new grant, hence a new
grant_id, hence a name space disjoint from the old one
by construction.Why a grant was issued lives in the
GrantIssued event
and its §8 acceptance receipt, never in the grant. What is not an
allocation source is any counter recoverable by restoring state, because
restoring state is how the second allocator is created.The earlier statement of this rule said the range is valid "within a namespace no two live allocators can hold at once". That named a guarantee and established nothing: mutual exclusion between live processes is precisely what a partition removes, so the rule was asking the failure to solve itself. The version above is narrower on purpose, and separating the four concerns that were bundled into that one sentence shows why the narrow one is enough:
| Concern | D8.4's job? | What settles it |
|---|---|---|
Identity uniqueness — two allocations never yield one relation_id | Yes — this is the whole rule | grant_id is in the preimage. Two different grants cannot collide at all, whatever their ranges, even overlapping ones. Only allocations drawn from the same grant can ever contend, which is what makes D8.4-a a statement about one grant rather than about the world |
| Allocator mutual exclusion — at most one live process per role | No, and this is the correction | Not required. A surviving old allocator draws under its old grant_id and its successor under a new one, so the two cannot produce the same name. The zombie is harmless — provided D8.4-b holds and the successor did not restore the predecessor's grant |
| Capacity — the range runs out | Yes, but as liveness, not safety | Exhausting counter_range stops allocation; it never breaks an identity already minted. The remedy is another grant, which is another disjoint space |
| Revocation and expiry | Yes, and it is not fencing | validity governs when new counters may be drawn. It never invalidates a drawn relation_id — D8.1 says identity survives — so expiry carries no safety weight and needs no agreed clock |
The second and fourth rows together are the reason the record is a grant and
not a lease. The five questions any expiring lease has to answer — whose
clock; what becomes of work in flight at expiry; is a renewal the same lease or a
new one; how is a holder that missed its revocation fenced; what if the fence
message is lost — mostly do not arise here. Expiry stops new draws and
touches nothing already drawn, so in-flight work needs no rule. A renewal is a
different value and therefore a different grant_id, so
"same or new" is answered by construction. There is no fence because a stale
holder cannot collide with anyone. The only question that survives is the first,
and it survives in weakened form: a disagreement about the clock can cause a draw
to be refused or wrongly admitted, which is an availability and
policy matter, and cannot make two relations share a name.
validity without a type. Showing a clock
disagreement cannot cause a collision is not the same as saying what the field
holds. As written, an implementer could reasonably store a wall-clock instant
read at draw time — and a replayer would then have no way to decide whether a
draw was admissible, because that instant is nowhere in the film. A field that
only the original run can evaluate makes replay consult the environment,
which §D9.1 refuses for exactly the reason it would refuse here.Basis for validity | Replayable? |
|---|---|
| a range of logical periods | Yes — the period is the ledger's own ordering, so a replayer already knows it |
| a clock observation admitted as an event | Yes — the observation is in the ledger, so the replayer reads the same value the original run did |
| an authority-issued expiration event | Yes — expiry becomes a fact with a position in the ledger rather than a threshold anyone must evaluate |
| another basis pinned by the profile | Yes, if the profile is one of §D9.1's pinned references, which is what makes it recoverable from the film |
| a wall-clock instant read at draw time | No — nothing records it, so admissibility is not a function of the film |
validity is therefore owed a basis, and it
is owed one before grants can be implemented rather than after. The rule stands
unamended: expiry still carries no safety weight, and none of these bases changes
D8.4-a. What they change is whether a replayer can reach the same verdict
about a draw as the original run — and a rule whose admissibility test is
unreplayable is one that will be enforced differently on every host without any
of them looking wrong. The
register carries this as its own property, separately
from the rule, because it is a debt against the field rather than a doubt about
the law.
Three fixes come with the content address, and the third is the one that was
actually missing. grant_id is now named by the
allocation. The earlier { world_id, issuer, nonce }
form obliged a validator to infer which grant authorised a given
allocation by scanning history — which is unambiguous only until an issuer holds
two ranges, or a renewal overlaps its predecessor, or a grant is replayed. An
allocation now says which grant it came from, and checking it is a lookup rather
than a search. world_id can leave the relation
preimage for the same reason: it is inside grant_id
already, so §D8.5's world-scoping survives
without the field being written twice.
This has a consequence worth stating plainly rather than discovering later: under D8.4, attaching a relation during a partition requires a grant obtained before it. That is a real restriction, and it is the honest one — the alternative is not "attach freely", it is "attach freely and discover the collision at merge", which the escrow discussion in §D9 already rejects for bounded resources for the same reason.
| Fact | Home |
|---|---|
| what range exists, and until when | AllocationGrant |
| which policy governs allocations from it | AllocationGrant — it constrains future behaviour |
| who proposed the grant, and why | GrantIssued — provenance |
| evidence offered for the grant | GrantIssued — offered_evidence |
| why the checker accepted it | the §8 acceptance receipt |
| which grant minted a given relation | GrantedAllocation.grant_id |
| that no two intents shared one drawn name | a ledger fold — D8.4-a is a replay property, not a field |
What "deterministic" has to mean here
An earlier version of this rule said a nonce "must not be random", which is
too strong and misstates the requirement. Draft
§21 already specifies /entropy as seeded
streams whose seeds are recorded, so a random value drawn through that
boundary is replayable. The prohibition is narrower and different:
Recorded entropy passes that law and is still a poor choice, for a reason that has nothing to do with replay: an unpredictable nonce cannot be recomputed by the proposer, so a retried proposal allocates a second relation instead of being recognised as the same one. What an allocation source actually needs is stronger than replayability: the proposer must be able to reproduce the same nonce for a retry of the same proposal, so that proposing twice is idempotent rather than doubling. A grant-scoped counter satisfies that by being held with the pending proposal until it settles; a fresh entropy draw per attempt does not. That is the real argument for the counter, and it is not the argument the first version gave.
"Until it settles" leaves the wrong question open
The sentence above says the counter is held with the pending proposal until it settles. It does not say what happens after it settles the wrong way, and the omission is not cosmetic — the obvious reading is that a rejected allocation returns to the pool, because it was never used for anything. That reading destroys D8.1:
allocation A42 appears in a proposal to attach cable X → rejected
A42 returns to the pool
allocation A42 appears in a proposal to attach cable Y → accepted
relation_id(A42) is one stable identity naming two unrelated intents,
and the film contains both
Nothing detects this. Both proposals are individually well formed, the second one is accepted by a rule that has no memory of the first, and a replay reconstructs a world in which one name meant a cable that was refused and later meant a different cable that was not. The refusal is recorded — that is what §D9.1 closure guarantees — so the collision is visible to anyone reading the whole ledger and invisible to every rule that reads a period.
RelationBirthKey, which
carries no period. It may be re-proposed under that same birth key, at any later
boundary, and may never be recycled for another relation — regardless of
acceptance, rejection, timeout, crash, or later retirement. Observation consumes
the allocation; acceptance is a different question and does not govern this
one.| Situation | Outcome |
|---|---|
| same attach retried at a later boundary | permitted — same birth key, new schedule. This is the retry the nonce law is designed to make idempotent, and it is why the birth key has no period in it |
| same attach, more evidence attached | permitted — same birth key and schedule, a different proposal envelope. An audit trail is not a lifecycle event |
| same allocation, a different initial revision | allocation collision — a different birth key, so a diagnostic, never a race |
| attach succeeds, then the same birth appears again | refused — not as a collision but because the relation already exists. Same birth, already spent |
| proposal rejected | allocation stays consumed. A refusal is an outcome, not an erasure |
| proposal times out or the proposer crashes | allocation stays consumed, for the same reason |
| relation later retired | allocation stays consumed — retirement is not deletion (historical representation) |
| a retired relation is later reactivated | a separate operation, not another birth. It does not spend a name, and it may not reuse the retired relation's allocation to avoid pretending it is one |
As first written, D8.6 was one sentence doing two jobs of very different standing. One of them a replayer can check from the film. The other one nothing outside the proposer can ever see. Leaving them fused made the whole rule read as unverifiable, which is worse than either half deserves:
| Half | The rule | What establishes it |
|---|---|---|
| D8.6-a the world law | The first observation of an allocation permanently binds it to that observation's intent key. Any later observation of the same allocation under a different intent is an allocation collision | Replay, from the ledger alone. Fold the ledger keeping a map from allocation to first intent; a second entry that disagrees is the diagnostic. §D9.1 closure is exactly what guarantees the fold sees every observation |
| D8.6-b the proposer obligation | A proposal must not be externally emitted before its reservation is durable | Nothing external, ever. It is unobservable by construction — the trace of a correct proposer and the trace of one that records after emitting and happens not to crash are identical |
AllocationReserved { grant_id, local_counter, birth_key }
; durable, proposer-side, written BEFORE emit — and written ONCE,
; because drawing the counter and binding it are the same fact
{ allocation, intent_key }, and
the shape hid a crash window. An allocation is a
GrantedAllocation that has already drawn a
counter, so the pairing implied two steps: take the next counter from the grant,
then bind it to an intent. A proposer that persisted those separately can crash
between them, and both orders lose:
counter advances, reservation is lost — the name is spent and nothing
records what for, so recovery cannot tell a burnt name from a free one; or
reservation persists, counter is offered again — and the proposer hands
the same name to a second birth, manufacturing the collision
D8.6-a will correctly diagnose and cannot undo.
Flattening the record makes the two a single append-only write.local_counter is read off the
durable reservation set — the smallest value in
counter_range that no
AllocationReserved record claims. Keeping a separate
authoritative integer beside that set would be a second home for one fact, and
the two can disagree after exactly the crash this record was reshaped to
survive. One fact, one home: the reservation log is the counter.D8.6-b being unverifiable is not a weakness in it, because it is not a safety property of the world. D8.6-a is that, and D8.6-a holds whatever proposers do. D8.6-b is a recovery property of a single participant: written the other way round — emit, then record — a crash in the gap leaves an allocation that is live in everyone else's ledger and free in the proposer's own, so the proposer's next honest act re-issues it and manufactures the collision D8.6-a will correctly diagnose and cannot undo. The obligation exists to stop a well-behaved participant from becoming a source of true diagnostics.
An implementation that wants the obligation checked anyway can buy it, and the price should be on the page rather than discovered. The two profiles differ only in what counts as the first observation in D8.6-a; both satisfy it unchanged:
| Profile | The reservation is | First observation is | Replay can check | Price |
|---|---|---|---|---|
| Local — the default | a proposer-private durable record | the proposal | D8.6-a | one local flush before emit |
| Published | itself an observed ledger event | the reservation | D8.6-a and the ordering — a proposal whose allocation has no earlier reservation entry is refused | a round trip before every attach, and a name spent whether or not the attach is ever proposed |
This is not a new mechanism invented for relations. It is the mechanism every other entity in the ontology already has, and writing the four out together shows that the third one was the anomaly:
| Entity | Stable identity | Replaceable value |
|---|---|---|
| Actor | the object id — durable across every config change, which is what "durable identity" means in §1 | role config, and under draft §26 the behaviour |
| Relation | relation_id (this rule — previously missing) | RelationRevision |
| Boundary | the wall id | its open/closed state, changed by operation 7 |
| World | the sem- id, which §D9 keeps fixed across every topology change | the active configuration at period n |
The world row is the argument in miniature. §D9's central move is that the world identity does not move when the world's structure changes, because identity and configuration are different things. D8.1 applies the same reasoning one level down. A rule that is right for the whole is not obviously wrong for a part.
What the ledger facts have to carry
Three of §D9's eight operations act on a relation, and each becomes expressible only once both ids exist:
RelationAttached { allocation, revision_id, provenance, offered_evidence }
RelationRevised { relation_id, expected_prior_revision, next_revision,
provenance, offered_evidence }
RelationRetired { relation_id, expected_prior_revision,
provenance, offered_evidence }
The allocation on
RelationAttached is a
GrantedAllocation and nothing else. Relations that
were already there when the world was sealed have their own fact, which is not an
operation at all:
InitialRelationDeclared { allocation, revision_id }
It has no provenance and no
offered_evidence because there was no proposer: the
seal is the whole story of how it came to be, and a
sem- id already records that story exactly. It is not
one of the eight operations, it does not need a grant, and it cannot be refused —
by the time any period runs it is already true. What it shares with the three
above is the shape of its name: an allocation, and the revision that allocation
was born holding.
The last two fields on the operations are
§D8.3:
provenance records how the operation came to be
proposed, and offered_evidence is what the
proposer put forward to justify it. Both are arguments of the operation
and are therefore covered by the event id. What the checker concluded is
not here; it is in the acceptance receipt.
Each still obeys §D9's eight obligations unchanged, and two of them get
sharper rather than harder. Canonical event identity is a content address
over the operation and its arguments — which now includes a stable
relation_id (via its allocation), so two hosts
proposing the same revision agree on the event id without having to agree on
hashing the attributes first. Conflict rules become statable: two
RelationRevised facts in one period naming the same
relation_id with different
expected_prior_revisions is a detectable conflict,
not a race that the smaller hash wins. Without a stable id there is no way to
notice that the two facts are about the same relation at all.
One fact, one home — including this one
The first version of this rule wrote history down in three places, and
the mistake is instructive because each one looked individually justified: a
current_revision field on an instance record (so the
active value is cheap to read), a predecessor? field
on the revision (so the chain survives a ledger read out of order), and the
prior → next pair on the ledger fact (so revision is
an edit rather than a coincidence). Three homes for one fact means they can
disagree, and nothing said which one wins:
current_revision = R3
R3.predecessor = R1
RelationRevised { prior = R2, next = R3 } ; which of these is true?
This is the defect §D8 rejects one section later for cardinality, committed in the section that rejects it. The rule is the same in both places, so the resolution is the same:
RelationRevision is a pure immutable value
with no reference to any other revision. Lifecycle history lives only in
the ledger events. The active configuration is derived:
current_revision(relation_id, ledger_prefix) is a
fold over the prefix, and any materialised
RelationInstance is a cache of that fold — derived,
non-canonical, and never hashed independently.Renaming prior_revision_id to
expected_prior_revision is part of the same
correction rather than cosmetic. The field is not a record of history — the
ledger already is that. It is a precondition: the operation asserts what
it believed the current value to be, and is rejected if the fold disagrees. That
is what makes two concurrent revisions a detectable conflict instead of a
last-writer-wins race, and naming it prior invited
exactly the redundant back-pointer that produced the three homes.
Dropping predecessor? buys something beyond
tidiness, and it is a property this language should want:
…and provenance was still in the preimage
The paragraph above is true and the record it described was not. The field
list inherited from Edge ended in
provenance, and provenance
is how a value was produced — the same information
predecessor? carried, arriving through the door left
open by "unchanged" in the migration table. A capacity reduced from 3 and a
capacity raised to 1 would still have hashed differently, for a different reason,
one line further down the same record. Removing the back-pointer while leaving
provenance in the preimage bought exactly nothing.
What makes this unambiguous rather than a matter of taste is that the rule is
already written down, and already executable, elsewhere in this
toolchain. wrl.js says it of the rounding policy on a
named rotor:
/** The build-provenance policy governing a named rotor, or null when exact.
* The policy id is provenance only -- it never enters the artifact bytes. */
Two identical rotors seal identically whether one was written exactly and the other rounded under a named policy; the policy id is reported and not hashed. That is the shipped behaviour of the frozen spine, verified by the battery, and §D8's record contradicted it.
RelationRevision carries no
provenance. How a revision came to be proposed is
carried by the ledger event, which is where §10 already keeps the record
of what occurred. If a piece of so-called provenance genuinely changes future
behaviour — a trust class, an authority, a source constraint — then it is not
provenance, it is semantics wearing a provenance label, and it must be
promoted to a named field of the value (most often
policy) where the hash covers it.| Information | Home | Hashed into |
|---|---|---|
| what the relation currently means | RelationRevision | the revision id |
| how the revision came to be proposed | the ledger event | the event id |
| evidence the proposer supplied to justify it | the ledger event, as an operation argument | the event id |
| evidence the checker produced in deciding | the acceptance receipt (§8) | the receipt |
| how the relation evolved over time | the ledger sequence | — (an ordering, not a value) |
| what a replay actually observed | the film | — (evidence, not identity) |
The two evidence rows are one distinction, not a hedge, and getting them the wrong way round rebuilds the defect this section exists to remove. Evidence supplied with a proposal is an argument of the operation: it must be inside the event preimage, or the event id fails to cover something the proposer asserted. Evidence produced while deciding belongs to the decision, and §8 already has a home for it — the immutable acceptance receipt, kept deliberately separate from the monotone fact set. Putting checker evidence on the event would be a back-pointer from a proposal to a decision that had not been made when the proposal was hashed.
How far the name reaches
D8.1 says relation_id survives its value. It does
not say for how long, and the two allocation
preimages answer that question whether or not anyone asks it.
H("WRL_RELATION" ‖ world_id ‖ issuer ‖ nonce) — the
flat triple that §D8.1 replaced with a tagged
union — and both formulas sat on this page as though both were the rule. That is
the one-fact-two-homes defect committed against the one
value where disagreement is unrecoverable: two readers implement two different
hashes, mint two different names for one relation, and neither page looks wrong.
So this section no longer restates a preimage. It names where
world_id enters each of the two, and the suite now
requires that exactly one normative equation exists
per allocation variant, sitewide.| Variant | How world_id reaches the id |
|---|---|
NamedInitialAllocation | Directly. It is a field of the allocation and a term of the hash, visible in one line |
LegacyEdgeAllocation | Directly, the same way — which is the whole reason this variant is safe to derive from bytes the artifact already holds. The edge key alone would be world-portable, and a relation id that survives being moved into another world is a migration nobody wrote |
GrantedAllocation | Transitively, through grant_id — which is the content address of an AllocationGrant whose first field is world_id. The same grant issued in a second world is a different grant with a different id, so a granted relation id cannot leave its world either |
The two are equally scoped and not equally robust, which is
worth saying rather than smoothing over. The authored form is world-scoped by
inspection of a single hash. The granted form is world-scoped only for as long as
world_id remains inside
AllocationGrant; remove it there as an apparent
tidy-up and every granted relation silently becomes global, with no equation on
this page changing. That is a load-bearing field wearing the clothes of
bookkeeping, and §D8.4 is where it lives.
world_id is in the preimage on purpose — it is
what makes two independently authored worlds unable to mint the same relation id
by accident, and what lets a validator check an id against a sealed artifact
rather than against a global service. The consequence is that relation identity
is world-scoped, and the precise scope is worth stating because two
different things are both loosely called "changing the world":
| Change | Does world_id move? | Do relation ids survive? |
|---|---|---|
| Attach, revise or retire a relation at runtime — §D9 operations 3–5 | No. §D9's central move is that structure changes while identity does not | Yes. This is the whole point of D8.1 |
| Re-author the source and re-seal | Yes — a different artifact is a different sem- id, by construction | No, unless something carries them across |
So the scope is exactly one sealed world version, and the boundary that moves it is re-sealing, not running. A relation attached in period 4000 and revised in period 900 000 keeps its name the entire time; the same relation after a redeploy is, absent a migration, a different relation with the same meaning.
Before treating that as a defect, it is worth noticing that it is the existing property of every other name in the language, not a new one that relations introduce. Object ids are already world-scoped in the shipped implementation: the canonical bytes reference an object as a bare name —
"edges": [{"dst": "r0", "kind": "SignalWire", "src": "p0"}]
— and r0 means something only inside the artifact
that declares it. Two worlds can both contain an r0
and they are unrelated. Scoping relations the same way is consistency;
scoping them globally would make the relation the only entity in the ontology
whose identity outlives the world it lives in, and that is the choice that would
need an argument.
relation_id is meaningful relative to the
world_id in its allocation, and only relative to it.
It is stable across every runtime topology change and is not preserved by
re-sealing. Any claim that a relation "is the same relation" across two sealed
world versions is a migration claim, and must
be carried by an explicit old_relation_id →
new_relation_id map that the migration produces and a checker can verify.
Silence is not continuity.Stating it now is the point. This is exactly the kind of consequence that is
cheap to write down while it is a design and expensive to discover during the
first upgrade of a running system, when the discovery arrives as an external
ledger whose references have quietly stopped resolving. It is filed against the
migration capability at
ladder step 9, which is where it will be
answered.
The checker the rule asks for is no longer hypothetical, and the reason is that the rule tests itself. Re-sealing is what moves a relation id, and re-sealing needs no new artifact version — so a world re-authored today already produces the break, and the map that is owed can be computed and checked against two V1 worlds. §D8.8 gives the record it produces; three rows of the register run against it, including the one that matters most here, which is that a relation with no counterpart is reported unpaired rather than handed one. Silence is not continuity is a sentence until something fails when it is violated.
There is a second policy, and it is not obviously wrong — it is just a different trade, so it should be recorded as one rather than ruled out:
| World-local (D8.5, the default) | Domain-stable | |
|---|---|---|
| Allocation scope | the sealed world | a longer-lived namespace — an asset registry, a fleet, an organisation |
| Verifiable against | the artifact alone | the artifact and an external authority |
| Survives re-sealing | only through a migration map | yes, by construction |
| Failure mode | references break loudly at upgrade | two worlds can claim the same relation, and nothing in either artifact detects it |
The domain-stable policy is admissible, but only under a condition this section already has a precedent for. The open-world admission table below seals the namespace rules in the artifact while letting the allocated name appear in the ledger — that is precisely the shape needed here. A registry outside the world may supply relation identity if the artifact seals, by content address, the rules that govern it, so that a replaying host checks the registry's allocation rather than trusting it. A registry that is merely named and believed reintroduces the property D8.4 was written to remove: an id whose uniqueness depends on something no replay can verify.
What it costs the frozen artifact
Nothing today, and the reason is worth stating precisely rather than
assumed. The legacy projection above emits
{"kind": K, "src": S, "dst": D} and carries no
relation id. That remains correct for the frozen corpus, but only because of a
property the corpus happens to have:
edges encoding can omit
relation_id only where the triple
(kind, src,
dst) is unique within the artifact — because that
triple is then a key, and the id is recoverable from position. Every world
published on this site satisfies that. A world with two parallel relations of the
same kind between the same objects does not, and it is precisely the multigraph
case D8 exists to admit. Such a world therefore cannot be encoded in
the V1 artifact family at all; it needs explicit ids
under a newer ir_version. This is a real limit on V1, not a
defect in D8.1 — and it is better as a stated precondition that a validator can
check than as an assumption that holds until the first redundant link.The uniqueness precondition above is doing double duty, which is the reason to
notice it: it is also the period-0 nonce rule. A
world whose relations are distinguished by
(kind, src,
dst) can both be encoded in
the V1 artifact family and have its relation ids minted
without any new authored syntax, and a world that needs parallel relations fails
both at once. That is one condition to check and one condition to explain, rather
than two that happen to coincide until they do not.
Cardinality is derived, and therefore never stored
A stored cardinality field can disagree with the
endpoints it describes: a record may claim 1:n while
holding two sources. That is a second source of truth, and a validator that has
to reconcile two sources of truth for one fact will eventually pick the wrong
one. Cardinality is computed during validation and canonicalisation:
cardinality(R) = |{e ∈ R.endpoints : e.role = source}|
: |{e ∈ R.endpoints : e.role = target}| for directed
= n-ary otherwise
It is a derived view for diagnostics and profile constraints
("a mobility.occupies relation is 1:1"), never a field, and so never
serialised and never part of the hash. This is the same judgement that removed
payload: one fact, one home.
Today's route is a specialisation, exactly
The frozen directed route of §0 and §5 is recovered by fixing five of the eight fields:
| Field | Value for a frozen route |
|---|---|
domain | signal |
kind | SignalWire or SocketControl — the two frozen edge kinds are the two kinds of the signal domain |
endpoints | {(writer.sig_out, source), (reader.sig_in, target)} — size exactly 2, one of each role |
orientation | directed |
attributes | empty |
| derived cardinality | 1:1 — computed, not written |
--sig-->
route serialises, every published sem- id
moves, including
sem-8ae91fe9…fe4a and every id printed on this site.
So the broadening ships only with a canonicalisation rule that elides every
defaulted field: a relation in the specialisation above must serialise to
byte-identical output with today's edge encoding. Empty
attributes must be omitted, not emitted as
{} — the same discipline §11 already applies to empty
port groups. If that cannot be arranged, D8 is a 0.2 breaking change and not an
0.1.x extension.
Two of the corrections above make this easier rather than harder, which is worth saying plainly. Cardinality is derived, so there is no field to elide. Endpoints sort by role then terminal, and
source
precedes target in the enumeration — so a two-endpoint
directed relation orders its endpoints exactly the way today's
src, dst pair is already
ordered.Elision is necessary and not sufficient
An earlier version of the note above claimed that eliding defaulted fields was enough to preserve every published id. That is wrong, and the gap is not a detail. Here is what a route serialises to today, from the starter world's own canonical bytes:
"edges": [{"dst": "r0", "kind": "SignalWire", "src": "p0"}]
Now remove every defaulted field from the general record —
domain, orientation,
default texture, empty
attributes, policy, and
the derived cardinality that was never a field. Three differences survive elision, because none of them is a
field that could be elided:
| Frozen artifact | General record | Why elision cannot bridge it |
|---|---|---|
container "edges" | "relations" | a key is renamed, not omitted |
"src" / "dst" | "endpoints" | two scalar members become one array of records; that is a change of shape |
"p0" | { object_id: "p0", port: "sig_out" } | the frozen form names the object; an endpoint names a terminal, and carries the port in a field of its own. Nothing is defaulted here — the general form is strictly more specific, and one scalar becomes two |
So the compatibility mechanism is not elision but a legacy projection: a normative rule that a relation meeting the frozen profile's exact shape is encoded in the old form. Stated so it can be implemented and tested:
domain = signal;
kind ∈ {SignalWire, SocketControl};
orientation = directed;
texture = solid; exactly one
source endpoint and one
target endpoint; both terminals being the ports the
kind implies for their objects' roles; and no attributes, policy or
non-default relation metadata — then the canonical encoder MUST emit it
into the edges array as
{"kind": K, "src": S, "dst": D}, where
S and D are the
object ids of the source and target terminals, and MUST NOT emit a
relations key at all. Any relation failing any
clause needs either a relations encoding under a new
ir_version, or a fully specified extension field
that provably cannot appear in an artifact of the frozen shape.
The clause about implied ports is what makes the projection lossless in both directions: it is exactly the condition under which the
port field is recoverable from an object id alone,
because the kind and the role determine the terminal. Recovery runs at the
boundary, on the way in, and the recovered value is stored — the record a
consumer reads already holds both fields. Where that is not true, the old form genuinely cannot represent the
relation, and pretending otherwise would be the actual breaking change.
Note which way the
domain and
texture clauses cut. They are not filters that
happen to admit every V1 edge; they are the two places the projection says what
a V1 edge means. An electrical relation and an
async one each fail a clause and have no old form,
which is why the kernel's field partition
files both fields as V1-fixed rather than as free.This is a testable claim rather than a promise, and it should be tested before D8 is called structurally closed. The golden test is a round trip with byte equality at the end:
frozen Edge → canonical bytes ─┐
├─ MUST be byte-identical
equivalent Relation → projection ────┘
and every published sem- id, re-sealed, is unchanged
Until that test exists and passes over the full pinned corpus, the correct status for exact compatibility is argued, not demonstrated — which is what falsification condition 4 already says, and this section narrows it from "can defaults be elided" to the sharper question of whether the projection above is total over the frozen corpus.
Why this shape and not another
| Precedent | What it establishes |
|---|---|
| Bigraphs [B] | A bigraph is a place graph (nesting) and a link graph (connectivity) over the same node set — which is §D1's node-alignment, arrived at independently — and the link graph is a hypergraph: a link may join any number of ports. Multi-participant connection is the general case in the best-studied formalism for exactly this problem, not an exotic addition to it |
| GQL (ISO/IEC 39075:2024) [G] | The first new ISO database language since SQL standardises the property graph as a directed, labelled, attributed multigraph in which edges carry properties too. Attributes on the connection are settled practice with a standard behind them |
| Directed hypergraphs [H] | A hyperarc is an ordered pair of disjoint vertex sets, a tail and a head. That is the settled formulation of a many-to-many directed connection, and it is where source and target come from: they are tail membership and head membership, recorded per endpoint instead of by position |
| Modelica [M] | A connection of c1, c2, c3 generates c1.p = c2.p = c3.p and 0 = c1.f + c2.f + c3.f. Three participants, no direction, and no order — the generated equations are symmetric in the connectors, which is the direct precedent for endpoints being a set |
| VHDL [V] | A resolution function takes a one-dimensional unconstrained array of all driver values and returns one value, and it is declared on the resolved subtype of the signal — not on any driver and not on any connection. That is why resolution is a property of the terminal here, and neither a role nor an orientation |
What it would look like
Not valid WRL. The conformance suite asserts the toolchain does not accept it, and records which capabilities it waits on:
profile wrl.electrical.v1
; the RESOLUTION is declared on the terminal, not on the
; relations that reach it — one object owns the rule (VHDL)
[bus:b0](width=8){drive: resolved(std_logic)}
[driver:u1](enable=true){out}
[driver:u2](enable=false){out}
; two directed relations into ONE resolved terminal.
; domain.kind, not a bare label: `digital` says which profile
; validates it, `drive` says what it asserts.
; under §0 this is a multiple-writer error; under D8 it is
; legal precisely because b0.drive declares a resolution.
[u1] --digital.drive--> [b0]
[u2] --digital.drive--> [b0]
; an acausal relation: three endpoints, all role=terminal,
; no orientation and no texture. potentials equal, flows sum
; to zero (D2). the three may be written in any order —
; endpoints are a SET, so all six spellings seal the same id.
[node:n1]{pin}
[rail:vcc](volts=3.3){pin}
[load:r1](ohms=470){pin}
relation electrical.node acausal {n1.pin, vcc.pin, r1.pin}
The part of this that already runsdraft design · running implementation
Everything above is design. This subsection is not: it records what a working implementation established, and it is here because four rows of the register below stopped saying awaiting because of it. Twelve more rows arrived later, when D8.7 found that this same implementation had round-tripped every V1 edge while implementing almost none of the endpoint model above. Four more arrived after that, when the migration statement in §D8.8 turned out not to need the artifact version it had been filed behind.
The obstacle was that §D8.1 mints a relation's identity from an authored
name, and V1 has no way to write one. A frozen artifact's edge is
exactly {kind, src, dst}. So an implementation that
invented names would have two sources sealing to the same
sem- id while producing different
relation_ids — and a sealed world that does not
determine its own future semantics is the one thing sem-
exists to prevent. The resolution is not to wait for a surface. It is
LegacyEdgeAllocation: for a world sealed under
the V1 artifact family, a relation's name is
its frozen edge key, and nothing about that requires new syntax.
The implementation lives in relation-identity.js,
which imports wrl.js. The direction is the
guarantee, not a convention: the spine has no way to reach the derived view, so
no rel- or rev- value can
enter a sem- preimage by accident. The suite checks
that both pinned fixtures seal byte-identically through the kernel and
through sealWorld, because a marker reading
non-canonical is a claim and byte equality is a fact.
It also has a consumer. The playground seals with the frozen spine and then derives from the artifact that seal produced, printing each relation's ids and its endpoints by role. That is deliberate on both sides: a derivation nothing renders is one nobody finds out is wrong, and a page that adopted the derived verdict along with the view would have quietly changed V1's frozen error surface for every visitor. The suite requires the page to import the derivation and requires it not to import the verdict — §D8.8.
What the kernel refuses to take on trust
A world-scoped identity is only as scoped as the world id it is scoped
to, and the first version of this kernel accepted that id as an argument.
Callers passed the one sealWorld had just returned,
so it was always right, and it was checked by nothing: an artifact handed over
with sem-000…0 beside it minted a full set of
relation ids under that name. That is not a bug in a caller. A value whose
meaning depends on an unverified claim travelling next to it has no meaning, and
the failure mode is silent — every id well-formed, every round trip green, the
whole set scoped to a world that does not exist. The kernel now recomputes
the id from the artifact's own canonical bytes and treats any supplied
sem- as a cross-check to be refused on mismatch. The
recomputed path is the only path; there is no faster one to fall back to.
Two smaller refusals follow the same shape, and it is the shape rather than the three instances that matters — a rule the code states and the code can be talked out of.
- The artifact's version is checked before its edges are read. The
adapter's constants —
directed,solid,signal, the frozen port pairs — are V1's elisions. Applied to an artifact of some later version whose elisions differ, they would not fail; they would produce confident, wrong relations. So anir_versionoutside the declared set is refused rather than assumed compatible. - The declared vocabularies are deep-frozen. The role enumeration is not merely a list, it is the sort key of the canonical form, and the orientation-to-role table is the legality rule. Exported as ordinary arrays, an importer could reverse the first and widen the second, and canonicalisation would obligingly produce a different id for the same relation. A closed enumeration a consumer can reopen is an open enumeration with a comment attached.
Two of seven fields are the name
The interesting result was not planned. §D8.1 says a relation's identity survives its revision changing; tested by mutating one field and observing stability, that is nearly vacuous, since the id is minted from an allocation that need never have touched a revision at all. So the test routes the revision through the V1 projection — the only path by which revision content can reach a V1 identity — mutates every field in turn, and classifies what happened. The result has to be a total partition of the field set: a subset check is how an eighth field that quietly names a relation would get in.
| Fields | Effect on relation_id | What that means |
|---|---|---|
kind, endpoints | moves it | these two are the name in V1 |
policy | free | may change without touching identity — this is §D8.1's live content |
domain, orientation, texture, attributes | V1-fixed | V1 states exactly one value for each and cannot write another, so a relation carrying any other value cannot be projected to a V1 edge at all |
The third row is the one worth reading twice, and its name was chosen after a wrong one. It read no V1 form, which invites the reading that V1 is silent about those four fields. V1 is not silent about them. It says exactly one thing about each, and this contract restores all four on import:
| Field | What V1 fixes it to |
|---|---|
domain | signal |
orientation | directed |
texture | solid |
attributes | {} |
What V1 lacks is not a meaning for these fields but an independently writable representation for any alternative value — they are resolved constants, fixed by the encoding rather than elided from it. So the count still stands, and it is still the price of §D8 stated numerically: four of the seven fields a relation revision carries are pinned by the frozen artifact rather than authored in it, which is why §D8's richer relation is not today's edge plus some unwritten extras. It is a record most of whose interesting content is inexpressible until the artifact version moves. §D8's cost note argued that qualitatively. This is the count.
domain moved into that row, and how it moved is
worth recording, because it is the clearest case of why V1-fixed is a
different classification from free. It was filed free on the
reading that a V1 edge does not write a domain, so changing one could not
disturb the id — which is true, and irrelevant: projectability is a question
about representability, not only about identity. A V1 edge does not write a domain because it is entirely a
signal-domain encoding: its two kinds are the two
kinds of that one domain. An electrical relation is
therefore not a relation whose domain V1 forgets to mention; it is a relation V1
cannot represent, exactly like an acausal one. The
field was classified by what the encoding omits rather than by what the
encoding means, and those come apart precisely on the elided fields —
which is the same confusion, in the same table, that put
texture in the wrong row.
A matched pair falls out of the same seam, and neither half means much
alone. The same edge key in two different worlds yields two different
relation_ids (§D8.5 — two worlds' relations are not
the same relation), while the same relation structure in two different
worlds yields the same revision_id (§D8.3 —
which world described it is provenance, and provenance rides the event, not the
value). Checked separately, either would pass on a build that had accidentally
fed both hashes the same input, so they are asserted together on the same
edge.
A miscount where there should be a name clash
Writing one relation twice is refused today, and refused for the wrong reason. The block below is a complete world, and its annotation records what the published spine currently says about it:
profile forge.world.core.v1
[pulser:p0](every 2){sig_out}
[relay:r0]{sig_in, sig_out}
; the same relation, written twice
[p0] --sig--> [r0]
[p0] --sig--> [r0]
r0 has 2 signal-wire inputs (a node admits one)
is true of the lowered graph and useless to whoever wrote it. There is one
input, written twice, and the author is now looking for a second wire that does
not exist. Under §D8 the failure is sharper than a miscount: these are
two relations whose only available name is the same name, so no later
revision could say which one it meant. The kernel reports
WRL_DUPLICATE_RELATION_KEY, and the ordering is the
substance — the census runs before graphToIr, which
is where the controller count lives. The suite asserts both answers, so
if the spine ever adopts the better one this annotation goes red and this
passage gets corrected rather than quietly outliving its subject.
runtime layer, and so does every §D9 row. A rule is
settled when a check can falsify it; handing a pending row a check that
tests something adjacent and easier is how a register becomes decoration.What the V1 adapter is, permanentlydraft
The kernel above is finished, and it is not the destination. §D8's relation is a record with named instances, general arity, attributes, textures and three orientations, and none of that can live in a V1 artifact. Reaching it takes a new artifact version, and that is the next milestone rather than this one.
So what happens to the kernel? It becomes the import adapter — the
one supported path by which a world already sealed acquires relation identity.
That is a permanent job, not a scaffold, because worlds sealed under
the V1 artifact family keep their
sem- ids forever and must remain readable by
relation-aware tooling that arrives afterwards. Writing the contract down
now, before the richer artifact exists, is what stops the adapter from
drifting into a second general relation format: every convenience added to it
would be a feature V2 then has to keep bug-compatible.
ir_version ∈ {"1.0", "1.1"}: the two
versions that share the structural edges encoding
this contract reads. 1.1 adds the mailbox-grounded
policy family and changes nothing this adapter consults — same profile, same two
edge kinds, same implied terminal pairs, same directed/solid reading, same
edges container. An implementation importing a member
of that family:
- Admits by the whole source tuple.
(
ir_version,profile_id,rulepack_id) must be one the adapter declares. Gating the version alone lets an undeclared rulepack ride in under a frozen profile and be read as a recognised relation source for the sole reason that its id is a nonempty string — and since the rulepack is copied into every derivedpolicy, it is then sealed intorev-identity rather than merely ignored. This is a different question from the field partition's: policy is free says a relation may change its rulepack without movingrel-; it does not say any rulepack names a family this adapter can read. - Reads only the sealed artifact. The source text, its formatting, its sugar and its declaration order are not inputs. An adapter that reads the parse produces a relation view that two byte-identical worlds can disagree about.
- Invents nothing. Four of a revision's seven fields have no V1 source; each is supplied as a fixed constant that is the only thing V1 can mean, and the constants are enumerated below rather than chosen per call.
- Mints one allocation variant.
LegacyEdgeAllocationonly.NamedInitialAllocationand grants stay refused — they are Path B's, and an adapter that could mint them would be asserting a surface and a grant machinery that do not exist. - Decides nothing. V1's error surface is frozen. The adapter may describe a world that sealed; it may not be the thing that decides whether one seals. Where the derived layer has a better diagnostic than the spine — and it does — that is an argument for a spine version, not a licence to change V1's answers underneath existing programs.
- Moves no
sem-. Checked as byte equality againstsealWorld, for every fixture, before anything else in the suite runs. - Round-trips exactly. Every imported relation projects back to the byte-identical V1 edge, with its endpoints located by role (§D8.7).
Where each field comes from
| Revision field | V1 source | Under V2 |
|---|---|---|
domain | resolved — the namespace the profile declares: signal | inherited or authored |
kind | read — edge.kind | authored |
endpoints | read — edge.src and edge.dst, each qualified by the kind's frozen port pair | authored terminals, general arity |
policy | read — semantic_policies.rulepack_id | inherited |
orientation | supplied — always directed | authored, one of three |
texture | supplied — always solid, which V1 elides rather than lacks | authored; required for directed |
attributes | supplied — always empty | authored, typed and united (§D8 owes) |
The third column is the whole reason to write the second one down. Every supplied row is a place where V2's importer has to know that the value came from the adapter and not from an author. And two of these rows are here because the first version of this table got them wrong in opposite directions.
domain read artifact.profile_id,
which put forge.world.core.v1 in the slot
§D8's field table reserves for a profile-declared
namespace. A profile declares domains; it is not one. The adapter
was reading the nearest available string rather than resolving the value the
section describes, and because nothing in V1 round-trips a domain, no test
noticed. The correction is a declared map from profile to default domain —
which for the one frozen profile is signal, the same
value the recovery table below already stated, and
which the adapter had been contradicting in the same file.
texture was filed absent — no key at all —
with the argument that a missing texture and a
texture: null are different claims, and that no V1
world ever considered texture. That argument is sound and was applied to the
wrong question. It answers may the adapter invent a value?, when the
question is what does this encoding mean? — and a V1 structural edge
means -- solid, because
§5 gives V1 no other texture to write. Restoring
solid is therefore expansion of an elision, exactly
like restoring directed, which the adapter had been
doing all along without anyone calling it authorship. An encoding elides a value
when the value is implied; the two cases are told apart by whether the omitted
value is determined, not by whether the bytes contain it.
Filing it absent had a cost beyond the field itself, and this is the part worth carrying forward. It made texture required for directed — a rule §D8.7 states — unenforceable, since the kernel's own relations violated it. So the rule was deferred to V2, and §D8's texture row went into the implementation as one enforced half and one commented half. A table with a commented row is not a table. Both halves are now driven from a single declaration and both are checked, which is only possible because the adapter stopped treating a determined elision as an absence.
Migration is a correspondence, not preserved identity
V2 is an explicit semantic-ID version break: the artifact's canonical bytes
change, so every world's sem- moves. And
relation_id is minted from the world id
(§D8.5) — so every relation id moves with it.
There is a tempting fix, and it is wrong.
So a V1 relation and its V2 import are two relations, and saying so is correct rather than unfortunate: they belong to two worlds, and §D8.5 already ruled what that means. What migration owes is not sameness but a correspondence — and, by §D8.2, it has exactly one home. The pairing is a ledger fact, of the same shape as every other lifecycle event:
RelationImported {
from_world: SemanticArtifactID ; the v1 world
from_relation: RelationID
to_world: SemanticArtifactID ; its v2 import
to_relation: RelationID
}
Two properties make it a fact rather than a note. It is derivable:
given both sealed artifacts and the mapping above, the pairing is computed, not
authored, so it cannot be maintained wrongly by hand. And it is
outside the value: neither relation's id, and neither world's
sem-, changes because the import happened. A
consumer that wants continuity reads the ledger, which is where continuity has
lived since §D8.2.
sem- moves, so by §D8.5
every relation id moves with it — is not new in V2. Re-authoring any
world and re-sealing it does exactly the same thing at a smaller scale, inside
the frozen corpus. So the law is testable against two V1 worlds today, and V2
is one more instance of it rather than the first.
deriveLegacyEdgeCorrespondence(from, to) in
the kernel computes the pairing from two sealed worlds;
the four rows it turned executable are in the register below. The strong form
of outside the value is the one checked: not merely that the artifacts
are unmutated, but that the imported world derives the identical ids while
having never heard of the world it was imported from — which is precisely the
property the refused alternative above would break.imported, and each entry had the four fields of a
RelationImported. Nothing was wrong with the
arithmetic. What was wrong is that a derivation had produced a record whose
whole content is an operation was accepted — from two artifacts, which
know nothing about operations. Read strictly, it says two worlds are related by
a migration because they happen to contain matching edge keys, and two
unrelated worlds built from the same starter template would then be full of
imports nobody performed.So the two are separated, and the separation is what makes the fact checkable at all:
- The correspondence is structural and derived. It emits
pairs— candidates — alongsidedroppedandadded. It asserts nothing about intent, and it is total: a world with no relations mapped against itself reports identity-preserving with all three lists empty, which is the degenerate case a correspondence that fails by returning nothing would be indistinguishable from. - The
RelationImportedfact is authored, emitted by an accepted migration operation, and therefore — uniquely on this page — a thing that can be maintained wrongly. Which is exactly why it is the one thing joined against something derived: a fact naming a pair the two sealed artifacts do not produce is refused.
That asymmetry is the general rule underneath the specific fix. Derived values do not need checking against their own inputs; asserted ones do. Merging the two loses the only join that could have caught a wrong assertion, and it loses it by making the assertion unfalsifiable rather than by making it false.
V2, where the fixed fields become writabledraft · new in 0.1.3
Everything above describes a relation model that V1 can be read as an
instance of. The field partition ends by
naming four fields V1-fixed — domain,
orientation, texture,
attributes — and the point of that name is that V1
says exactly one thing about each and has no way to say another. The adapter is
not withholding an alternative; there is no alternative to write. Making one
writable is a new encoding, and this section is its schema.
The shape of the change is smaller than the argument around it. A V1 world
writes edges: [{ kind, src, dst }]. A V2 world writes
relations, and each entry is
{ identity_seed, revision } — the
canonical revision exactly as §D8.7 defines it, and
the preimage its name is minted from, minus one field.
Why the stored preimage is a seed
§D8.1's preimages are all world-scoped:
NamedInitialAllocation { world_id, relation_name }.
So a V2 relation cannot store its allocation, because
world_id is the hash of the bytes that relation is
written in — a stored copy would be a value stating what its own container
hashes to, before the container exists. Storing some other world's id is
worse, because it type-checks: the world's relations would then be minted under
a name a different world already owns.
What is stored is therefore the allocation minus its world coordinate, and derivation puts the coordinate back. That subtraction is the whole content of the word seed, and it is also the derivation order:
canonicalise -> world_id = hash(bytes) -> expand seed into an allocation
-> relation_id -> revision_id
Three ids are functions of these bytes and none of the three is in them. That is the same rule cardinality obeys, applied to identity rather than to structure, and it has the same justification: a stored copy of a derived value is a second source of truth for one hash, and the copy is the one a reader trusts.
ir_version "2.0" encodes topology as
relations, and:
relationsreplacesedges. They do not coexist.edgesin a V2 artifact isWRL_LEGACY_EDGES_IN_V2;relationsin a V1 artifact isWRL_RELATIONS_IN_V1. The rule is two-sided on purpose. If only one encoding refused the other's topology key, a world could carry both, and a V1 reader and a V2 reader would then disagree about its topology while agreeing about its bytes.- A relation is
{ identity_seed, revision }, exactly.world_id,relation_idandrevision_idare never stored. - A seed is its allocation without
world_id. Stated as an equation over the two field tables rather than as two lists, so a variant cannot gain a field in one and not the other. - Initial bytes admit two variants.
named-initial, the native one, andlegacy-edge, which is import-only: it records that a relation crossed the V1 migration without ever being named — itsrel-moved with the world id, as §D8.5 requires of every relation. To give such a relation a name afterwards is an explicit adoption, not a property of the seed.grantedis absent, and the absence is the statement — a grant is drawn at runtime (§D8.4) from an authority that does not exist at period 0. - A missing name is an error.
WRL_MISSING_RELATION_NAME, never a fall back to alegacy-edgeseed built from the endpoints. That fallback is the tempting repair and it is the forbidden one: it would make two independently authored relations over the same two ports the same relation, silently, on the author's behalf. relationssorts by canonical seed bytes. An array's order is in its bytes, so an unordered set either has its order decided here or has it decided by whoever typed the world.- A seed names at most one relation. Two relations under one seed expand
to one allocation and therefore one
relation_id, so the world cannot say which of the two a later revision revises — the V1 duplicate-key defect, arriving through a field an author now controls directly.
ir_version, profile_id,
rulepack_id) must be one the encoder declares, each
coordinate refusing with its own code.A version is not a profile
The ruling that fixes ir_version at
"2.0" says nothing about
profile_id, and the tempting move is to mint
forge.world.core.v2 alongside it. This encoding does
not, because the two coordinates answer different questions:
ir_version is how topology is written —
edges or relations — and
profile_id is what vocabulary it is written
in: which domains exist, which kinds, which ports. Bumping them together would
mean every new domain forces a new encoding version, and every encoding change
invalidates every profile. So V2 is a new encoding over the same
forge.world.core.v1 profile, and a wider profile —
one declaring electrical, or an acausal kind —
arrives later without touching the encoding.
The consequence worth stating is what V2 does not buy on its own.
Moving the encoding makes the four V1-fixed fields representable; it does
not make an electrical relation legal, because
legality is the profile's answer and the profile has not moved. Those are two
separate unlocks and this is the first of them.
That sentence was true and unenforced for one slice, which is the more
interesting half of the story. The V2 gate checked the version tuple and the
container shapes and then stopped, so domain: "banana",
kind "WarpTunnel", a terminal naming an object that
does not exist, a port called "made_up", two objects
sharing an id and role "Alien" all sealed and got
real sem- ids. A profile that is only said to
govern does not govern. So §D8.14 makes the gate
profile-aware, and it does so by deriving the V1 world the artifact describes
and handing it to the frozen validator, rather than by restating a registry
that could then drift from the one it copies.
What the V1 projection is, and is not, under V2
§D8.8's projection stays normative as the V1
compatibility adapter, and it has no authority over V2 canonical bytes: nothing
it returns is hashed, and no V2 field is derived from it. It is called
inside the V2 encoder, by §D8.14's world gate, and the
distinction between those two sentences is the whole of what that gate is
allowed to do — a validator may ask the V1 adapter whether a relation is one this
profile defines; a canonicaliser may not ask it what the bytes are. The
direction it does not run is the one
worth being explicit about: an explicit V2 → V1 downgrade produces a new V1
artifact, with its own sem- id, which is not and
never was the id of the V2 world it came from. Nothing here preserves a
sem- across an encoding change, and
§D8.5 already says why: a relation id is
world-scoped, and a claim that two sealed worlds hold the same relation is a
migration claim someone has to carry.
Deriving the three ids
The order above is not a style note, it is the dependency graph: each arrow
is a value that cannot be computed before the one to its left exists. Both
places this can go wrong are places where an implementation is tempted to skip
an arrow, and both skips produce ids that look right. Deriving from an
artifact that was never canonicalised gives two spellings of one world two sets
of relations. Accepting a world_id from the caller
instead of recomputing it gives a forged seal real-looking relations — V1
shipped exactly that bug, and worldIdOfArtifact was
the repair.
- Canonicalise first. Ids are a function of canonical bytes, so an id derived before canonicalisation is an id of how a world was typed rather than of the world.
- The seal is recomputed, never believed. A caller may state the world
id it expects and the statement is checked
(
WRL_SEMANTIC_ID_MISMATCH); it is never used as the allocation preimage. A function with two independent sources of truth about one hash has no source of truth. - A seed plus its world is an allocation
§D8.1 accepts. The reconstruction is handed to
the kernel's validator, not to a local one — if the two ever disagreed, the
disagreement would surface as a
rel-id that exists and is wrong, which no round trip can see. - The encoding is in the world id. A world id is
sem-in both families, because V1 and V2 are two encodings of a world rather than two kinds of thing — but the same topology written in the two encodings is two byte strings and therefore two worlds. The downgrade rule above is this rule, not a second one. - A
rel-is world-scoped and arev-is not. Editing one relation moves every relation'srel-, because the world id moved and the world id is in every allocation; only the edited relation'srev-moves, because a revision is standalone (§D8.3). That asymmetry is not an inconvenience to be engineered away. It is §D8.5 arriving in the native encoding, and the reason a migration has to be a claim someone carries. - Derivation is a pure read. Nothing it computes is written back into the bytes it read. A derivation that memoised an id into the artifact would move the artifact that produced it.
Carrying a V1 world across
A migrated relation does not keep its id, and no amount of care can
make it. The world id moved, the world id is in every allocation, so every
rel- moves with it. What survives is the
rev-, because a revision is standalone — so a
relation's structure crosses intact and its name does not.
That is the whole reason the import mints a
legacy-edge seed rather than a
named-initial one. The tempting alternative is to
name each migrated relation so the ids look stable; it does not make them
stable, and it costs two things. It makes an import look like authorship, and
it collides with the first author who later writes that name deliberately.
legacy-edge instead records the true fact — this
relation was never named — which keeps the two worlds indexable by the
same key and so keeps the correspondence derivable rather than
asserted.
edges array, because
canonicalizeGraph sorts. It was then weakened again
under a second, better explanation, which is true and still did not
support the conclusion.
The true part: both encodings canonicalise, and they disagree about the
key. V1 sorts edges by the tuple it stores
them as, (kind, src, dst). V2 sorts
relations by canonical
identity_seed bytes — key-sorted JSON — so a
legacy-edge seed compares on
dst before kind before
src. Two total orders over the same set, neither of
them authored, and a real disagreement.
The false part: that the disagreement must therefore be visible. An
order is canonical only within an encoding, so the encoding that decides
is the one being written — and a downgrade writes V1. The version that
permuted was reading its relations off in V2's order and emitting them
unchanged, which is not a translation but a V1 artifact in the wrong encoding's
order: valid against every field rule, correct as a set, and carrying a
sem- that no seal of that world could produce. One
line of missing normalisation, wearing an explanation that made it look
inevitable. V1 → V2 → V1 is byte-exact and preserves the
sem-; the V2 world still has an id of its own, which
is §7, for the ordinary reason that it is
different bytes.
The general form is worth more than the fix. A failing check is a claim about the world only after the code under it has been ruled out, and an explanation that makes a failure feel inevitable is the most expensive kind to accept, because it retires the question. Order is where this bites hardest: it is the one part of an encoding that no field rule checks, so a private copy of a sort key — or the absence of one — fails silently and passes review.
- An import mints only unnamed seeds.
legacy-edge, nevernamed-initial— a V1 artifact has no field a name could come from. The authority is the kernel'sIMPORTABLE_VARIANTS, reused rather than restated, because what can be minted from a V1 artifact has one answer regardless of what is being written into. The authorable list is not reused, because that one is a question about the target encoding's surface, where V1 and V2 differ. - Only the encoding moves. Every key the migration does not own passes
through byte-identically. A migration that also tidied
objectswould make the world differ from its source for reasons unrelated to the change, and no test could then attribute a moved id to the migration rather than to the tidying. - Structure survives; identity does not. Every
rev-recurs and norel-does. A migration reporting a preservedrel-would be claiming the thing §D8.5 exists to deny. - The claim is checked by the checker that already exists. The
cross-family correspondence has the same shape as the V1 one, so
candidateImportedFactsandcheckRelationImportedverify it unchanged. §D8.5 asks for one checkable claim, not one per encoding pair — a rule needing its own verifier at the boundary where migrations actually happen has an exception exactly where it is load-bearing. - Both seals are recomputed. A migration is precisely where a caller holds two worlds and is best placed to be wrong about one of them.
- A named relation has no legacy counterpart. It is
added, never paired — a relation with no V1 preimage is not a relation the V1 world quietly also had. - The target version of a downgrade is chosen, not remembered. V2 records no provenance, because a value that recorded its own history would be the second home §D8.3 refuses.
- A downgrade writes the order of the encoding it is writing. Both
encodings sort and they sort by different keys, so the one that decides on the
way out is the target — asked of the frozen
canonicalizeGraphrather than restated, because an order is the one part of an encoding no field rule checks, and a private copy of a sort key fails silently. V1 → V2 → V1 is therefore byte-exact and preserves thesem-.
Naming a relation
One spelling, and it is a per-route one:
[clock_feed]: [pulser:p0] --sig--> [relay:r0]
A V2 source is a V1 source whose route lines carry a name. That is the
whole language change, and it is deliberately the whole language change:
every other line — the profile header, declarations, comments, sugar — is
handed to the frozen spine untouched, so the surface cannot quietly become a
second parser that agrees with wrl.js today and
drifts from it later. Names come off the front of their lines without changing
the line count, the spine does all the parsing and all the validation, and the
names go back on afterwards.
canonicalizeGraph sorts the edges, so the
parsed order is a canonical order and not the order anybody typed — the same
fact the round trip reports. An index-based zip
attaches the wrong name to the wrong relation, and the failure is
silent: the world seals, every id is well-formed, and every one of them
is wrong.
So the pairing is derived from provenance the spine already keeps.
desugarCoreMapped records a source line for every
emitted line and the parser stamps its emitted line onto each edge; following
that back gives, for each authored line, exactly the relations it produced —
through any amount of sugar, and whatever order the parser hands them back in.
That same map is how the surface knows which lines are routes without
restating the frozen edge grammar: a route line is a line that produced an
edge.
ir_version 2.0
header. Autonomous decision, flagged for review. The ruling fixes
exactly one piece of V2 syntax and it is per-route, so a document-level header
would be a second, unruled one. Instead the encoding is the caller's
choice: parseNamedWorld is the V2 parser,
sealWorld is the V1 one, and a source is native V2
because it was handed to the V2 parser. This is also what keeps
WRL_MISSING_RELATION_NAME meaningful — “an unnamed
route under native V2” needs “native V2” to be a fact about the request, since
an unnamed route under V1 is simply a route.- A name is the only thing V2 adds. Stripping the names returns the V1 source byte for byte, and the artifact the spine validated is the artifact the V1 parser produces from it. V2 adds names to relations; it does not re-litigate what a legal world is.
- Every route must be named. An unnamed route under native V2 is
WRL_MISSING_RELATION_NAME, reported at the authored line. A name is never derived from the endpoints — a relation named after its terminals would be re-minted the moment an object is renamed, so its identity would track the spelling of something else. - A name is an identifier. The prefix is detected loosely and judged
afterwards, so
[clock-feed]:is diagnosed as a bad name rather than falling through to the core parser as a malformed route. - A name denotes exactly one relation. Zero and many are the same
fault — the name fails to denote — and share
WRL_AMBIGUOUS_RELATION_NAME. The many case is reachable by accident, because sugar expands one line into several routes, and one name over all of them would make one id out of several. - The pairing follows provenance, never order. The note above.
- A repeated name is a repeated seed, and the encoder says so. The surface carries no duplicate-name rule of its own — a second rule about the same fact can disagree with the first, and the encoder's is the one that decides bytes. A surface that de-duplicated its input could not hand the encoder the collision the encoder exists to refuse.
- Renaming moves identity and nothing else. Every
rev-recurs, because §D8.3 makes a revision standalone and a name is not in it; everyrel-moves, and so does the world. A rename that moved arev-would mean the name had leaked into the revision; one that moved norel-would mean the name is not the preimage. - An author cannot write a legacy seed. The surface mints
named-initialonly.legacy-edgerecords that a relation was never named, which is a fact about an import and not something an author can assert about a relation they are writing.
Writing a V2 world back out
A formatter's characteristic failure is not refusing — it is emitting text
that its own parser reads back as something else.
formatCore has that scar already: it once wrote a
[mailbox:m] line that
parseWrlCore then refused. The V2 formatter
inherits both the lesson and the mechanism. It does not know the route
grammar; it asks the frozen formatter for the V1 source, then finds the route
lines the way the parser does, by following provenance back to the line each
edge came from. Neither direction of the surface contains a copy of the
arrow syntax.
A migrated world. Its relations carry
legacy-edge seeds and therefore have no names, and
§9 gives the surface no way to write a route
without one. So a migration produces a world that runs, seals and compares,
but is not authorable text until someone names its relations. That limit
is real and it is now exitable: §D8.16's
adoption is the operation that supplies the names, as an explicit act with
names the caller carries. What the formatter still may not do is mint them,
which is the one thing §D8.1 forbids.
A world with two relations over the same terminals was listed here as
the second such world, on the grounds that it is "well-formed V2" and only the
text is missing. Both halves were wrong, and the correction is more useful than
the claim was. The text is not missing: [a]: and
[b]: on two otherwise identical route lines are two
distinct, unambiguous lines, and the name stripper reads them back as two names
on two lines. And the world is not well-formed:
forge.world.core.v1 admits one signal-wire input per
object, so the second relation is a
WRL_CONTROLLER_CONFLICT — in either encoding, from
the same law. It only ever looked well-formed because the V2 gate did not
look at the world. The multigraph is therefore not a
surface debt but a profile one, and it clears when a profile ships a
controller law that admits more than one.
- The round trip is a fixed point. Format → parse → format returns the same canonical bytes, the same world id and the same text. The surface is a surface, not a lossy view.
- The formatter does not know the arrow. Strip the names off the V2 output and what remains is byte-for-byte what the frozen formatter emits. The V2 side contributes name prefixes and nothing else, so there is no second copy of the route syntax to drift out of step.
- An unadopted migrated world has no source form.
WRL_UNWRITABLE_SEED— and the limit is in the surface, not in the world, which still derives its ids like any other. §D8.16 is the way out of it. - Formatting is normalisation, never a gate. An unformatted world seals to the same id as a formatted one, in both encodings.
- Routes come out in the encoding's own order. Sorted by seed bytes,
not by V1's
(kind, src, dst). Source route order decides nothing — the spine sorts it — so emitting the order this encoding's bytes are already in is the choice that restates no sort key. - A V2 world runs as the V1 world the spine validated. V2 changes how topology is written, not what a world is, so nothing downstream of the seal learns a second encoding.
- Naming a world does not change the world. The V1 artifact underneath
a named source seals to the pinned demo
sem-. Naming relations adds identity to relations; it changes nothing about the world they are in.
The world gate: an invalid world mints no id
Since WRL-P0 the gate reads a profile row — the lowered row delegates to the frozen spine exactly as described below; a static row is checked against its own declaration. §D8.20 is the rule; this section is the lowered half of it.
For one slice the V2 encoder validated the encoding and never the
world. It checked the version tuple, the container shapes, the relation
record fields and the generic revision vocabulary — all of them questions about
how bytes are arranged — and then sealed whatever came out. So every one of
these got a well-formed sem-: an object with role
"Alien", two objects sharing an id, a terminal
naming an object called "ghost", a relation with
domain: "banana", one with kind
"WarpTunnel", a terminal on a port called
"made_up", and two relations onto a controller input
that admits one.
An id for an impossible world is worse than a refusal, because a refusal stops. An id exists: it is stable, it compares equal to itself, it can be cited, stored, and paired across a migration, and every consumer downstream treats it as evidence that something legal was sealed. The gate's absence was also invisible from the outside — the encoder did everything it said it did.
What the gate must not be is a second registry. A V2 validator with
its own list of roles, ports, domains and kinds is a second opinion about what
forge.world.core.v1 permits, free to drift from the
profile it claims to enforce, and drifting silently, because nothing ever
compares two registries that never meet. So the gate derives: it builds
the V1 world the V2 artifact describes — objects as declarations, relations
through §D8.8's projection — and hands it to the
frozen validator. Every refusal therefore carries the spine's own code, and a
profile that widens widens both encodings in the same commit.
Deriving the world also closed a bug of exactly the kind
§D8.13 warns about, found only because the derivation
made the two answers comparable: the V2 canonicaliser sorted
objects by (role, object_id)
where V1 sorts them identity-first by (object_id, role).
Two total orders over the same set, no field rule able to see the difference, and
a private copy of a sort key is exactly what
the edge-order bug one slice earlier was. The
canonical objects are now taken from the derivation rather than sorted
again beside it.
- Validate the world before canonicalising or hashing it. A
sem-is minted only for an artifact that is a legal world under the profile it names. Shape-checking the encoding is not world-checking. - The gate derives; it does not restate. The V1 world the artifact describes is built and handed to the frozen validator, so objects, roles, configs, terminal existence, domain, kind, ports, orientation, arity, attributes and controller constraints are all checked by the profile's own implementation. A refusal carries the spine's code.
- Derived fields are checked, not accepted. Every key the world
derives —
semantic_policies,schemas, and each object'sportsandstate_schema_ref— must equal what the derivation produces; a disagreement isWRL_V2_WORLD_MISMATCH. An artifact may not state a fact its own content contradicts.ir_versionis the exception, and the reason is that it is the coordinate that moved. - Canonical order comes from the derivation. Not from a second sort beside it. An order is the one part of an encoding no field rule checks, so a private duplicate of a sort key fails silently and passes review.
Which encoding is this world written in?
A V2 source was, for one slice, a V1 source whose route lines carry names —
and nothing in the text said so. That reads like an economy until you write a
world with no routes. Take a profile line and nothing else, hand it to both
parsers, and both accept: two different valid artifacts, two different
sem- ids, one byte string, and nothing in it that
says which one those bytes mean. The id of that world would depend on which
function a caller happened to reach for. An identity spine cannot permit that;
it is the same defect as a private sort key, arriving through the front door.
So a V2 world declares its encoding, as the second non-comment declaration,
immediately after profile. The stripper that removes
it is line-preserving — it blanks the line rather than deleting it — for
the reason the name stripper is: every line number a diagnostic reports must be
a line number in the text the author wrote. And it asks
validateProfileHeader where the header block starts
rather than counting lines itself, so "the profile comes first" stays one rule
with one implementation.
- A V2 source declares its encoding.
ir 2.0, as the second non-comment declaration, on the line afterprofile. A source that does not is not a V2 source:WRL_MISSING_IR_HEADER. - One declaration, in one place, in one spelling. Declared twice is
WRL_DUPLICATE_IR_HEADER; declared after anything but the profile isWRL_MISSING_IR_HEADER; anything other than exactly one version token isWRL_MALFORMED_IR_HEADER; a version outside the V2 family isWRL_UNSUPPORTED_IR_VERSION. - The surface emits what it reads.
formatNamedWorldwrites the declaration andparseNamedWorldreads it back, so the round trip is a fixed point over the header too, and the artifact'sir_versionis the one the source declared rather than the one the encoder assumed. - Stripping is line-preserving. The declaration's line is blanked, not removed. A stripper that deleted its line would shift every diagnostic the frozen spine reports afterwards.
Adoption: naming a migrated world
A migrated relation was never named — that is a fact about its history, and
legacy-edge is the record of it. The surface cannot
write one (§D8.13) and the authoring boundary refuses
to mint one. Left there, the limit is permanent, and the migration is a one-way
door out of the language: a world that crossed it runs, seals and
compares forever, and can never again be handed to an author as text.
Adoption is the door back. It is an act, not a repair: someone
supplies the names. That is the entire design, and each half of it is a
consequence already stated elsewhere. The names are the caller's, because a
generated name would make §D8.1's forbidden move
legal by relocating it one function to the left — and the generated name would
then be indistinguishable in the bytes from one an author chose, which is the
exact confusion legacy-edge exists to prevent. The
structure does not move, because a revision is standalone
(§D8.3) and a name is not in it. The identity moves
entirely, including for the relations nobody adopted, because the seeds
changed, so the bytes changed, so the world id moved, and the world id is in
every allocation (§D8.5).
- Names are supplied, never generated. An adoption carries a name per relation it adopts. Absent, empty or non-identifier names are refused; none of the refusals falls back to deriving one.
- A relation is selected by its own preimage.
(
kind,src,dst) — the only name an unnamed relation has. Selecting one this world does not have unnamed isWRL_UNKNOWN_RELATION; selecting one twice isWRL_DUPLICATE_ADOPTION. A relation that already has a name is not adoptable — replacing it would be a rename. - Every adopted seed becomes
named-initial, and everyrev-is unchanged. Adoption changes what a relation is called, not what it connects. - The world
sem-moves, and everyrel-with it — including the relations that were not adopted. A correspondence pairing the two worlds onrev-is returned, and it reportsidentityPreserved: falserather than letting a caller assume otherwise. - Adoption is exhaustive, or it is refused. The assignment must carry
exactly one name for every
legacy-edgerelation in the world. A leftover isWRL_INCOMPLETE_ADOPTION; a world with nothing unnamed in it is not adoptable at all (WRL_UNKNOWN_RELATION). A refused adoption leaves its argument byte-identical. - Migration and adoption are operations, never consequences. A
surface exposes them as two acts a person performs — import this V1 world as
V2, then adopt these names — and neither may run because the editor
changed. Both of them seal: an import mints a world
sem-and with it everyrel-under that world, an adoption mints another set, and a page that ran either on keystroke would mint a dozen real sealed worlds during a two-second edit. An id cannot be un-minted by deciding it was not meant. A surface also holds no copy of clause 5: the completeness gate is the library's, so an incomplete form is submitted and refused, rather than made unsubmittable by an interface state nothing can test.
What a partial adoption does is seal. The half-named world is a real
world with a real sem-; by clause 4 every
rel- in it is re-minted, including for the relations
nobody touched; and no surface can write it. Naming the rest moves every id a
second time. So naming four relations two at a time mints two throwaway worlds
whose ids are in no way distinguishable from wanted ones, and a caller who
stops halfway is left holding an artifact that runs, seals, corresponds — and
cannot be edited.
The distinction that dissolves it: collecting names is editor work, and editor state is not sealed. A tool may gather names one at a time, from a form, over a week; none of that is a world until it is handed over. So the refusal is not a restriction on what a caller may want. It is a statement about where the seal goes, and it makes the law shorter: a migrated world takes exactly one adoption to become writable.
Admission: which parser reads this text
Every rule above is written for a caller who knows what they are holding. A tool does not. It holds a textarea, and the person typing into it may be writing either encoding, may paste one over the other, and is not going to announce which. That is a real question, and §D8.15 already contains its answer: the reason a V2 source declares its encoding is so that it can be asked rather than guessed at.
An admission is that argument used. One entry point, two parsers, and the discriminator comes out of the text — not from a dropdown, a file extension, a last-button-pressed, or a preference. An interface may put starter text into an editor, because text is the input; an interface that decides how existing text is read has made the identity of a world a function of the interface, which is the same defect §D8.15 was written against with a different guess in the blank.
The half that is easy to get wrong is the failure path. A source that
declares its encoding badly — ir alone,
ir 3.0, the declaration twice — is a source someone
meant as V2 and mistyped, and it is usually a perfectly good V1 world apart from
that one line. So a fallback would have plenty to seal, and what it would print
is a real sem- for a world in an encoding its author
did not write. The frozen parser has no ir rule at
all: asked, it answers WRL_UNSUPPORTED_FEATURE
pointed straight at the declaration, which tells an author to delete the one
line in the file that was doing its job. Both outcomes are worse than a refusal,
so declared badly and not declared have to be different facts, and
only the second one routes.
- The source decides. A text that declares an encoding is read by that encoding's parser; a text that declares none is read by the frozen V1 parser. Nothing outside the text participates in the choice.
- Declared badly is not undeclared. The routing question is whether an encoding is declared, and it is answered before anything is parsed. A declaration that is malformed, doubled, misplaced or of an unsupported version is refused by the parser it selected, and is never retried under the other one.
- The undeclared arm is the frozen verdict. Admission adds the family and the declaration flag; it renames, drops, nests and re-codes nothing. A world in which nothing changed may not cost its readers a migration.
- One admission seals once. A V2 admission runs the frozen spine exactly once, over the de-named text, and derives everything else from the artifact. A tool that sealed two texts would be showing a reader an id that nothing in the editor produces.
- The world id and the execution view id are different claims. A V2
world's
sem-is the identity scope of its relations (§D8.5). Thesem-of the V1 artifact it projects to is proof of an execution view and nothing else. Displaying either under the other's label is a false statement about scope, so the two are shown apart and named apart. - A surface calls this rule; it does not restate it. An editor, a playground, a CLI or an importer routes by calling the admission, and holds no second copy of "what a declaration looks like". A surface with its own dispatch can answer the question differently from the rule that defines it, and the first sign of that is a world sealing under an id nobody can reproduce.
That last clause is the reason the published playground has no V1/V2 control
anywhere on it, and the absence is checked rather than intended. The page reads
the encoding off the source on every keystroke, shows the two ids in two
labelled rows, and prints the projection beside them; the one-click example that
matters most is a well-formed V1 world with a broken declaration, because
that is the source a fallback would have handled most convincingly — it would
have sealed, printed a real sem-, and given the
reader no sign that the encoding they wrote had been overruled.
The runtime projection: what a runtime is handed
A runtime does not read source and does not need to. It needs an artifact it
can execute, and §D8.13's downgrade produces one. The
temptation is to stop there — hand the runtime the V1 artifact and let it get on
with it — and the reason to refuse is that a runtime handed a bare V1 artifact
will seal it, get a sem-, and use that
sem- as the world. It will be right to. That is
exactly what a V1 sem- is for, and nothing in what it
was handed says this one is different.
It is different. It is the id of a projection: the bytes this world runs as, not the bytes this world is. Scope anything durable to it — a grant, a birth key, a revision, a ledger event — and the scope moves the moment the encoding does, while the world has not moved at all. The failure is silent in the worst way: every id involved is real, every hash is correct, and the world's own relations end up allocated in a scope that is not the world.
So the handoff is an envelope rather than a return value. It carries both ids, says which is which, and carries the bindings that let a consumer check the answer instead of believing it. A raw downgrade cannot say any of that — not because it is wrong, but because it has nowhere to say it.
{ semantic_world_id, semantic_artifact, execution_view_id,
execution_artifact, relation_bindings }, where a binding is exactly
{ relation_id, revision_id, legacy_edge }. It is
derived, not canonical, and not in the world bytes.
semantic_world_idis the world. It is authoritative wherever a world is meant: identity, scope, provenance, comparison. No consumer of a projection needs to know which encoding it came from before knowing which id is the world's.execution_view_ididentifies projected bytes and nothing else. It is the id ofexecution_artifactunder the frozen spine — the value a runtime would compute for itself — and its only correct use is to check that what it holds is what it was given.- A runtime executes
execution_artifact. The semantic artifact is not an execution input; the encoding stops at this boundary, and nothing downstream of it learns a second one. - Observations name relations by
relation_id. A runtime reports against the V1 edge it ran, so the binding carries that edge: an observation keyed by an edge lifts back to the relation it belongs to without the runtime being asked to remember a map it was never given. execution_view_idis not a world scope. No grant, birth key, relation allocation, revision or ledger event may be scoped to it — ever, and not "unless the two coincide". Everyrelation_idin a projection is minted undersemantic_world_id, which is the same rule §D8.5 already states, arriving at the one boundary where a second plausible id is in the room.- Every binding is independently recomputable. A consumer recomputes
binding i from
semantic_artifactalone — positionally against that artifact's own topology list — and needs nothing from the envelope to do it. A projection carrying its own preimages would be offering a check against itself. - A V1-native world is the degenerate coincident case. It runs as itself, so the two ids are equal and the projection says so. The envelope is total over both encodings for this reason: an envelope that existed only for V2 would leave every V1 caller passing bare artifacts around, which is the habit clause 1 exists to break.
One consequence is sharp enough to be worth stating separately, because it is
the mistake a careful consumer makes. The bindings are not positional against
execution_artifact.edges. The two encodings sort
their topology by different keys — V2 by seed bytes, V1 by edge — so binding
i and edge i are routinely different relations in the same world. A
consumer joins the two lists on legacy_edge, which is
why the binding carries it; joining on the index mis-attributes every observation
in any world whose two orders disagree, which is most of them.
The projection on the wire: what survives a boundary
Everything above holds inside one process, where an envelope is an object a caller got back from a function it called, and the words derived and not canonical are guaranteed by the fact that the caller watched them be computed. A runtime is on the far side of something — a socket, a file, a language, a repository. Over there those two words are two more bytes in a message, set by whoever wrote the message, and a receiver asked to trust them is in the position §D8.18 was written to get out of.
So a transmitted projection carries no flags and no prose. It carries the semantic artifact — the only value on it that is not derivable from another value on it — and the sender's claims about what that artifact derives to. The receiver recomputes every claim and refuses on any disagreement. Derived stops being an assertion the receiver is asked to believe and becomes a fact it establishes.
Two things are deliberately absent from the record, and the absences do more work than the fields. The execution artifact is not transmitted: it is a function of the semantic artifact, so carrying it would let one message pair one world's semantics with another world's bytes to run, and leave the receiver holding two artifacts and a silent choice about which is real. Omitting it makes that message unrepresentable rather than merely detectable — and the view id, which is transmitted, is the checkable proxy for those bytes. Coincidence is not transmitted either, for the same reason in miniature: it is an equality between two fields that are both present.
It is fair to ask why the claims are shipped at all, given that every one of them recomputes. A record carrying only the artifact would be unfalsifiable: the receiver would derive everything, find no disagreement possible, and learn nothing about whether the sender believed the same things. Shipping a claim you can check is what turns a divergence between two implementations of this rule — which is exactly what a second implementation in a second language will eventually produce — from silence into a refusal.
- A transmitted projection is exactly
projection_version, semantic_world_id, semantic_artifact, execution_view_id, relation_bindings. An unrecognised key is refused, not ignored: the whole record is claims, so a field the receiver cannot check is a field the sender may be relying on. - The record carries no execution artifact, no coincidence flag, and none of the derived / not-canonical / not-in-the-bytes flags. What cannot be checked is not sent.
- The bytes are canonical, under the same serializer the spine uses on worlds. Two senders holding one world transmit one byte string, so a transmitted projection can be compared, hashed and diffed by a party that cannot parse WRL at all.
- A receiver recomputes every claim from the semantic artifact and returns an envelope rebuilt on its own side. What a receiver ends up holding is what it derived, never what it was told.
- A claim that does not recompute is refused and names the field that disagreed. It is not repaired and not preferred against: a projection whose claims do not recompute is evidence that two ends disagree about what a world derives, and continuing past that means picking a winner silently.
- There is one route to the wire, and it takes an envelope rather than an artifact. A serializer that derived the projection itself would be a second route, and two routes can disagree about a world while both look right.
- Incoming bytes are read exactly, by a reader written for this wire rather than a general one. Integer tokens keep their value at every magnitude the artifact domain admits; a non-integral number, a duplicate key, insignificant whitespace, or any other lexical variant of a valid record is refused. A protocol that calls its bytes canonical and then accepts variants of them has declined to check the thing it named.
Clause 7 was added after the rule shipped without it, and the way it was missed is the argument for it. A rotor lane is the one artifact scalar the spine leaves unbounded — every other is capped at the safe-integer limit, and lanes are held as arbitrary-precision integers so that a 64-bit lane is exact. A world carrying such a lane seals correctly, derives correctly, and serialises to bytes holding the integer exactly. Read back with an ordinary JSON parser, that integer becomes a nearby one, and the receiver refuses its own honest bytes. Every step was individually right; the loss happened in the single step that was borrowed instead of written. A protocol whose identity is its bytes cannot be read by something that does not preserve them.
Worth noticing where that surfaced, because it is the good case rather than the bad one. The rounded value did not pass — it tripped the range check on re-serialisation, so the failure was loud. But it was loud in the wrong vocabulary: the receiver reported that a number was out of range, when what had happened was that its reader could not hold a number that was perfectly in range. Refusing a valid world is a much smaller fault than admitting an invalid one, and it is still a refusal of something that should have passed.
Clause 5's sharpest case is the one that changes no identifier anywhere: reorder the bindings. Every id in the message stays real and stays correct, and the message is still refused — because the order of the bindings is itself derived, and a list in the wrong order is a different claim about which relation runs as which edge.
Clause 3 is what makes the rule testable across a boundary rather than only
within one, and test/projection-vectors.json is where
that lands: a committed set of sources paired with the canonical bytes their
projections serialise to. It is the only artifact in this repository that an
implementation which cannot run JavaScript can be held to. Notably, no vector
states its encoding — §D8.17 already says the
source decides, so a reader that needed to be told the family here would be
failing that rule before it got to this one.
The profile table: a wider vocabulary without a wider encoding
The header of relation-v2.js argues at length
that ir_version and profile_id
answer different questions — how is topology written, and in what
vocabulary — and that bumping both together would make every new domain a
new encoding. Through C.5 that argument had no consequence, because the encoder
admitted one profile per encoding and §D8.14 lowered
every relation through the frozen V1 projection. So a world that no V1 world
corresponds to could not be sealed at all. The first such world arrived from an
application: Graphonomous holds a deterministic V2 world with twenty-one roles,
one port, a semantic domain and thirty-one directed
solid kinds, and nothing in it is a Pulser. It was minting its own world id
under its own prefix, and the kernel's validateAllocation
was refusing — correctly — to scope a rel- to a
world nobody had sealed.
The repair (WRL-P0, 2026-09-03) is a table, not a branch:
V2_PROFILES, frozen data keyed by
profile_id. What was considered and not done says
what the table is. Keying it by ir_version would
force every new profile to be a new encoding. Giving each row a
deriveWorld function would be code per profile — a
branch in a costume, and not freezable or inspectable as data. Deriving
schemas and state_schema_ref
for a static row from some default would seal a runtime claim into identity
for a runtime that does not exist. And widening
validateAllocation to a second world-id prefix
would let a world nobody sealed scope a rel-; the
fix is to seal the world.
- A world's
profile_idselects a row of a frozen table keyed by profile id. An id with no row is refused asWRL_UNSUPPORTED_PROFILEbefore any byte is hashed; the refusal names the rows that exist and is the only place the list of them is read. - A row's derivation is one of two, told apart by one data tag.
lowered: the profile's worlds are V1 worlds spelled in V2; the row declares nothing of its own, and the frozen spine's registries are its declaration, reached by deriving the V1 artifact and handing it tographToIr.static: the declaration is data in the row — roles and the ports each has, the relation domain, the kinds, one orientation/texture/arity signature, and the (source role, target role) pairs each kind admits — and the derivation reads the row without asking which row it is reading. - A static profile implies no runtime. It derives
semantic_policies = { rulepack_id }and nothing else — no admit policy, no film schema, no numeric policy list, noschemas, nostate_schema_ref. Those fields are claims about how a world runs, and a profile that declares roles and kinds has said nothing about that; a static world that states one is refused asWRL_V2_WORLD_MISMATCH. The downgrade (§D8.13), the runtime projection (§D8.18) and the text surface (§D8.12) refuse a static world. A seal is not a run. - The world gate (§D8.14) holds every relation to
its profile. Under any row,
revision.policymust be in the row's declared vocabulary (WRL_UNDECLARED_POLICY). Under a static row, an object's role, a terminal's port, a relation's kind, its (source role, target role) pair and its domain/orientation/texture/arity signature are each refused by name —WRL_UNDECLARED_ROLE,WRL_UNDECLARED_PORT,WRL_UNDECLARED_KIND,WRL_UNDECLARED_ENDPOINT_PAIR,WRL_PROFILE_SIGNATURE_MISMATCH. Validation runs before canonicalisation, so an invalid world mints no id. - A row is frozen data and is never edited. A successor profile is a new id whose row spreads its predecessor and widens by declaration — never by becoming permissive. Nothing iterates the table to decide bytes, so adding a row moves no existing identity: every world sealed under an earlier row seals to the same id after the successor exists.
- The kernel mints. A static world's
sem-is the hash of its canonical bytes under §D8.10, and everyrel-andrev-is the kernel's allocation and revision id over thatsem-. An application submits an artifact; it does not mint, and it does not sort or de-duplicate on the way in — object order and seed order are decided by the encoder and are not in the bytes. forge.world.core.v1remains the encoding's default profile: the one the text surface writes, since that surface is the V1 spine with names, and the one whose rulepack the lowered row reads so that the two tables cannot disagree. The text surface writes no other row.
The consequence worth stating plainly: a profile now lands as data, and a
new domain is a new row. The table has four rows at the time of writing —
the lowered forge.world.core.v1 and three static
Graphonomous rows, graphonomous.semantic.v0,
v1 and v2, one
lineage where each successor is its predecessor spread plus a measured delta
(v2 is v1 plus exactly
one endpoint pair, SUPERSEDES [LAW, LAW]). The
reference prints the rows and their
counts, read out of the module by the suite. None of this is
ProfileSchemaV1 (§D6.1):
a static row has no attribute types, no units, no bounded resources and no
resolution tables. It is the smallest thing that lets an application's world
be sealed by the encoder rather than by the application, which is the property
§D8.5 needs and the one the spike was
violating.
What D8 owes before it can ship
- A canonical form that leaves every existing
sem-id where it is (the note above). An allocation surface.Shipped. The minting rule is specified (§D8.1) and, for period-0 relations, implemented; the way to write an authored relation name is §D8.12, and the way to write one back out is §D8.13.NamedInitialAllocationnow has a surface that emits it, and is still refused at the importer's boundary — a migration may not mint a name, which is the distinction the two writer boundaries exist to keep.What an authorability constant is allowed to be named after. This note used to record an autonomous decision — that the kernel'sAUTHORABLE_VARIANTSstays[]while the V2 surface carries its own list — on the grounds that the two are answers to the same question about two different encodings. The reasoning was right and the naming was wrong, and the difference matters.AUTHORABLE_VARIANTSmakes a claim about every encoding, and once V2 shipped a surface that emitsnamed-initial, the claim that nothing is authorable anywhere was simply false — while the executable consequence,assertAuthorableAllocation, was refusing an allocation that a real surface really does emit. So the global constant now says["named-initial"], and the per-encoding shares are named for their encoding:V1_AUTHORABLE_SEED_VARIANTSis[]— permanently, because V1 has no field a name could live in — andV2_AUTHORABLE_SEED_VARIANTSis derived as the intersection of the global list with V2's initial variants rather than restated beside it. Each share is a subset of the global list, which is a property the tests check rather than a convention the reader is asked to maintain.This item used to also demand a validator enforcing that (kind,src,dst) is unique within a V1 artifact, warning that without it "the first genuine multigraph world silently seals as something it is not." That validator now exists:WRL_DUPLICATE_RELATION_KEY. The warning was also wrong in a detail worth keeping — such a world never sealed silently. It was refused, as a controller miscount, which is arguably worse than silence: a wrong diagnostic sends the author somewhere there is nothing to find.- An attribute type system.
capacity=3is meaningless without a unit and a range. This is the same requirement as typed ports (§D2), and neither should ship without the other. - Terminal-level validation. Single-writer becomes a property of a terminal rather than a global law, so validation must move with it: an owned terminal with two
sourceendpoints reaching it is still an error, and must still be caught before the seal. A terminal declaringresolved(f)is the one exemption, and the exemption is visible on the terminal. - A well-formedness rule per orientation. Roles and orientation can contradict each other and the checker must say so:
directedrequires at least onesourceand at least onetargetand admits no other role;symmetricadmits onlypeer;acausaladmits onlyterminaland carries no texture. Each violation needs its own code.These rules now run — §D8.7, withWRL_ENDPOINT_ROLE_ILLEGAL,WRL_INCOMPLETE_ORIENTATIONandWRL_ACAUSAL_TEXTUREas the three codes. What remains of this item is the other half of the texture row: texture is required fordirected, and no V1 artifact can supply one (§D8.8). It becomes enforceable when the artifact version moves, and not before. - A settlement rule per orientation.
directedsettles by the existing REACT pass — including into a resolved terminal, in one pass, provided the resolution is a pure, total, deterministic function of the canonical driver map with a declared empty value (§D2.1). Algebraic laws are not required: idempotence belongs to §9's join, and associativity and commutativity are priced separately as aparallel_foldopt-in).acausaldoes not settle by passing values at all and requires a solver wall (§D4).symmetrichas no settlement rule yet, which is the honest state of it. - A film schema. A relation's attributes are state, and §10 requires every state variable that determines future behaviour to be serialised.
§D9Dynamic topologydraft
Every world on this site has a fixed topology: the objects and routes
are what the document says, for all time. That is what makes
sem- a meaning. It is also the single largest gap
between Part II's thesis and any real fleet — drones are commissioned and
retired, a vehicle joins a road segment and leaves it, a lease expires, a
delivery is handed from one operator to another. A topology language that cannot
describe the topology changing describes a photograph.
sem- id mean? Three answers are possible:
the id covers the initial topology only (and says nothing about later states);
the id is recomputed on every change (and stops being a stable name for
anything); or the id covers the initial topology together with the rules
governing change, and each change is an event in the ledger. Draft rule D9
takes the third. It is the only one under which two hosts replaying the same film
must reach the same topology, which is the property §10 exists to
guarantee.Draft rule D9. The sealed artifact is the initial configuration
plus the authorised operations. A topology change is an operation applied
during a period, recorded in the event ledger, and replayed exactly. The world
identity does not move; the configuration at period n is a
deterministic function of the sealed artifact and the ledger prefix. This is
precisely the structure §10's ReplayBundle already
has, extended from claim state to structure.
One boundary, not four
An earlier draft of this section gave each operation its own timing rule: creation took effect next period, retirement at the end of the current one after FILM, boundary changes immediately before the next OBSERVE. Every one of those descriptions named the same instant in different words, and four spellings of one rule is four chances for two implementations to read it differently. There is one rule:
The reason is §8's REACT phase, which settles a cascade to a fixpoint by deterministic microsteps over a fixed configuration. If the configuration could change during REACT, the fixpoint would depend on microstep order and determinism would be lost at exactly the point §10 promises it. So every operation is proposed during a period, ordered canonically at COMMIT, and observed in the next one.
Nothing is lost by refusing the mid-period exceptions, because the thing they were reaching for is a different mechanism. Urgency is a safety question, not a structural one: a fault can suppress an identity's outputs within the current period through the supervision floor (draft §25) without deleting anything. Structural retirement still becomes visible at the next boundary. Silencing an actor now and removing it from the graph are two operations, and conflating them is what made retirement look like it needed its own timing rule.
The eight operations
Reaction rules in a bigraphical reactive system [B] reconfigure a bigraph locally and may create or destroy nodes. That is the general form; the list below is its closed, checkable specialisation. It is closed on purpose — an open set of topology operations is an open set of ways for two hosts to disagree.
| # | Operation | Changes | Ledger fact |
|---|---|---|---|
| 1 | create identity | adds an object with a role, config and ports | IdentityCreated |
| 2 | retire identity | marks an object non-participating; its history stays addressable | IdentityRetired |
| 3 | attach relation | adds a relation over existing terminals (§D8), minting its relation_id from a claimant-owned granted allocation | RelationAttached { allocation, revision_id, … } |
| 4 | detach relation | ends a relation's participation; the id stays addressable | RelationRetired { relation_id, expected_prior_revision } |
| 5 | replace relation attributes | changes capacity, cost, latency … without changing endpoints or the relation_id (§D8.1) | RelationRevised { relation_id, expected_prior_revision, next_revision } |
| 6 | transfer ownership | moves an owned terminal's single writer from one identity to another, atomically — no period has zero writers or two | OwnershipTransferred |
| 7 | open / close boundary | admits or refuses crossings at a wall | BoundaryStateChanged |
| 8 | expire lease | the timed form of 2, 4 or 7, scheduled at attach time | the corresponding fact, with the deadline as its cause |
RelationAttached,
RelationRetired,
AttributesReplaced — with no field saying which
relation. That reads fine and is unimplementable: under a content address, the
third fact changes the very bytes the first two used as a name, and two identical
parallel relations cannot be told apart by any of them.
§D8.1 supplies the missing stable
relation_id, and the facts above are written in terms
of it. The verb AttributesReplaced is renamed
RelationRevised to match: what happens is a relation
gaining a new revision, not a free-floating attribute set being swapped.The three relation facts above show the fields that distinguish them,
not their full field lists. Each also carries
provenance and
offered_evidence, which are common to all three and
are specified once, in §D8.3, rather than
restated here. The rule that put them there is the same rule that keeps them from
being written down twice.
The ledger grows; the active view does not
Notice that the fourth column has no verb that means delete. Retiring an identity and detaching a relation append a fact; they never remove one. Nothing is ever physically taken out of the replicated store, and the active topology is a derived view:
active_topology(sealed_artifact, ledger_prefix, period)
This distinction is what lets §9's grow-only evidence model survive a non-monotonic world. The set of facts is grow-only and merges by an associative, commutative, idempotent join, exactly as before. The set of active identities and relations rises and falls, because it is computed from the facts rather than stored alongside them. A film from period 40 can still resolve every identity it names — as §10 demands — because the fact that named them is still there, with a retirement fact after it rather than in place of it.
What every operation must answer
An operation is not specified until all eight of these are answered for it. None of them is optional, and each one is a way for two hosts to diverge:
| Obligation | The general rule |
|---|---|
| Authorisation | which capability permits it, and which wall grants that capability. An unauthorised operation is a diagnostic, not a silent no-op — a world where a change failed and nothing said so is a world whose film lies |
| Canonical event identity | a content address over (operation, arguments, period, proposer). Two hosts proposing the same change must produce the same event id, or the ledgers differ and replay fails |
| Timing semantics | the configuration boundary above, stated normatively rather than by convention. There is one rule and each operation must be shown to obey it, not given its own |
| Conflict rules | what happens when two operations in one period touch the same object. Ordering by canonical event id makes the outcome deterministic; it does not make it right, so incompatible pairs (two transfers of one terminal, attach-and-retire of one identity) must be a diagnostic and not a race that the smaller hash wins. For relations this is classified before acceptance, because §8's acceptance law is MIN CandidateKey — which is precisely the smaller hash winning |
| Historical representation | a retired identity is not a deleted one. §10 requires that a film from period 40 still resolve every identity it names, so retirement appends a fact and never erases one |
| Film consequences | the operation appears in the ledger with enough detail to reconstruct the configuration, and the film records the configuration digest at each period so a diverged replay is detected at the period it diverged, not at the end |
| Replication behaviour | which of the five classes below the operation falls into — at the granularity of the relation kind and the invariant it is subject to, not of the verb |
| Archival behaviour | when a retired identity's state may be dropped without changing any future observation. Nothing may be collected while a reachable film still requires it — which makes garbage collection a property of the evidence, not of the runtime's convenience |
Two valid preconditions can still be a conflict
§D8.1 gave one concurrent-revision example —
two RelationRevised facts naming the same relation
with different expected_prior_revisions — and
that is the easy case, because one precondition is simply stale. The hard case is
the common one, and it was not stated:
RelationRevised { relation_id = L, expected_prior_revision = R0, next_revision = R1 }
RelationRevised { relation_id = L, expected_prior_revision = R0, next_revision = R2 }
Both observed the same current revision. Both preconditions are
individually valid. They disagree only about the result. Nothing in a
compare-and-set rule rejects either one, so the outcome is decided by whichever
is applied first — and §8's pinned acceptance law is
MIN CandidateKey / first receipt, which is the smaller
key winning. That is deterministic and it is exactly what the
conflict obligation says is not good enough. A
lane closure and a lane reopening, proposed in the same period from the same
observed state, must not be settled by a hash comparison.
relation_id within one configuration boundary, all
proposed lifecycle operations are classified as a set before any is
applied. §8 already separates recognition (distinct-CandidateKey
count) from acceptance (MIN CandidateKey), and this is a recognition-stage
law: the acceptance policy never sees a pair it should have refused. That is the
same discipline §9 states for conflicting receipts — recognized evidence for
later settlement, never blindly merged — applied to structure.What is proposed, and what was observed proposing it
Before the table can be written, one word in it has to stop meaning two
things. §D8.3 put provenance and
offered_evidence onto the event and left the event id
a hash over all its arguments — deliberately, since the alternative is a
rule with a silent exception. The consequence was not noticed:
revise L: R0 → R1, evidence = [certificate_A]
revise L: R0 → R1, evidence = [certificate_A, inspection_report_B]
Two different events. One transition. The second is the first with a supplementary document attached, and under a rule that classifies by event identity the pair is not a retry — so a proposer who adds an inspection report to a pending proposal has manufactured a lifecycle conflict out of an audit trail. That is an evidentiary difference contaminating a structural decision, which is the same category error §D8.3 was written to stop.
TransitionIntentKey = H(operation core, target,
expected_prior_revision, next_revision)
RelationBirthKey = TransitionIntentKey where operation = Attach
ScheduledTransition { intent_key, effective_period }
ProposalEvent { scheduled, proposer, provenance, offered_evidence }
event_id = H(canonical ProposalEvent)
TransitionKey with
period inside its preimage, and it broke a rule four
hundred lines away in the same commit that wrote it.
§D8.6 burns an allocation against "that
proposal's transition identity" and permits re-proposal "for that same
transition". With the period in the key, a proposal retried across a boundary —
which is the ordinary case under partition, and the only case a retry exists for
— carries a different key, so D8.6's own second row classifies the honest
retry as an allocation collision. Two rules, written days apart, disagreeing
about what "the same transition" means. Splitting the schedule off the intent is
what makes them agree; nothing else here changes.TransitionIntentKey answers what change is
proposed and contains no period. A
ScheduledTransition answers for which boundary.
The event_id answers which complete envelope was
observed. Many events may name one schedule; many schedules may name one
intent. Lifecycle conflict is classified on scheduled transitions;
allocation burning binds to the intent. Whether several envelopes for one
transition accumulate their evidence, or one must win, or contradictory evidence
is itself a verification conflict, is an acceptance-policy question — and
it is a different question, decided after classification and never during
it.| Layer | Answers | Carries a period? | Consulted by |
|---|---|---|---|
TransitionIntentKey | what change is proposed | No, by construction | retry recognition; §D8.6's burn |
ScheduledTransition | for which boundary it is proposed | Yes — that is its entire content beyond the intent | conflict classification, which is boundary-scoped |
event_id over ProposalEvent | which envelope was observed | inherits the schedule's | ledger addressing; nothing classificatory |
Dropping the period from the intent raises an immediate objection, and it has
to be answered rather than waved past. A relation may be revised R0 → R1, revised
back R1 → R0, and revised R0 → R1 again; the first and third proposals have
identical intent keys and are plainly not one intent. They are, however, never
compared — the classification table states its own scope in its header, and the
scope is one boundary. Two proposals for different boundaries are not a
pair. A period-free key is complete inside the scope it is used in, and
effective_period is the thing that says which scope a
proposal is in.
The allocation burn is not boundary-scoped — it is permanent — so the objection has to be answered again, and differently, there. It answers itself, but the first way I wrote the answer was too strong. It said an attach intent cannot recur, and that is false as stated: a proposal can be emitted any number of times. The exact claim is narrower and is the one the rule needs:
RelationBirthKey are retries before
attachment and invalid duplicates after it. The proposal may recur;
the birth cannot happen twice. That is what makes a period-free key safe
for a permanent burn — once-only by construction rather than by scoping.
Revisions can repeat; births cannot, and only births spend a name.Because that distinction is load-bearing, the burn now binds to a
named specialization rather than to the general intent identity by way of
a sentence in a paragraph. RelationBirthKey is
TransitionIntentKey at
operation = Attach, and nothing else. Writing it as
its own name makes D8.6's permanent burn
visibly narrower than the reusable intent identity a revision uses: a
reader who wonders whether revising a relation twice spends two names can see
from the type that it does not. Prose that only one operation consumes an
allocation would have to be re-derived on every reading, and this page has
already shown what happens to facts that live in prose alone.
The schedule may say when, never what
One condition has to hold for any of the above to be sound, and it is easier to state as a rule than to keep as an assumption.
effective_period may control when a relation becomes
active. It must not silently change what the relation means. Where the
activation time is itself semantic — a contract that begins on a date, a rate
that depends on the activation period, a reservation with a window — that value
belongs explicitly in the revision or the attach arguments, where it is
part of the birth key and the content address. It may not leak in indirectly
through scheduling.Without D9.4 the split is not merely incomplete, it is unsound. The whole argument for a period-free birth key is that rescheduling changes the schedule and not which relation is being born. An implementation that lets the boundary reach into the meaning — a lease whose rate is read from the period it activates in — falsifies that premise directly: two schedules would then be two different relations wearing one birth key, which is the collision D8.6 exists to prevent, arriving by the one door the split leaves open. The rule closes it by requiring the semantic part to be written down where identity can see it.
Two more things fall out that are worth having on the record. The first is that the
conservative rule at the end of this section was already written in terms
of "one distinct lifecycle transition" while the table below it was
written in terms of events. The prose had the right rule and no defined term
behind it; D9.2 supplies the term, and the two stop disagreeing. The second is
that this is §8's own move: CandidateKey exists
precisely because which candidate and which receipt carried it are
separate questions, and the recognition law counts distinct keys rather than
distinct arrivals. §D9 needed the same split one level up and had been using one
identity for both.
Pair, same relation_id, one boundary | Outcome |
|---|---|
same ScheduledTransition — attach, revise or retire, whatever the envelopes | deduplicate — one transition proposed more than once, not two intents. Which envelope's evidence is kept, or whether they accumulate, is §D9.2's acceptance-policy question and is decided afterwards |
same allocation, different TransitionIntentKey | allocation collision — a burned allocation was recycled (§D8.6) or two allocators held one namespace (§D8.4). Which of the two it was is answered by the reservation record, not by this table |
same expected_prior_revision, different next_revision | competing-revision conflict — the case above |
different expected_prior_revisions | stale / branched-revision conflict |
| revision and retirement from the same current revision | lifecycle conflict — the relation cannot both continue and end |
| attach after a prior retirement | reject, unless an explicit reactivation operation exists — and it does not. Retirement is not deletion (historical representation), so the id is still addressable, and silently reusing it would make two different relations share one name across time |
The first version of this table had three deduplication rows — one each for identical attaches, identical revisions and identical retirements — because it was written before there was a word for "the same change". With §D9.2 they are one row, and the row says something the three did not: a retry is recognised by what it proposes, not by arriving byte-identical. Without that, a retried proposal under partition is indistinguishable from a second intent, which is the failure the nonce law prevents from the proposer's side. The two rules are one property approached from each end.
A digest detects divergence; it does not reconstruct a world
The film obligation above asks for a
configuration digest per period, and the surrounding sections have been treating
that as sufficient. It is not, and the reason is the one D8.2 created: once
lifecycle history is only in the ledger, the ledger's references have to
resolve, and a ledger of relations references far more than object ids —
allocations, revision ids, policy ids, admission evidence, acceptance receipts.
A digest proves two replicas disagree. It cannot tell either of them what a
revision_id stood for.
This is where a design draft has to be careful, because §10 does not leave the shape open:
ReplayBundle { initial_artifact, initial_state, event_ledger, frames, policy_ids }
; §10: "Frozen: the three-way distinction and the ReplayBundle shape.
; Not frozen: byte layouts -- they are the Film serialization."
| Way to satisfy D9.1 | What it costs |
|---|---|
Carry the canonical bytes inside event_ledger — it is already defined as "accepted events, receipts, effects, outcomes", and its byte layout is explicitly not frozen | nothing frozen changes. This is the default, and it is available precisely because §10 froze the shape and released the layout |
Add a sixth field, an object_closure of canonical bytes for every referenced semantic object | a new field on a frozen record. That is a spec revision with a version bump, not an editorial change, and Part II cannot make it. It is cleaner, and cleanliness is not a reason that outranks a freeze |
| Pin an external content store — the package names the store and its retrieval contract | replay stops being self-contained. Legitimate for large effect and solver artifacts, and it moves the failure from "hash missing" to "store unreachable", which must then be a stated failure mode rather than a hang |
Stating the three is the deliverable here. The draft's job is to make the choice visible before something is built on the assumption that a digest was enough, and to record that the tidiest option is the one that costs a freeze.
Closure is not enough; it has to be closure one way
The default above says the bytes go inside
event_ledger and stops there, which leaves the
serialization free to satisfy D9.1 in several different ways:
| All of these are closed | And they are different bytes |
|---|---|
| each referencing event embeds its own copy of a revision | a revision referenced five times appears five times |
| one shared copy is emitted up front | once, in some order |
| duplicates are kept but ordered differently | same set, different sequence |
| some objects are embedded, others left as bare references | a mixture, chosen per object |
Every one of them replays the same computation and produces a different film. That is not a small defect in a language whose central claim is that a run is a film that replays exactly: two conforming implementations would disagree on the artifact they hand each other, and neither would be wrong. Closure without canonicalization is the same failure as canonicalization without closure, arrived at from the other side.
EventLedgerBytes {
objects, ; sorted map: content_id -> canonical bytes
entries ; ordered accepted events, receipts, effects, outcomes
}
| # | Law on objects | What it removes |
|---|---|---|
| 1 | each referenced content id appears exactly once | copy-per-reference, and the question of which copy is authoritative |
| 2 | keys are sorted by raw content-id bytes | emission order as a degree of freedom |
| 3 | the bytes stored under a key must hash to that key | a table that lies, and makes it checkable rather than trusted |
| 4 | every transitively required id appears | D9.1's own requirement, now a property of a named structure |
| 5 | unreferenced objects are excluded by canonicalization, or rejected | a bundle that carries private payload and still hashes as conforming |
| 6 | entries reference objects only by id | the embedded/referenced mixture in the fourth row above |
| 7 | closure traversal is deterministic and finite | an implementation-defined walk, and non-termination on a cyclic reference |
None of this touches the frozen record. §10 froze the
ReplayBundle shape — five fields — and
explicitly released the byte layouts as a Film serialization concern; this is a
layout for one of the five, so it is exactly the change §10 left available.
Law 5 is the one worth reading
twice: without it, "closed" and "minimal" come apart, and a bundle could satisfy
D9.1 while carrying an arbitrary extra payload that every replaying host would
faithfully hash.
objects
table as a manifest … and omits only the large values", and concluded that
"the film's identity does not depend on whether a solver artifact was inlined or
fetched". Under law 3 those
cannot both be true. The objects table with its
values is what the film's bytes are; a table with the values omitted hashes
differently, and two hashes are two films. The sentence wanted external storage
to be free of consequences, and nothing in this section makes it free.There are exactly two coherent ways out, and they are not equally compatible with the rest of §D9:
| Model | Film identity covers | What follows |
|---|---|---|
| Manifest film — a manifest of content ids, shipped beside a separate object pack | the ids alone | films are small and cheaply exchanged, and holding one does not let you replay it. A holder learns exactly which objects they are missing |
| One canonical film — the film always contains the bytes; an external store holds a transport form that must be rehydrated before it is a film | the full closed bytes | films are as large as their contents. A pack is not a second serialization, it is a compression of the only one |
The first is the model this section was drifting toward, and §D9.1 has already ruled against it under a different name. Its opening sentence is "a digest detects divergence; it does not reconstruct a world" — and a manifest of content ids is a digest with better manners. Adopting it would leave D9.1's argument intact right up to the paragraph that serializes it, where a convenience quietly reverses it.
EventLedgerBytes
always contains the object bytes, and a film's identity is the hash of that form.
A deployment that keeps large values in a content store holds a transport
form, not a film: it becomes a film on rehydration, and
law 3 makes rehydration
verifiable rather than trusted, since every retrieved value must hash to
the key it was fetched under. A manifest without its objects is a digest, and
§D9.1 has already said what a digest is worth.The cost is real, and it is why the replaced sentence was attractive: a run that consults a large solver artifact has a large film, and D9.3 offers no way to make it smaller as a film. What it offers instead is a name for the smaller thing and a plain statement of what the smaller thing cannot do. Compression, deduplication across films and shared content stores all remain available; none of them is a film until it is closed, and being closed is the entire property §D9.1 exists to require.
The promise D9.3 is protecting is one sentence, and it is worth isolating from the machinery, because everything above is only a way of keeping it true:
Possessing a valid film is sufficient to reconstruct its replay inputs, without trusting the continued availability of an outside service.
That is also the test for whether some other artifact is a film. Four things get called one, and only two of them pass:
| Artifact | What it contains | Replays alone? |
|---|---|---|
| Canonical film | the complete canonical object closure | Yes |
| Compressed film | a losslessly compressed canonical film | Yes, after decompression — the bytes are recovered exactly |
| Transport pack | a film minus objects held remotely or shared | No, until rehydrated |
| Manifest | ids and retrieval information | No, and no amount of local work changes that |
Physical storage need not be monolithic, and D9.3 is often misread as saying it must be. Shared content-addressed chunks, deduplicated object stores, Merkleized storage, streaming and pack files are all available, and a deployment using every one of them can still be conforming. What is fixed is the canonical logical bytes: they are uniquely defined, and rehydration has to reproduce them exactly. The optimizations are permitted precisely because law 3 makes their result checkable.
Closure stops at the effect wall
Read without a boundary, D9.3 is not merely expensive but absurd. If a period consults an external model behind a gate, and a film must contain everything needed to reconstruct its replay inputs, then a naive reading demands the provider's weights, its inference cluster, its operating system and the packets on the wire. No such film has ever been produced and none ever will be. The rule needs to say where the closure ends, and the honest place is the point where WRL stops making decisions:
request → authorized effect → observed response → effect receipt
| |
+------------------------- inside the film --------------------------+
the wall is the effect itself, not what lies behind it
| The film needs | The film does not need |
|---|---|
| the canonical request | the provider's model weights |
| the adapter and policy identities | its inference cluster or operating system |
| the observed response | the physical network packets |
| the acceptance and effect receipts | any state private to the far side |
| any state WRL used after the response | a guarantee the provider still exists |
Replay reconstructs the world from the recorded outcome of an effect. It does not rerun the external universe, and it never claimed to. This is not a weakening: the outcome is what the world's subsequent decisions were actually made from, so a film containing it can reproduce every one of those decisions. An effect is a wall in the ordinary WRL sense — a boundary that commits and seals what crosses it — and closure stops at walls for the same reason everything else in this language does.
There is one case where the wall moves, and it moves because the promise changes. Where WRL claims a derivation is deterministic and independently re-executable, the recorded outcome is no longer sufficient — the point of the claim is that someone else can recompute it — so the solver program and the artifacts it needs do enter the closure. Two different promises are being made in the two cases, and a single word for both would let a film advertise the stronger one while carrying evidence for only the weaker:
| Level | The promise | Required of |
|---|---|---|
| Replay-complete | reconstruct every WRL state transition from recorded observations and outcomes | every conforming film |
| Re-execution-complete | recompute designated derivations from their implementations and inputs | particular profiles, for the derivations they designate |
The identity stack these rules add up to
The stack is seven layers, and only five of them are new. The other two are frozen, which is what makes this a chain rather than a proposal: §8's acceptance receipt and §8b's film were settled before any of these rules existed, and §D8 and §D9 build between and beneath them without the standing to renegotiate either end. The reason the five new names are not five ways of saying one thing is that each answers a question none of the others can:
AllocationGrant authorizes a namespace
RelationAllocation selects one name within that namespace
RelationBirthKey binds that name to one semantic birth
ScheduledTransition proposes when that birth becomes effective
ProposalEvent records who proposed it, and with what evidence
acceptance receipt records why the world accepted or refused it ; §8 frozen
Film closes the complete replay-visible history ; §8b frozen
AcceptanceReceipt appeared in the summary having been
minted nowhere — a CamelCase record coined on the spot for something §8 already
owns and spells acceptance receipt. The fix was not to introduce the name
properly; it was to stop introducing it, which is why one line of this block is
deliberately not in the shape of the others. Two of the seven were never these
sections' to name, and writing six was an attempt to say so without
admitting which.Read downward it is an authority chain, and read upward it is a provenance chain. The test for whether a layer is redundant is whether removing it forces another to answer two questions — and every collapse tried in these sections did exactly that. Putting the period in the intent made one key answer what and when, and D8.6 and D9.2 immediately disagreed. Putting evidence in the structural identity made one key answer what and with what justification, and an inspection report became a lifecycle conflict. Letting a manifest be a film made one artifact answer what happened and where to find out, and D9.1's own opening sentence ruled against it.
What has to be proved when this becomes writable
A draft rule that is never reduced to a falsifiable property is a rule that will be quietly reinterpreted instead. The register below is the decisive battery: each row states a property, names the rule it would falsify, and declares two things that are easy to confuse — which layer the property lives at, and whether a check for it runs today. A row may not claim to run without naming a check that exists.
| Layer | What it settles |
|---|---|
model | the semantic layer: what an identity is, provable from sealed structure alone |
surface | needs syntax an author can write that does not exist yet |
runtime | needs a running world: grants, proposals, acceptance, crash and restart |
film | needs the recorded artifact: closure, packaging, rehydration |
The two axes are independent, and keeping them apart is not bookkeeping. Every
row of this register once read awaiting surface, which quietly asserted
that nothing here could be tested until syntax landed — and that is false for the
whole model layer, where identity is determined by
structure that is already sealed. Collapsing the axes would have pushed
the next milestone toward inventing syntax when the semantic model was the part
still unproved.
That is not hypothetical. Separating the axes is what produced
the kernel, and the model
layer is now empty of debt: eighty rows run. The suite enforces that as a
ratchet rather than a milestone — settling a model-layer property lowers the
permitted count, and a new model row therefore has to arrive already
executable. That ratchet is exactly what turned
D8.7 from prose into twelve tests in the same commit:
the rule could not be stated here without them. It also works in the
other direction, which is the less obvious half: four rows first filed as
awaiting a future artifact version turned out to be testable against two
V1 worlds, because the break they were waiting
for already happens every time a world is re-sealed. A row parked at the
wrong stage is the same defect as a row parked at the wrong status, and it is
harder to notice, because nothing about it looks unfinished.
The sharpest version of that error is a row that is half misfiled, and
two rows here were. "The same authored name in two worlds yields two ids" was
filed surface · awaiting, and "two overlapping grants do not collide" was
filed runtime · awaiting — both correctly, as far as it went, because
there is no syntax for naming a relation and no machinery for issuing a grant.
But an identity is a function of a preimage, and
both preimages are writable today: a NamedInitialAllocation
carries its world_id and a
GrantedAllocation carries its
grant_id. The arithmetic was never blocked;
only the way a person reaches it was. So each row is now two: a model row that
runs, and a narrower awaiting row that names the surface or the runtime it is
genuinely still waiting on. A single row covering both had the effect of hiding
a settled property behind an unsettled one, and reported less than was known.
The severity is the point. The model layer is defined as the layer with no external blocker, so a model row saying awaiting is saying "nobody wrote it" in language that sounds like "nobody could" — and that substitution is the entire failure this register exists to catch.
| Property | Expected result | Falsifies | Layer · status |
|---|---|---|---|
| every field of a revision mutated in turn, and the id re-derived through the projection | only kind and endpoints move relation_id; policy is free; the other four — domain, orientation, texture, attributes — are V1-fixed, each pinned to the single value V1 can mean, and the partition is checked total so a new field cannot arrive unclassified | D8.1 | model · executable |
a RelationRevision carrying a pointer to its predecessor | refused as WRL_REVISION_BACKPOINTER — lifecycle has one home, and it is the ledger | D8.2 | model · executable |
| the same relation structure sealed into two different worlds | the same revision id — which world described it is provenance, and provenance rides the event, not the value | D8.3 | model · executable |
the same local_counter minted under two different grant_ids | two different relation_ids — the separation is a property of the preimage, and it holds before any allocator exists to exploit it | D8.4 | model · executable |
two grants issued with overlapping counter_range, both actually drawn from by a running world | no collision — which needs issuance, validity and draw, none of which exist; the arithmetic half of this is the model row above | D8.4 | runtime · awaiting |
the same relation_name allocated in two sealed worlds | two different relation_ids — a NamedInitialAllocation carries world_id, so world-scoping is decided by the preimage, not by the surface that types the name | D8.5 | model · executable |
| the same four names as an author writes them, in two worlds they authored — with byte-identical seeds, differing only in a pulser's clock, which touches no route | two different relation_ids for every one of them, and no migration implied between them — in the sharpest form the law has, since every rev- also recurs: same name, same structure, different world, different relation. Were a rel- to survive here a name would be a global identifier, and two authors who never met could collide. This row said surface · awaiting from D8.12 until B.7, on the ground that no syntax existed for naming a relation; the syntax landed in B.4 and nobody re-read the row | D8.5 | surface · executable |
the same (kind, src, dst) edge key in two sealed worlds | two different relation_ids — and the same revision id, because identity is world-scoped and description is not | D8.5 | model · executable |
| a world re-authored and re-sealed, every edge key unchanged | a different relation_id for every one of them — and a world mapped against itself preserving all of them, so the first half is not passing because nothing ever pairs | D8.5 | model · executable |
| a relation present in one sealed world and absent from the other | reported unpaired, in whichever direction it is missing — never handed a counterpart the map invented for it | D8.5 | model · executable |
| same allocation, different initial revision | allocation collision, deterministically | D8.6 | runtime · awaiting |
a grant whose validity is evaluated against something the film does not record | refused — a replayer must reach the same verdict about a draw as the original run | D8.4 | runtime · awaiting |
| local reservation, crash, restart | the same allocation and birth key are retried, never a fresh counter | D8.6 | runtime · awaiting |
| every endpoint of a relation derived from a sealed world | a terminal record carrying object_id and port — never a bare object id, because a node has several ports and only one of them is on this wire, and never a packed object.port string, because then a consumer has to parse what the model already knew | D8.7 | model · executable |
every endpoint's role | one of the four declared roles — a port name in the role slot is the 0.1 defect, and it type-checks against nothing | D8.7 | model · executable |
a revision whose endpoints array is written in reverse | the same revision_id — the roles carry the direction, so array position may not | D8.7 | model · executable |
| canonicalisation of a revision with endpoints in arbitrary order | endpoints sorted by role in the declared enumeration order, then by terminal — sorting roles alphabetically would put peer first and disagree with the table | D8.7 | model · executable |
| a revision naming the same terminal twice, in either the same or a different role | refused as WRL_DUPLICATE_TERMINAL — the normative rule was stated and unenforced | D8.7 | model · executable |
a directed relation carrying a peer | refused as WRL_ENDPOINT_ROLE_ILLEGAL | D8.7 | model · executable |
a symmetric relation carrying a source | refused — a symmetric relation with a writer is a directed one that has not admitted it | D8.7 | model · executable |
an acausal relation carrying a target | refused — an acausal connection has no writer to name | D8.7 | model · executable |
an acausal relation carrying a texture | refused as WRL_ACAUSAL_TEXTURE — the solver wall of §D4 already governs settlement, and a texture would be a second account of it | D8.7 | model · executable |
a directed relation stating no texture at all | refused as WRL_MISSING_TEXTURE — §D8's texture row is required for directed, and 0.1.1 enforced only the acausal half while filing this one as a V2 obligation, which is a table with one live row and one commented row | D8.7 | model · executable |
| the set of textures the implementation admits, against the one V1 can write | exactly §5's four — solid, async, verified, fault — with V1 encoding only solid. What the surface can write and what the model declares are different sets, and narrowing the second to the first deletes three guarantee classes while every other texture check still passes | D8.7 | model · executable |
| an importer that reverses the role enumeration and widens the legal role sets, then re-runs canonicalisation | unmoved — the exported tables are deep-frozen, so canonical order and role legality are still the ones this page declares. A closed enumeration a consumer can reopen is an open one with a comment | D8.7 | model · executable |
a directed relation with sources but no targets | refused as WRL_INCOMPLETE_ORIENTATION — a hyperarc is an ordered pair of vertex sets and neither may be empty | D8.7 | model · executable |
| the set of orientations the implementation admits | exactly directed, symmetric, acausal — the 0.1 kernel branched on an undirected that appears in no table on this page | D8.7 | model · executable |
| every V1 edge of a sealed world taken to a revision and back | the byte-identical edge — with the reverse direction found by role and the port suffix checked against the kind, never endpoints[0] and endpoints[1] | D8.7 | model · executable |
| every field of every relation imported from both pinned worlds, classified as read, resolved, supplied or absent | read fields equal a value the artifact carries; domain is resolved to the namespace the profile declares — signal, never the profile id, which is what declares domains; supplied fields equal the declared constant, including texture = solid, which V1 elides rather than lacks — and the four sets exhaust the field list | D8.8 | model · executable |
| a named-initial or granted allocation offered at the importer's boundary, and again at the authoring surface's | refused at both as WRL_UNWRITABLE_ALLOCATION, while relationIdFromAllocation still mints all three — which variants exist, which a trusted importer may construct, and which a surface may emit are three authorities, and folding them into the hash made identity depend on who was asking | D8.8 | model · executable |
| a page that shows the derived view, given a world V1 refuses | V1's frozen answer — the derived layer describes a world that sealed and does not decide whether one seals, however much better its diagnostic is | D8.8 | model · executable |
| the structural correspondence between two sealed worlds, recomputed from the two artifacts alone | every id in it recomputable by each side on its own — a correspondence that has to be authored is one that can be maintained wrongly | D8.8 | model · executable |
| the imported world derived in ignorance of the import, and re-sealed after it | the same relation_ids and the same sem- — the import is a ledger fact, and a world whose ids shift because it was imported from is a value carrying provenance | D8.8 | model · executable |
| each of the three allocation variants, with a field added and a field removed | refused as WRL_BAD_ALLOCATION in both directions — the preimage of an id is an exact field set, and a variant that tolerates an extra key hashes two different things to one name | D8.1 | model · executable |
a well-formed artifact handed to the derivation alongside a sem- it does not hash to | refused as WRL_SEMANTIC_ID_MISMATCH — a world-scoped id whose scope is a claim the caller supplies is world-scoped to whatever the caller says | D8.5 | model · executable |
| the derivation run with no claimed id at all | the same ids as the checked call — the claim is a cross-check, not the source, so the two paths cannot disagree and there is no unchecked path in | D8.5 | model · executable |
an artifact declaring an ir_version outside the frozen set | refused as WRL_UNSUPPORTED_IR_VERSION — the adapter's constants are V1's, so applying them to a version whose elisions it has never seen is a silent reinterpretation rather than an import | D8.8 | model · executable |
an artifact declaring an undeclared rulepack_id, or a foreign profile_id, under a recognised ir_version | refused as WRL_UNSUPPORTED_RULEPACK / WRL_UNSUPPORTED_PROFILE — admission is the whole tuple, because a rulepack admitted for being a nonempty string is then copied into every derived policy and sealed into rev- identity. Policy is free is a claim about revision change, not a licence to read an unknown family | D8.8 | model · executable |
| the four V1-fixed fields, checked against every derived revision and then re-projected under an alternative value | each carries exactly the value V1 fixes — signal, directed, solid, {} — and every alternative is refused. A field V1 omits and a field V1 pins are different claims, and only the second licenses restoring it on import | D8.8 | model · executable |
| a world with no relations, mapped against itself | identity-preserving, with nothing paired, dropped or added — the degenerate case, where a correspondence that reports failure by returning an empty pairing is indistinguishable from one that works | D8.5 | model · executable |
the derived correspondence, inspected for anything shaped like a RelationImported | none — the pairs are candidates, and a derived view that emitted the ledger fact itself would be asserting that a migration was accepted because two artifacts happen to agree | D8.8 | model · executable |
a RelationImported fact naming a pair the two artifacts do not produce | refused as WRL_UNVERIFIED_IMPORT — the fact is asserted by an accepted operation, which makes it the one thing here that can be maintained wrongly, which is exactly why it is the one thing checked against something derived | D8.8 | model · executable |
a V2 artifact grown an edges key, and a V1 artifact grown a relations key | both refused — WRL_LEGACY_EDGES_IN_V2 and WRL_RELATIONS_IN_V1. The rule is two-sided because an encoding that merely ignores the other's topology key admits a world whose meaning depends on which reader opens it | D8.9 | model · executable |
| a V2 artifact with a foreign version, profile or rulepack, and the declared family table held against a world that really sealed | each coordinate refused with its own code, and the table agreeing with the artifact the encoder admits — a gate checking a constant nobody publishes is a gate on nothing | D8.9 | model · executable |
the seed field table and the allocation field table, compared as an equation, plus a seed carrying a world_id | seed fields ∪ {world_id} = allocation fields, for every seeded variant, and a stored world_id refused — it is the hash of the bytes the seed sits in, so the copy is either a value that does not exist yet or another world's | D8.9 | model · executable |
a relation record carrying a world_id, a relation_id or a revision_id | refused — all three are functions of these bytes, and a stored copy is a second source of truth for a hash, which is how a forged id gets believed | D8.9 | model · executable |
a granted seed written into a world's initial relations | refused as WRL_UNWRITABLE_SEED — a grant is drawn at runtime from an authority that does not exist at period 0, so a granted seed here is a runtime fact asserted before the runtime ran | D8.9 | model · executable |
| a native V2 relation written with no name — key absent, and key empty | refused as WRL_MISSING_RELATION_NAME, never a fall back to a legacy-edge seed built from the endpoints. That fallback would make two independently authored relations over the same two ports the same relation, silently | D8.9 | model · executable |
a world whose relations array is written in reverse | byte-identical canonical output, and the emitted order equal to the seeds sorted — an array's order is in its bytes, so an unordered set has its order decided by the canonical form or by whoever typed the world | D8.9 | model · executable |
two relations in one world under the same identity_seed | refused as WRL_DUPLICATE_RELATION_SEED — they expand to one allocation and therefore one relation_id, so the world cannot say which of the two a later revision revises | D8.9 | model · executable |
a legacy-edge seed offered as admissible bytes, and again at the authoring boundary | admitted at the first and refused at the second — the seed records that a relation crossed the V1 migration without ever being named — its id moved with the world id, as every relation's does — so authoring one claims a provenance that did not happen. Naming such a relation is §D8.16's adoption, an explicit act, and not something a surface may do on its own | D8.9 | model · executable |
| every revision imported from a V1 edge, canonicalised standing alone and again inside a V2 relation | identical bytes — a revision is standalone under §D8.3, so a family-dependent canonical form would give one wire two rev- ids on either side of the migration that law exists to make checkable | D8.9 | model · executable |
| every exported V2 table, written to from outside the module — and the refusal that depends on one, retried afterwards | unmoved, and a granted seed still refused. An exported table that decides bytes is a shared mutable object unless it is frozen, and a consequence re-checked is the difference between frozen bytes and a frozen claim | D8.9 | model · executable |
a world whose relations and objects are both written in reverse | the same world id and the same relation view — neither array's written order is in the canonical bytes, so neither may reach an id. A value derived before canonicalisation names how a world was typed | D8.10 | model · executable |
a real artifact paired with a forged sem-000…0, and the allocations that derivation actually built | refused as WRL_SEMANTIC_ID_MISMATCH, and no allocation minted under the forgery — V1 shipped the believing version, which returned real-looking ids without complaint | D8.10 | model · executable |
an expanded seed held against §D8.1's own constructor, and a world id that is not a sem- id smuggled through the expansion | byte-identical to the kernel's allocation, the same rel-, and the non-seal refused by the kernel's validator — a private reconstruction that drifted would surface as an id that exists and is wrong | D8.10 | model · executable |
| one topology sealed in both encodings | two different sem- ids, both well formed — a shared prefix says both name a sealed world, a shared value would say the encoding is not in the bytes. This is why a downgrade produces a new artifact | D8.10 | model · executable |
one relation's attributes edited, and every relation's pair of ids compared across the edit | every rel- moves, because the world id moved and the world id is in every allocation; only the edited rev- moves, because a revision is standalone. That asymmetry is §D8.5 arriving in the native encoding | D8.10 | model · executable |
| two worlds whose relations differ only in name, compared as multisets because renaming re-sorts the canonical array | every rev- recurs and no rel- does — a revision names a structure and a relation names a thing in a world, so exactly one of the two survives being renamed | D8.10 | model · executable |
| the artifact's bytes before and after deriving from it, and the canonical output searched for a derived id | unmoved, and no rel-, rev- or world_id present — a derivation that memoised an id into the artifact would move the artifact that produced it | D8.10 | model · executable |
the relation count, the id shapes, and the distinctness of every rel- | one well-formed pair per relation, all distinct — the boring check, which catches the loud failures: a relation silently dropped, a stringified object as an id, two relations collapsing onto one allocation | D8.10 | model · executable |
the seed variants a migration emits, a named-initial seed offered to the importer, and a migrated seed offered to an author | only legacy-edge emitted, and both crossings refused — naming each migrated relation does not make its id stable, and it makes an import look like authorship while colliding with the first author who later writes that name | D8.11 | model · executable |
| every key the migration does not own, compared across the change | byte-identical — a migration that also tidied objects would make the world differ from its source for an unrelated reason, and no test could then attribute a moved id to the migration rather than to the tidying | D8.11 | model · executable |
| the two id multisets across a real migration | every rev- recurs, no rel- does — a migration reporting a preserved rel- would be claiming the thing §D8.5 exists to deny | D8.11 | model · executable |
the V1 → V2 correspondence run through the kernel's existing RelationImported checker, honestly and with a forged to_relation | verified and refused respectively, by unchanged code — a rule needing its own verifier at the boundary where migrations actually happen has an exception exactly where it is load-bearing | D8.11 | model · executable |
| the artifact across V1 → V2 → V1, and a second pass over the result | byte-identical to the original, and the second pass a fixed point — a change of encoding costs nothing, so there is no normalisation to bound | D8.11 | model · executable |
| each side's own sort key, whether the keys disagree on this world, and where the disagreement ends up | V1 is sorted by (kind, src, dst), V2 is sorted by seed bytes, the two disagree here — and the downgrade lands in V1's order anyway, so the sem- returns. An order is canonical only within an encoding, so the encoding that decides is the one being written; the V2 world keeps an id of its own for the ordinary reason, different bytes. This row was twice wrong before it was right — see the note under D8.11 | D8.11 | model · executable |
| a named relation added to a migrated world, put through the correspondence | added, never paired — it has no legacy key, so pairing it would mean pairing on something other than a preimage, and a relation with no V1 preimage is not one the V1 world quietly also had | D8.11 | model · executable |
a forged sem- on the V1 side, and again on the V2 side | both refused as WRL_SEMANTIC_ID_MISMATCH — a migration is precisely where a caller holds two worlds and is best placed to be wrong about one of them | D8.11 | model · executable |
| a named source, stripped back down, and the artifact the spine validated | the V1 source byte for byte, same line count, and the artifact the V1 parser produces — a surface that also normalised whitespace or re-ordered declarations would be a second parser agreeing with the frozen one today | D8.12 | model · executable |
| an unnamed route under the V2 parser, and the same source under the V1 one | WRL_MISSING_RELATION_NAME at the authored line, while the V1 parser seals it fine — a name is never derived from the endpoints, which would re-mint the relation whenever an object is renamed | D8.12 | model · executable |
[clock-feed], [clock feed], [0feed], [] | WRL_BAD_RELATION_NAME for each, at the authored line — the prefix is matched loosely and judged afterwards so a bad name is not reported as a malformed route | D8.12 | model · executable |
| one name over a two-way fan-out, and a name on a declaration | WRL_AMBIGUOUS_RELATION_NAME for both, the fan-out at the authored line — zero and many are the same fault, and the many case is reachable by accident because sugar expands one line into several routes | D8.12 | model · executable |
| a world whose authored order, parsed order and V2 order are three different orders | every name on the relation its own line wrote, checked against the endpoints read back out of the source — a zip by line index passes this only by luck, and a mis-attached name seals silently | D8.12 | model · executable |
| two routes under one name, and the code table | WRL_DUPLICATE_RELATION_SEED from the encoder, and no WRL_DUPLICATE_RELATION_NAME exists — the first spelling keyed its map by name, quietly overwrote the earlier entry and reported the collision as a missing name somewhere else | D8.12 | model · executable |
| one relation renamed, and the three ids re-derived either side | every rev- recurs, no rel- survives, the world moves — a rename that moved a rev- would mean the name leaked into the revision; one that moved no rel- would mean the name is not the preimage | D8.12 | model · executable |
| the variants the surface actually minted, and a legacy seed offered by an author | named-initial only, every seed through the authoring gate, and the legacy one refused — legacy-edge is a fact about an import, not something an author can assert about a relation they are writing | D8.12 | model · executable |
| a named world put through format → parse → format | the same canonical bytes, the same world id, the same text — the surface is a surface and not a lossy view | D8.13 | model · executable |
| the V2 output with its names stripped back off | byte-for-byte what the frozen formatter emits — proved behaviourally rather than by grepping for an arrow; the V2 side contributes prefixes only, so there is no second copy of the route syntax to drift | D8.13 | model · executable |
| a migrated world offered to the formatter, and the same world offered to the deriver | WRL_UNWRITABLE_SEED, while the world still derives its relations normally — the limit is in the surface, not the world, and a formatter minting the missing names would be deriving identity | D8.13 | model · executable |
| two named relations over one pair of terminals, offered as an artifact and again as the two source lines that would write them | WRL_CONTROLLER_CONFLICT in both encodings, while the name stripper reads two names on two lines — this row previously claimed the text could not exist, and both halves were wrong. [a]: and [b]: on two otherwise identical routes are two distinct unambiguous lines, and the world is refused by the profile's own controller law rather than by the surface. It only looked like a surface debt because the V2 gate did not look at the world | D8.13 | model · executable |
| a profile whose controller law admits more than one signal-wire input, and the two-relation world written under it | accepted, with two distinct rel- ids and a source form that reads back as the same world — the multigraph V2 exists to make possible is a profile obligation, not a surface one, and nothing is owed by the encoding until a profile ships that law. Recorded as awaiting rather than as an impossibility, because "this validator does not accept it yet" and "this can never be written" are different claims and only the first one is true | D8.13 | surface · awaiting |
| the world id either side of a format, in both encodings | unmoved, and the V1 world underneath still seals to the pinned demo sem- — formatting is normalisation, never a compiler gate | D8.13 | model · executable |
| the order the formatter emits routes in, against V1's | the canonical seed-byte order, and not V1's — source route order decides nothing because the spine sorts it, so emitting the order the bytes are already in restates no sort key | D8.13 | model · executable |
| the V1 artifact a V2 world is consumed as | byte-identical to the one the spine validated on the way in, and so it seals to the pinned demo sem- — V2 changes how topology is written, not what a world is, so nothing downstream of the seal learns a second encoding. This row read “the same relation set, and every other key byte-identical” while a real ordering bug lived in edges; a comparison that excludes the field a bug is in is not a weaker check but a check of something else | D8.13 | model · executable |
| the V1 world underneath a named source, sealed | the pinned demo sem-, unmoved — naming relations adds identity to relations, and if the surface had perturbed a config, an order or a port projection this is the id it would show against | D8.13 | model · executable |
eight mutations of one world that seals — role "Alien", a duplicate object id, a terminal on an object that does not exist, domain: "banana", kind "WarpTunnel", port "made_up", a second relation onto a controller input, and an invalid static_config | no sem- for any of them, while the unmutated world still seals — every one of the eight got a well-formed id for one slice, because the encoder validated the encoding and never the world. An id for an impossible world is worse than a refusal: a refusal stops, and an id is stable, citable and believed downstream | D8.14 | model · executable |
| the codes the V2 gate answers with, against the frozen spine's vocabulary and against V2's own | every one of them the spine's, none of them V2's — the gate derives the V1 world and asks the frozen validator, so it holds no registry to drift. A V2 validator with its own list of roles and ports would be a second opinion about what the profile permits, free to disagree with it silently, because nothing ever compares two registries that never meet | D8.14 | model · executable |
| a route-free source — a profile line and nothing else — handed to both parsers, and the same bytes offered undeclared | two valid worlds with two different sem- ids from one byte string, so the undeclared source is WRL_MISSING_IR_HEADER — a world's id may not depend on which function a caller reached for. This is the private-sort-key defect arriving through the front door | D8.15 | model · executable |
| absent, doubled, versionless, over-long, from a future family, declared after something else, with no profile at all, and commented out | a distinct typed refusal for each, the missing-profile one raised by the frozen profile checker rather than a second copy of it, and a commented-out declaration correctly not one — the header stripper asks validateProfileHeader where the block starts instead of counting lines, so "the profile comes first" stays one rule with one implementation | D8.15 | model · executable |
| a migrated world adopted under supplied names, and the formatter offered it before and after | every rev- recurs, the world sem- moves, every rel- is new, and the formatter turns from WRL_UNWRITABLE_SEED to writing it — without this the migration is a one-way door out of the language, and the world it produces can never again be handed to an author as text | D8.16 | model · executable |
| eight ways of not supplying a usable name: nothing adopted, the key omitted, the name empty, a non-identifier, an unknown relation, one relation adopted twice, two relations under one name, and an unrecognised assignment key | a typed refusal each, and not one of them produces a name — a generated name would make §D8.1's forbidden move legal by relocating it one function to the left, and would then be indistinguishable in the bytes from one an author chose | D8.16 | model · executable |
| one relation of several offered for adoption; then all of them at once; then an already-adopted relation offered again | WRL_INCOMPLETE_ADOPTION, with the argument left byte-identical; then every seed named and the formatter writes it; then WRL_UNKNOWN_RELATION. A partial adoption seals — it mints a world sem- and re-mints every rel- in the world, including relations nobody touched, and then moves them all a second time when the rest are named — so two-step naming leaves throwaway identities that are indistinguishable from wanted ones. Collecting names is editor work and editor state is not sealed. B.6 permitted the partial case and registered a partly adopted world is still unwritable, every word of which was true and which was the wrong behaviour to pin | D8.16 | model · executable |
| the published editing surface, swept for the path by which each of the two operations is reached | each is called from exactly one place, and the listener that fires without a person — the editor's own input — reaches neither. Both operations seal: an import mints a world sem- and every rel- under it, an adoption mints another set, and a two-second edit would mint a dozen real worlds whose ids something downstream may already hold. This is a textual guard on the published page, and it proves the wiring half of the rule; the arithmetic half is the row below | D8.16 | surface · executable |
| the surface's own workflow, run end to end over the pinned fixture: import, refuse to write, refuse a short assignment, adopt, format, and admit the text that comes out | three different world ids and one unmoved execution view. The import moves the id, the migrated world will not write (WRL_UNWRITABLE_SEED), a short assignment is refused (WRL_INCOMPLETE_ADOPTION), adoption moves the id again — and the text the formatter then produces re-admits to the world it was written from. Without that last step a surface could show an id nothing could get back to. The sharp end: the V1 execution view of the world at the end of all this is the pinned fixture's id, unmoved, so the world changed identity three times and what it runs as never moved once | D8.16 | surface · executable |
the same world offered twice to one entry point — once undeclared, once carrying ir 2.0 and relation names — with no flag, mode or selector passed either time | two families, two worlds: the undeclared bytes seal to the pinned V1 sem-, the declared bytes to the V2 world id. Which parser reads a text is a fact in the text; a tool that decided it from a dropdown would make a world's identity a function of the interface, which is the defect D8.15 was written against with a different guess in the blank | D8.17 | model · executable |
| five sources that declare an encoding badly — bald, doubled, over-long, an unsupported version, and one declared too late | refused in the V2 vocabulary, and never retried under V1. The fallback is tempting because such a source is usually a good world otherwise, so it would seal and print the id of a world in an encoding its author did not write; and the frozen parser, which has no ir rule, would answer with WRL_UNSUPPORTED_FEATURE pointed at the declaration — telling an author to delete the one line in the file that was doing its job | D8.17 | model · executable |
a V2 world's own sem-, beside the sem- of the V1 artifact it projects to | two different ids that are two different claims. The projection is byte-exact against the seal the spine already performed on the way in, and it lands on the pinned demo world; the V2 id does not. The second is proof of an execution view — it is not the scope any relation in this world is allocated in, and nothing may use it as one | D8.17 | model · executable |
an undeclared world admitted, compared key by key against a direct sealWorld; and an undeclared world that is refused | the frozen result with two keys added and nothing renamed, dropped or nested; the refusal reports the same code at the same line. A world in which nothing changed may not cost its readers a migration | D8.17 | model · executable |
| the published surface a reader types world source into, swept for a control that would decide the encoding | there is none, and the absence is the law. A selector may put starter text in an editor — text is the input — but a control that reinterprets text already there would make the id on screen a function of which button was last pressed. The page's dispatch is D8.17's dispatch, called rather than re-implemented, so the page cannot answer the question differently from the module that defines it | D8.17 | surface · executable |
a published, one-click example that is a well-formed V1 world carrying one broken ir declaration | admitted under v2 and refused there — never sealed by falling back. This is the example a fallback would have handled most convincingly, which is why it is the one that is published: a page that quietly downgraded it would show a reader a real sem- and no sign that anything went wrong | D8.17 | surface · executable |
a V1 world and a V2 world put through one projection function, with no family argument passed to either, and a third call handed a forged sem-000…0 | each arm reports its own world's id, flagged derived / not canonical / not in the artifact bytes; the forged claim is refused with WRL_SEMANTIC_ID_MISMATCH. A consumer holding a projection never has to ask which encoding it came from before knowing which id is the world's — and a projection that believed a caller's id would mint every binding under whatever it was told | D8.18 | model · executable |
the view id of a V2 world's projection, recomputed from execution_artifact by the same function a runtime would use on it | the runtime's own answer — it lands on the pinned demo world and differs from the world id. The danger was never that this number would be wrong; it is that it is right, and looks exactly like a world | D8.18 | model · executable |
| what crosses the runtime boundary, compared against the seal the spine already performed on the way in | V1 bytes, byte-equal to the spine's own reading — edges and no relations, in the frozen version family, while the semantic artifact beside it carries the opposite pair. The encoding stops here, which is the whole of what V2 claims about its own blast radius | D8.18 | model · executable |
| the same four edges derived twice — once as the projected world's own relations, once as the V2 world's bindings | every rev- shared and not one rel- shared. Structure is the same world; identity is not. So a runtime reporting against an edge cannot re-derive the relation it belongs to — it has to be told, and legacy_edge is the telling | D8.18 | model · executable |
| this world's own seeds expanded under the view id — the forbidden thing, done deliberately | relation ids the world does not hold, arrived at by correct arithmetic from real inputs; under the world id the same seeds mint exactly the ids it does. Nothing about the wrong one looks wrong, which is why the rule is a prohibition rather than a validation | D8.18 | model · executable |
every binding recomputed from semantic_artifact alone, in both encodings, and the key set of a binding record | the envelope's own answer, and exactly three fields. The narrowness is part of the law: a projection shipping its own preimages would be offering a check against itself | D8.18 | model · executable |
| a V1 world's projection, beside a V2 world's | coincident: the two ids are equal and the two artifacts are the same bytes, and the envelope says so rather than leaving it to be noticed. Totality over both encodings is what lets the first clause be stated without an unless | D8.18 | model · executable |
| the binding list and the projected edge list of one world, compared index by index | the same edges in different orders — V2 sorts by seed bytes, V1 by edge, and neither ordering is wrong. A consumer joining on the index would produce a total, plausible, entirely mis-attributed mapping, and the pinned demo world is one where it happens | D8.18 | model · executable |
| the published surface, swept for how it arrives at the execution view it shows — and for whether it takes the short way to one | one function, called once per family, and no second route to the same number. A page that projected the artifact itself and sealed the result would hold a real sem- with nothing attached saying it is not the world's, and would be free to label it anything; a raw downgrade is not wrong, it is mute, and every id it produces is correct. Calling the envelope in the V1 arm as well is the same rule: a page that asked for a projection only when the encoding was V2 would be treating D8.18's coincident case as a feature V1 happens not to need | D8.18 | surface · executable |
| a projection of each encoding serialised, crossed, and received | the same envelope, with the execution artifact and the coincidence rebuilt rather than transmitted — and the derived / not-canonical flags present on the far side because the receiver derived them, not because they arrived. The V1 case comes back coincident and the V2 case does not, neither having said so on the wire | D8.19 | model · executable |
| the transmitted record's key set, and the fields an envelope has that it does not | five fields, and the two that would have made the receiver's job easier are the two that are gone. The flags because on a wire they are bytes a sender chooses; the execution artifact because carrying it would let one message pair one world's semantics with another world's bytes to run. The omission makes that message unrepresentable rather than detectable | D8.19 | model · executable |
| three messages a sender could send: the world id filed as the view id, the bindings reordered, and a forged world id | WRL_PROJECTION_MISMATCH, WRL_PROJECTION_MISMATCH, WRL_SEMANTIC_ID_MISMATCH — the last one the deriver's, not a second implementation of the same comparison. The first is the mute downgrade's mistake written down: two real ids, each in the other's field. The second changes no identifier anywhere and is caught only because the order of the bindings is itself derived | D8.19 | model · executable |
| an extra key, a missing key, an unknown version, unparseable bytes, and an array | WRL_BAD_PROJECTION for all five. The extra key is the execution artifact, offered back as a convenience — refusing it is what keeps clause 2 a rule rather than a default. It also cannot be expressed in a JSON message at all, because a V1 artifact may hold a BigInt: the field's absence is enforced twice over, once by the gate and once by arithmetic | D8.19 | model · executable |
| one world serialised twice, and the bytes re-serialised through the spine's own serializer | identical both times, and already canonical. Without this a receiver could not compare two messages, and a cross-implementation comparison would have nothing to compare — the divergence clause 3 exists to expose would be invisible under a byte diff that two conforming senders already fail | D8.19 | model · executable |
| the committed vector file, every source re-admitted, re-derived, re-serialised and re-received | byte-identical, in both encodings. This is the only artifact here that an implementation which cannot run JavaScript can be held to, and it is exactly as useful as it is falsifiable: the moment the wire form moves it goes red, and a consumer pinned to those bytes finds out here rather than in production. No vector states its encoding — D8.17 says the source decides, so a reader that needed to be told would already be failing a different rule | D8.19 | model · executable |
| a bare artifact handed to the serializer | refused. The serializer exposes one intended route to the wire; it does not prove its argument came from the deriver, and nothing on this side could — provenance is not transmissible, which is precisely why the receiver establishes every claim rather than trusting any. A serializer that derived the projection itself would be a second route, and two routes can disagree about a world while both look right | D8.19 | model · executable |
| a frozen V1 world whose rotor lane is 263−1, serialised and read back | the same integer, and the same world id. Rotor lanes are the one artifact scalar left unbounded, so this is the whole of the wire's exactness domain and it was untested until a world was written that entered it. An ordinary JSON reader renders that token as 9223372036854776000 and the receiver then refuses a legal world — reporting a range error about a number the world never contained | D8.19 | model · executable |
duplicate keys, a fraction, leading whitespace, trailing bytes, -0 | each refused. A general parser keeps the last duplicate key silently, which lets two records with different bytes and different meanings arrive as one object — on a wire whose premise is that bytes are checkable, last-writer-wins is a way to walk a field past a reader. Clause 3 and clause 7 are one claim: bytes cannot be called canonical by a reader that accepts variants of them | D8.19 | model · executable |
| a record whose keys are re-ordered — same value, different bytes | refused, by re-rendering what the bytes denote and comparing. This is the gate that does not have to enumerate: it covers key order, escape spelling, leading zeros and every lexical variant nobody thought to name, so the reader's strictness does not depend on the completeness of a list | D8.19 | model · executable |
| the pinned world round-tripped through the exact reader | both ids unmoved, still coincident. The obvious wrong fix for the 64-bit case is to change how every integer is read, which would move every id in the repository at once. Exactness had to arrive as a reader that is stricter about bytes and identical about values | D8.19 | model · executable |
a two-object world under graphonomous.semantic.v0: RECEIPT → WITNESSES → CLAIM, in the application's own shape | a real sem-, and the canonical artifact carries exactly what the row derives — semantic_policies is { rulepack_id }, every object's ports are the row's, and no schemas or state_schema_ref appears. Before WRL-P0 this world drew WRL_UNSUPPORTED_PROFILE and the application was minting a gsem- of its own | D8.20 | model · executable |
| the same static world offered to the downgrade, the runtime projection and the text surface | each refused. A static row has said nothing about how a world runs, so a value that says it does would be a runtime claim sealed into identity for a runtime that does not exist. Clause 3 | D8.20 | model · executable |
a static world that writes an admit policy, a film schema or a state_schema_ref into its own bytes | WRL_V2_WORLD_MISMATCH — the artifact states a derived field its own profile does not derive. The row is the authority on what a static world may claim, not the submitter | D8.20 | model · executable |
| the table and every row in it, probed for mutability, and the static derivation, probed for any name of a profile | deep-frozen, and the derivation names no row. Clause 2: a row that could be assigned to is a row that decides bytes for whoever assigns last, and a derivation that knew which row it was reading would be a branch in a costume | D8.20 | model · executable |
| a static world with, in turn, an undeclared role, an undeclared kind, an undeclared port, an inadmissible (source role, target role) pair, and a signature the row does not declare | five distinct codes — WRL_UNDECLARED_ROLE, WRL_UNDECLARED_KIND, WRL_UNDECLARED_PORT, WRL_UNDECLARED_ENDPOINT_PAIR, WRL_PROFILE_SIGNATURE_MISMATCH — and no id minted for any of them. Clause 4 | D8.20 | model · executable |
graphonomous.semantic.v1 added beside v0; every v0 golden world re-sealed; a DEFEATER attacking a RECEIPT offered under each row, and the same relation reversed | v0 still 21 roles / 31 kinds / 92 pairs and every v0 id unmoved; v1 exactly 24 / 32 / 102 and a checked superset, pair by pair. The attack seals under v1, draws WRL_UNDECLARED_ROLE under v0, and reversed still draws WRL_UNDECLARED_ENDPOINT_PAIR under v1 — a successor widens by declaration and never by becoming permissive. Clause 5 | D8.20 | model · executable |
a forge.world.core.v1 world whose relation carries policy: "anything.at.all" | WRL_UNDECLARED_POLICY. Before WRL-P0 this world sealed to sem-b9b0c089…: the policy field was hashed and never checked against anything. The gate closes GAP-W9 by validation only — no accepted bytes move | D8.20 | model · executable |
a world declaring a profile_id the table has no row for | WRL_UNSUPPORTED_PROFILE, naming the rows that do exist, and no hash computed. Clause 1: the table made a second profile possible without making an unknown one acceptable | D8.20 | model · executable |
| the encoding's default profile and its rulepack, read from both tables | one answer. forge.world.core.v1 is the lowered row and the family default, and its rulepack is read from V2_RELATION_SOURCE_FAMILIES rather than restated beside it, so the two tables cannot disagree about it. Clause 7 | D8.20 | model · executable |
| the static world's derived relation view, held against the kernel called directly on the same seed and revision | the same rel- and the same rev-, scoped to the sealed sem-. The application submitted; the kernel minted. Clause 6, and the whole reason the spike's gsem-/grelpre- ids had to go | D8.20 | model · executable |
| the static world submitted with its objects and relations in a different order | the same bytes and the same id. Order is decided by the encoder and is the one part of an encoding no field rule checks — the trap §D8.18 found is refused here at the seal rather than discovered downstream | D8.20 | model · executable |
| the world gate's dispatch, under a lowered row and under a static row | delegates, and does not restate. The lowered row is checked by the frozen spine's own graphToIr; the static row by a derivation that reads the row. Neither path lists a registry of its own, which is what keeps the gate one rule with two readers rather than two rules | D8.20 | model · executable |
| a package whose reference resolves only through a service it does not pin | refused — replay must not depend on a contract the package never stated | D9.1 | film · awaiting |
| same birth proposed at period 20 and at period 21 | same RelationBirthKey, different ScheduledTransition | D9.2 | runtime · awaiting |
same schedule, different offered_evidence | one structural candidate, two proposal envelopes | D9.2 | runtime · awaiting |
| transport pack plus object store | rehydrates to a byte-identical canonical film | D9.3 | film · awaiting |
| a closure object is absent | an incomplete package — never a valid film | D9.3 | film · awaiting |
| a value that depends on the activation period, left implicit in the schedule | refused — two schedules must not share a birth key while meaning different things | D9.4 | runtime · awaiting |
| a film claims a derivation is re-execution-complete but omits its implementation | refused — replay-complete is the most it may advertise | D9.5 | film · awaiting |
layer · status is checked against the machine-readable
attributes too, because a table that says one thing to a reader and another to the
suite is worse than no table.D8.1, D8.2, D8.3, D8.4, D8.5, D9.1: six rules,
several of them the oldest and most load-bearing on the page, stated with
nothing anywhere saying what would falsify them. That is the same asymmetry that
let a retired equation survive four reviews. Attention goes to what was most
recently argued about, and a subset check only ever confirms what its author
already had in mind; only a census counts what is missing.What a period has to record about structure
The second half of closure is not about bytes but about what is worth recording, and §8 has already answered it for claims. The frozen blockquote says the phase distinction is what determines "which claims were seen, which candidate was accepted, whether a retransmission retries an effect" — so a film that recorded only accepted claims would already be non-conforming. Structure gets held to the same standard:
PeriodFilm { period,
observed_proposals, accepted_events, rejected_events,
conflict_outcomes, effective_topology_events,
configuration_digest, world_frame_digest }
rejected_events is the row that looks redundant
and is not. A rejected topology change does not affect future state, so a purely
state-oriented film would drop it — and dropping it means a film cannot answer
"did anything try to attach a writer to that terminal?" An attempt that
was refused is an observable fact about the world, and for authority and
admission it is frequently the fact that matters most. It is also the only
evidence that classification ran at all: a period in
which two competing revisions were proposed and one diagnostic was raised looks,
in an accepted-only film, exactly like a period in which one revision was
proposed.
Replication is a property of the invariant, not of the verb
An earlier draft of the row above said attaching a relation is merge-safe; transferring ownership is not. The second half is right and the first half is only right for unconstrained attachment. Two partitions can each attach a perfectly well-formed relation and still produce a union that violates a domain invariant — both consuming the last unit of road capacity, both attaching a writer to a single-writer terminal, both reserving the same charger, one attaching to an identity the other retired, or together exceeding a degree bound. Every one of those merges cleanly as a set of facts and is wrong as a configuration.
| Class | Example | Coordination required |
|---|---|---|
| Monotone addition | attaching an unconstrained observation relation | none — §9's join is sufficient |
| Addition under a bounded resource | reserving a charger; occupying one of three capacity units on a road segment | an allowance split across replicas in advance — the escrow or bounded-counter construction [E]. Local while the share lasts; coordination only when a replica's share is exhausted |
| Exclusive transition | transferring ownership of a terminal | consensus, or a unique lease authority holding the right to transfer |
| Removal-like transition | retiring an identity; detaching a relation | none for the fact itself — append the tombstone and let the deterministic active-view rules settle the outcome |
| Scheduled transition | a lease expiring | none, provided the lease and its deadline were accepted before the partition — the expiry is then a function of the clock, and both sides compute it |
The escrow class is the interesting one, because it is the case a naive
reading treats as needing global coordination and a careless one treats as
merge-safe. It is neither. It is coordination arranged in advance: the
allowance is divided while the network is healthy, so that the common path is
local and only exhaustion is expensive. That construction is the reason
capacity=3 in the sketch below is a claim a
partitioned world can actually keep, and it is also the reason
capacity cannot be an ordinary attribute — a bounded
resource has to be declared as one, in the profile, before anything can be
escrowed against it.
What it would look like
profile wrl.mobility.v1
[depot:west](bays=4){dispatch}
[segment:a12](capacity=3){occupancy}
; the operations a world authorises are part of what it seals.
; a change not named here cannot happen, however privileged
; the proposer — this is what keeps the id a meaning.
topology {
allow create of drone by [west].dispatch
allow attach mobility.occupies by [west].dispatch
escrow [a12].capacity
allow transfer occupancy by quorum
allow retire of drone by [west].dispatch
; an ADMISSION rule, not an enumeration: the world does
; not know which vehicles exist, and does not need to.
; what it seals is the test they must pass.
admit vehicle
identity = public_key
when certificate.root in #federation_ca
and schema == #vehicle_v1
and operator.quota_available
}
; a lease schedules its own expiry at attach time, so the
; release is in the ledger before the period that applies it
[d7] --mobility.occupies(cost=1, lease=6)--> [a12]
Sealed admission, not sealed enumeration
An earlier draft of this section ended by conceding that a world admitting genuinely unforeseen participants — another operator's vehicle joining mid-run — might be the point at which sealed identity and open topology genuinely conflict. That concession was too quick, and it mistook one way of closing a world for the only way.
A sealed artifact does not have to enumerate the identities that may exist. It has to fix the rule by which a new one is admitted. The distinction is routine outside this document: a SPIFFE trust domain [SP] is a trust root, and workloads are admitted by attested policy rules against that root rather than by having been listed in advance — which is exactly why two organisations can federate by exchanging bundles instead of exchanging inventories.
So what the seal covers is the admission rule and everything it depends on:
| Sealed in the artifact | Appears later, in the ledger and the film |
|---|---|
| the trusted certificate roots | the concrete certificate a vehicle presented |
| the identity scheme (what counts as a name) | the vehicle's actual public key |
| the schema new participants must satisfy | the instance that satisfied it |
| the admission checker, by content address | its verdict, and the evidence for it |
| the quota and resource policy | the quota state at the period of admission |
| the namespace rules | the allocated name |
Two hosts replaying the same film admit the same participants, because they run the same sealed checker over the same recorded evidence. The world identity never moves; what moves is the configuration, which §D9 already established is a function of the artifact and the ledger prefix. Novelty is admitted without being anticipated.
This matters more than it looks, because the interesting worlds are all federated ones: drone fleets crossing operators, public roads carrying anyone's vehicle, a utility interconnecting with a neighbouring utility, androids moving between owners. A language that could only describe a closed roster would have been unable to describe any of them. The remaining hard part is not identity — it is that admission is now a wall, and a wall that admits participants has to be as carefully specified as one that admits messages.
What D9 owes before it can ship
- Configuration digests in the film. Without a per-period structural digest there is no way to detect that two replays diverged, and the whole rule is unenforced.
- A capability model. "Authorised by" presupposes capabilities, which are drafted (draft §23) and not built.
- Attributed relations (§D8). Operation 5 changes attributes that do not yet exist.
- A conflict diagnostic set. Every incompatible pair needs a code, in the same family as
WRL_CONTROLLER_CONFLICT, which is today's static analogue of exactly this problem. - A declared bounded-resource kind. The escrow class above cannot be applied to an attribute that is merely a number.
capacityhas to be declared in the profile as a bounded resource, with a total, before an allowance can be split against it — which makes this a §D8 attribute-type-system obligation as much as a §D9 one. - An admission wall. The
admitrule above turns participant entry into a boundary crossing, and §D6's wall obligations apply to it in full: what evidence is required, what is recorded, what happens when the checker rejects, and what a partition does while it cannot reach the quota state.
References
- [K14] M. Kivelä, A. Arenas, M. Barthelemy, J. P. Gleeson, Y. Moreno, M. A. Porter. Multilayer networks. Journal of Complex Networks 2(3), 203–271, 2014. — node-layer tuples, aspects, intra- vs inter-layer edges, and the node-aligned special case.
- [B] R. Milner. The Space and Motion of Communicating Agents. Cambridge University Press, 2009. — a bigraph is a place graph (nesting) and a link graph (connectivity) over one node set; links are hyperedges; bigraphical reactive systems reconfigure structure by rules that may create or destroy nodes.
- [G] ISO/IEC 39075:2024, Information technology — Database languages — GQL. — the property graph as a directed, labelled, attributed multigraph in which nodes and edges carry properties.
- [H] G. Gallo, G. Longo, S. Pallottino, S. Nguyen. Directed hypergraphs and applications. Discrete Applied Mathematics 42 (2–3), 177–201, 1993. — a hyperarc as an ordered pair of disjoint vertex sets, a tail and a head; the standard formulation behind endpoint roles.
- [E] V. Balegas, D. Serra, S. Duarte, C. Ferreira, M. Shapiro, R. Rodrigues, N. Preguiça. Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants. SRDS 2015. — the Bounded Counter: escrow splits a numeric allowance across replicas so that decrements commute locally and coordination is needed only when a replica's share is exhausted.
- [SP] SPIFFE, Trust Domain and Bundle and Federation specifications. — a trust domain is a trust root; workloads are admitted by attested policy rules rather than by prior enumeration, and domains interoperate by exchanging bundles.
- [M] Modelica Language Specification, ch. 9, Connectors and Connections. — potential variables equated, flow variables summed to zero, acausal connection semantics.
- [V] IEEE 1164
std_logic_1164:std_ulogic(unresolved) vsstd_logic(resolved, with a resolution function). - [P] Ptolemy II / System Design, Modeling, and Simulation using Ptolemy II, ch. on directors and hierarchical heterogeneity. — a director implements a model of computation; an opaque composite's local director need not match its executive director.
- [R] ROS 2 documentation, About Quality of Service settings. — reliability, durability, history, depth, deadline, lifespan, liveliness as independent policies.
- [S] Eclipse SUMO documentation, Randomness. —
deterministic by default;
--seed/--random. - [O] Google OR-Tools routing issue reports on reproducibility. — solution quality varies run to run; construction order affects results.
End of WRL Core 0.1.2, Parts I and II. Part I is frozen. Part II is an argument, and is meant to be argued with — see Direction for the case in prose.