WallRiderLangWRL CORE 0.1.2 · FROZEN
PART I NORMATIVE · PART II DESIGN DRAFT

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.

This file has two parts, and only one of them is normative. Part I (§0–§17) is the frozen family extract: everything in it is settled, implemented, and covered by a battery. Part II (§D1–§D7), new in 0.1.2, is a design draft — the architectural direction the frozen core is being aimed at. Nothing in Part II is frozen, nothing in it is implemented, and every construct it sketches is rejected by today's toolchain with 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.
Part II is not the language's design draft. The authored Complete Design Draft — forty-six sections covering the expression notation, boundaries, effects, supervision, metaprogramming and the seven worked examples — is a separate and much larger document, published in full at Design draft. That is the primary forward map and the one whose section numbers the tier table cites. Part II below is a later, narrower extrapolation: nine sections on the questions the authored draft does not settle — how many topologies one identity set may carry at once (§D1–§D7), what a connection is when it is not a directed pair (§D8), and what happens when the topology itself changes (§D9). Read the draft first.

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.

Freezing a family means: the set of members is closed, and each member's meaning-role is settled. It does not freeze exact glyphs, argument grammars, sugar, or edge-case rules — those live in the design draft and are Experimental or Proposed until a later frozen revision promotes them.
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:

  1. The period cycle is OBSERVE→ACCEPT→MAP→COMMIT→REACT→FILM, not Collect→Order→Reduce→Commit→Record.
  2. "Conflicts dissolve" is narrowed — single ownership stops generic cell races, but claim and control conflicts need explicit policies.
  3. Canonical ordering is policy-pinned, not a fixed hash formula.
  4. History splits into WorldFrame / EventLedger / BuildFilm.
  5. 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:

  1. Mailbox is partial, not reserved. 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.
  2. * is narrowed. Only the replication-by-position meaning is implemented; wildcard matching remains reserved with no lowering. §4 is corrected.
  3. The document boundary is normative (new §15). A WRL world document and a scenario are different documents. periods and [epoch:N] claims are run inputs and are deliberately outside the SemanticArtifactID. The strict world parser is normative; the combined-document parser is an explicitly-named migration bridge.
  4. 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:

WRL source / canvas canonical semantic graph Forge Semantic IR TRVM facts · films · reductions

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.

KindRole (frozen)
Functioncalculates — pure local computation
Actorpersists — durable identity + owned state
Routecommunicates — a directed, textured transition edge
Wall (boundary)authorizes — gate / commit / seal on propagation
Periodorders — logical-time step under which events settle
Fragmentcarries code — quoted graph as movable data
Stencilconstructs graphs — parameterized fragment producer
Derivetransforms typed graphs
Filmproves what occurred — append-only replayable log
Hashidentifies 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

  1. Shape says what a thing is. Bracket shape identifies the kind of object.
  2. Line texture says how it moves. Route style identifies the operational guarantee.
  3. The colon introduces; the slash bounds. Both families cap at three strokes, ordered by permanence: transient · committed · sealed.
  4. Marks for architecture, words for computation. Process notation owns punctuation; expression notation defaults to words.

§3Container / shape-as-kind familyfrozen · 3 members

ShapeKind (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

SymbolMeaning-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:

MeaningStatus
replication-by-position ([relay:r*3])implemented — surface sugar, bounded, identity-equivalent (§17)
wildcard matchreserved — 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.

TextureGuarantee (frozen)
--x--> soliddeterministic local transition; settles within the period
~~x~~> asyncasynchronous message; appended to a mailbox; observable next period
==x==> verifiedevidence-backed / committed transition under a named policy
!!x!!> faultcrash / 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:

TextureSurface statusSee
-- solidsurface-grounded — declarable, emittable, round-trips§14
~~ asyncpartial — IR/runtime-grounded, not surface-emittable, no structural edge declaration§14b, §16
== verifiedpartial — acceptance machinery exists; the construct does not§16.1
!! faultpartial — fault state exists; route and supervision do not§16

§6Time familyfrozen

Frozen: the period/microstep model and the wall-clock-as-fact discipline. Not frozen: the tick surface syntax details.

§7Memory-kind familyfrozen · 5 members

ShapeMemory kind (frozen)Lifetime rule
(state)volatile cellmutable by replacement; single-owner
{facts}monotonic knowledgegrow-only; merges by lattice union; never retracts
[archive]durable storepersistent addressable actor; survives restart
:: fragment //code memoryquoted graph as portable data
#hashsealed identityimmutable, content-addressed
Frozen invariant: no shared mutable cell. Shared knowledge is {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)

  1. Single-owner cells — no shared mutable cell.
  2. 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

OBSERVEACCEPTMAP COMMITREACTFILM
PhaseDefinition
OBSERVEcanonicalize and insert distinct claims
ACCEPTcreate missing receipts per the pinned acceptance policy
MAPnewly-accepted successful ops → controls
COMMITapply control writes and fault resets to owned cells
REACTdeterministic within-period token cascade to fixpoint; latch current overflow
FILMrecord 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:

ConcernPinned law
recognitiondistinct-CandidateKey count
acceptanceMIN CandidateKey / first receipt
applicationcanonical event order
faultCOMMIT clears, then REACT ORs
capacityatomic 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.

ArtifactFrozen role
WorldFrameauthoritative observable state sufficient for future behavior — the physical world snapshot (Film v0.6 bytes)
EventLedgeraccepted events, receipts, effects, outcomes — the claim-aware layer (Film v0.7 adds this over v0.6)
BuildFilmcompiler / 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.

Explicitly NOT frozen (Proposed in draft): acceptance-receipt settlement across logs, Byzantine and trust modes, sequencer or authority settlement, signatures and reputation. Conflicting receipts are recognized evidence for later settlement, never blindly merged.

§10Boundary (wall) familyfrozen · gate / commit / seal

BoundaryReduction effect (frozen role)
/gaterequire capability; emit an effect-request node (no ambient I/O)
//commitcanonicalize the fragment-so-far; assign a content id; freeze it
///sealcommit, 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

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.

FamilyGuarantee
Formatting invariancepresentation never changes the hash
Replay exactnessa live run and its replay share all observables
Build invariancesame inputs → identical film + hashes across hosts
Merge lawsfact union is commutative / associative / idempotent
Boundary safetyundeclared effects cannot cross walls
Scheduler invarianceworker ordering never changes the committed film
World round-trip 0.1.2a format→parse cycle preserves the complete canonical world projection, defined by exclusion (§15)
Identity round-trip 0.1.2a format→parse cycle preserves the canonical artifact bytes and the SemanticArtifactID
Document separation 0.1.2a world document excludes run inputs both semantically and lexically (§15)

§13What Core 0.1 does NOT freeze

Deferred to later frozen revisions:

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

FamilyTRVM grounding
The five surface-grounded world roles (Pulser / Relay / Door / Spinner / Orb) — Mailbox is not a sixthfixture node kinds; lowered by the compiler
State + wiringstate schema (counters, wires, rotor-as-state, pose, sticky fault); sig-wire + socket edges
Deterministic signal route (--)sig-wire delivery; within-period REACT fixpoint
Periodsepoch-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

FamilyStatus
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 behaviorsreserved — 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:

TermMeans
IR/runtime-groundeda canonical IR declaration exists, the runtime executes it, and it is covered by a battery
surface-groundedadditionally, 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.
The edge-declaration point is the one that constrains the next slice. An async route is not an ordinary structural edge — it does not settle within the period, so it cannot participate in the within-period REACT fixpoint the way -- 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.

DocumentCarriesEnters SemanticArtifactID?
World documentprofile, objects, edges, static config, wiringyes
ScenarioV1periods, [epoch:N] claims, run inputsno

Frozen consequences

  1. Run inputs are not world content. periods and [epoch:N] claims are run inputs. Changing them does not move the SemanticArtifactID; the same world can be run by many scenarios.
  2. 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 0 would satisfy the semantic half while still writing scenario syntax into a world document.
  3. 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 pointAcceptsStatus
parse_wrl_core(text)world source only; rejects run inputs with a typed WRL_WORLD_SOURCE_HAS_SCENARIOnormative
parse_wrl_legacy_document(text)a pre-boundary combined documentexplicit 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.

Open question (flagged, not decided). The Phase-3C presentation surface 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.

#StepGate it must pass
1L-0 closurethe round-trip and document-boundary laws restated and green — complete
2~~ async routeneeds a canonical logical route declaration distinct from the structural edge declaration (§14b)
3== verified routesee §16.1
4# references and & composition
5//commit and ///seal
6!! fault routemay only follow a minimal supervision floor actually existing
7/gate capability gatelast

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:

== 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

LayerCarries
World documentdeclares a verified channel and its schema
ScenarioV1carries the claim instances
Receiptsprove 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:

Two required properties, gated by battery

  1. Bounded expansion. A one-to-many form must reject an absurd count with a typed diagnostic before allocating the expansion. sp*1000000000 is a diagnostic, not an out-of-memory event.
  2. 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.

Nothing in Part II is normative, implemented, or promised. Every world sketched below is currently rejected by the toolchain, and the site's conformance suite asserts that rejection on every run — each sketch also names the promotion capabilities it is waiting on. Marking one of those capabilities shipped in the registry makes the suite fail every block still waiting on it, by file and block number, and this section must then be rewritten before the build is green again. The sketches do not begin sealing by themselves — most are fragments with no 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.
And this is not the whole forward map. The authored Complete Design Draft already specifies most of what WRL intends to become — §10–§17 the expression notation, §18–§27 the process semantics, §28–§34 metaprogramming, §35–§38 programs at scale, §39–§46 the reference. All of it is on the Design draft page, marked with the author's own Core / Experimental / Proposed tiers. Part II here takes up three things that document leaves open: whether a world's identities may carry several distinct topologies at once (§D1–§D7), what a connection is once it stops being a directed pair (§D8), and what it means for the topology itself to change while the world runs (§D9). If you want the language's designed feature set, read the draft; if you want these architectural arguments, read on.
How the draft rules are numbered. A rule keeps the number it was given when it was written, and a sub-rule is numbered in the order it was added to its section, not the order it ends up reading in. So §D8 presents D8.1, then D8.4, then D8.2, D8.3 and D8.5 — because D8.4 answers a question raised by D8.1's first paragraph and belongs beside it. Renumbering for tidiness would silently break every citation of a published rule, including the reviews that produced several of them, so the numbers are treated as identities and the index at the left is the ordered view. This is the same distinction §D8.1 makes about relations, applied to the document that argues it.

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:

ArrangementAn entity in n layers isConsequence
Replica-based (general case)n node-layer tuples, tied together by explicit inter-layer coupling edgesidentity is a property of the coupling; the couplings can be wrong, missing, or asymmetric
Node-aligned proposed for WRLone node, present in every layer by constructionidentity 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.

This is not a new idea in the language; it is a consequence of §4. Identity is already a first-class construct kind whose whole purpose is that the same name means the same thing everywhere in the document. D1 says only: do not weaken that when layers arrive. The temptation will be to model a drone's radio as a separate node so it can have its own connections. Resist it — a radio that is not the drone is a different identity, and then the question "where is the radio" has two answers.

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:

PrecedentRuleWhy 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 kindAdmitsMeaning of a connectionStatus
Ownedat most one writerthe writer's value becomes the reader'sfrozen — this is today's rule, unchanged (§0)
Resolvedmany drivers plus a named resolution functionall drivers contribute; the function determines the valuedraft
Acausalmany participants, no driverspotentials equal, flows sum to zero across the connectiondraft
The resolution function must be part of the sealed artifact. A resolved port whose resolution function is supplied by the runtime, or chosen by the host, breaks §11: two worlds with identical canonical bytes would then be able to disagree about a value. If a resolution function cannot be named in the document and hashed with it, resolved ports do not ship.

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.

ContractInputRequired propertiesWhat it buys
Canonical-map resolverone map, source terminal → drive value, in §11's existing terminal orderpure, total, deterministic, plus a declared value for the empty mapthe 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 resolverthe same map, folded in any order, partitioned, or resolved incrementallyassociative, commutative, total, plus a declared value for the empty mapan 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 resolvervalues arriving in unknown order and unknown multiplicityassociative, commutative, idempotent, with an identityredelivery 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.

Draft rule D2.1 — three resolver contracts. A resolution is one of the three above, and a profile declares which. Only the join resolver owes §9's three laws; a canonical-map resolver owes purity, totality and determinism and nothing else, and a parallel-fold resolver buys an implementation freedom by paying associativity and commutativity for it. All three owe one thing in common, and it is the one most often left out: every resolution table must answer the empty driver set. A binary operation does not define the value of a terminal with no enabled drivers, and this is not an edge case — it is a floating bus, which is the normal state of a tri-state line between transactions. 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.
Nothing already true stops being true. 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:

AxisRange
Domainspatial, informational, authority, energy, economic, evidential …
Directionalitydirected · bidirectional · acausal
Cardinality1:1 · 1:n · n:1 · n:m
Timingsettles this period · settles later · continuous
Capacity & costunbounded · metered · priced
Reliabilityguaranteed · lossy · adversarial
Geometryabstract · metric · embedded in a coordinate frame
Schemauntyped · 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.

The corollary is that attributed relations must ship before any domain profile does. A mobility profile without them would have to encode capacity and cost in identifiers, which is exactly the class of mistake §15 exists to prevent.

§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.

This is the reason Part II proposes no new construct kind. The ten kinds of §1 already include identity, relation, boundary, and history. Multilayer topology is relations with attributes; heterogeneous execution is boundaries with profiles. A language that needed an eleventh kind for every domain it modelled would not be a topology language — it would be a collection of DSLs sharing a parser.

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:

EngineDeterminism
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.

A sealed plan may record feasible. It may record an objective value. It may never record optimal. Optimality is a claim about the search, and by construction the search is not sealed. A film that asserted optimality would be asserting something no replay could check — which is precisely the class of claim §12's replay-exactness law exists to exclude.

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.

The ordering hazard. A domain profile is the last thing to build, not the first. It depends on typed ports (§D2), ~~ (§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.

Draft rule D6.1 — the profile bootstrap kernel. Step 3 delivers 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.

PropertyFinite tableUser-written function
Canonical bytesthe sorted entries — §11 already hashes exactly this shapeneeds a canonical form for syntax, which is a new and contestable artifact
Totalitychecked by counting: |T|n entries presentundecidable in general; requires a totality checker before any profile can be trusted
Terminationnothing executesrequires the language to be restricted anyway, at which point the restriction is the design
Associativity, commutativityverified exhaustively over the typeproved, assumed, or checked by sampling — and §D8's one-pass settlement depends on it
Reviewable by a domain expertyes — it is the table already in the standardonly 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:

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.

What this does to the ladder, and to a falsification condition. Step 3 is now deliverable at step 3: nothing in 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 4resolution 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:

This table is the normative statement of the conditions. Direction expands five of the eight at argument length and is a non-normative summary of them; where the two differ, this table governs. Each row carries a stable 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.
#ConditionWhat it would falsify
1Real 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
2The 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
3Profiles 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
4Resolved-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
5Per-profile boundary safety across walls proves undecidable in practice§D4. Heterogeneous execution would have to be dropped, leaving acausal connections nowhere to live
6A 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
7Admission 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
8The 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 6, 7 and 8 were added with §D8 and §D9, which are drafted below and which each state their own remaining obligations in place. Conditions 7 and 8 replaced an earlier, weaker entry which said that unforeseen participants might put sealed identity and open topology in genuine conflict. They do not: §D9 seals the admission rule rather than the roster. What is genuinely uncertain is narrower and is stated above — whether admission can be decided from evidence, and whether bounded resources can be divided in advance. Every entry in this table is a way for the argument to be wrong that its author can recognise; the list is not claimed to be complete.

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 byWhat it requiresWhy Edge cannot carry it
§D3 attributescapacity, cost, units, geometry on the connection itselfEdge has payload (what crossed) and policy (how it crosses), but no field for what the connection is
§D2 resolved portsmany drivers meeting at one terminal and settling by a named functionn 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 connectionsthree or more participants, no source and no destination, potentials equal and flows summing to zeroa source/destination pair asserts a direction that physically does not exist
§D1 multilayeran occupancy relation between one drone and one segment that means something different from a messagelabel is a name, not a typed domain

The three ways out, and the one taken

OptionVerdict
ABroaden the entity. Rename Edge to Relation and generalise its shape. A directed binary route becomes the simplest specialisation of it.taken
BAdd 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
CEncode 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 }
This block used to spell the allocation out as { 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.

FieldRangeReplaces
domaina profile-declared namespace — signal, digital, mobility, electrical (§D6)— (new)
kindthe predicate within that domain — SignalWire, drive, occupieslabel, promoted from a bare name to a domain-scoped kind
endpointsan unordered set of Endpoint, size ≥ 2source, destination
orientationdirected · symmetric · acausal— (was implicit and always directed)
texturethe four frozen textures (§5). Required for directed; profile-defined for symmetric; absent for acausal, where a solver wall governs settlement instead (§D4)texture
attributestyped, unit-bearing values validated by the profile— (new; this is §D3's requirement)
policyunchangedpolicy
(no provenance)removed from the value and carried by the ledger event instead — §D8.3provenance

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.

RoleAdmitted byMeaning
sourcedirecteda member of the tail: it writes
targetdirecteda member of the head: it reads
peersymmetricneither side distinguished; the profile says what the pairing obliges
terminalacausala 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:

RuleAdmitsVerdict
once per role{(x, source), (x, target)}the same terminal is simultaneously in the tail and the head
once per relationrejectedcorrect

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:

Normative. A terminal appears at most once in a relation, regardless of role. For a directed relation this makes the 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 objecta.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.

Draft rule D8.7 — canonical relation revision. A RelationRevision has exactly one canonical form, and a conforming implementation reaches it by:
  1. 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 spelling p0.sig_out used 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.
  2. Semantic roles. Every endpoint's role is one of source, target, peer, terminal. A port name is never a role.
  3. 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.
  4. Role completeness. A directed relation carries at least one source and at least one target. A hyperarc is an ordered pair of vertex sets; neither may be empty.
  5. Terminal uniqueness. A terminal appears at most once per relation, regardless of role — the normative rule stated above.
  6. 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.
  7. A texture on directed. The other half of the same row, and it is enforced on the same table. A directed relation 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.
  8. 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.
  9. Absent optionals absent. A missing texture is a missing key, never a null.
A reverse projection to a V1 edge locates its endpoints by role, never by array position, and validates each terminal's 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.

FailureCaseWhat breaks
Distinct relations collidetwo 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 kindcontent-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 nameoperation 5, replace relation attributes — a road segment's capacity drops from 3 to 2 after a lane closurethe 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.

Draft rule D8.1 — relation identity. A relation has a stable 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)
Three variants, not one overloaded record. The first version of this was a single { 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.
The period-0 case was itself a hidden union, and this is the second time that defect has been found in the same record. It was one variant, 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.
V1 has no writable relation names, and that is the ruling, not a limitation to be worked around. Identity for a relation already present at period 0 is derived from the frozen edge key (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.
"Nothing may emit it" is a rule about writers, and it is not a rule about the hash. These are three different authorities and the implementation keeps them in three places:
  1. Which variants exist. All three. The equations above are total: given a well-formed allocation of any variant, relation_id is defined, and it is defined the same way for everyone.
  2. Which a trusted importer may construct. Only legacy-edge, because that is the only preimage a frozen artifact supplies every field of.
  3. Which an authoring surface may emit. None, today — there is no syntax for a relation name and no machinery for a grant.
Folding these together — refusing to compute an id for a variant nobody may yet write — was the first implementation's mistake, and it is worse than it looks. It makes identity depend on who is asking, which is the property a content address exists to not have; and it silently marks two facts untestable that are not. That the same name in two worlds yields two ids, and that the same counter under two grants yields two ids, are statements about these equations. Both are decidable today, and both are now checked. What is genuinely still missing is the surface and the runtime — which is a smaller claim, and the true one.
‖ is a framing, not a concatenation. Three fields joined by an unframed separator are ambiguous the moment any of them is variable-length: 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
One event carrying either variant would have been an overload wearing a type. A period-0 relation is not attached by anybody; it is present because the world was sealed with it, and there is no proposer, no grant and nothing to accept. Letting 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.
RelationVariantWhere uniqueness comes from
Present at period 0 — named in the sealed worldNamedInitialAllocationthe 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 sealLegacyEdgeAllocationthe 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 3GrantedAllocationa 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.

The first version of that scope repeated the mistake D8.3 had just corrected. It read 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.
The record was called an 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.

Draft rule D8.4 — non-forking allocation. A relation allocation is drawn from a grant: a content-addressed value carrying only what governs future allocation.

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:

ConcernD8.4's job?What settles it
Identity uniqueness — two allocations never yield one relation_idYes — this is the whole rulegrant_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 roleNo, and this is the correctionNot 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 outYes, but as liveness, not safetyExhausting counter_range stops allocation; it never breaks an identity already minted. The remedy is another grant, which is another disjoint space
Revocation and expiryYes, and it is not fencingvalidity 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.

That is an argument about consequences, and it leaves 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 validityReplayable?
a range of logical periodsYes — the period is the ledger's own ordering, so a replayer already knows it
a clock observation admitted as an eventYes — the observation is in the ledger, so the replayer reads the same value the original run did
an authority-issued expiration eventYes — expiry becomes a fact with a position in the ledger rather than a threshold anyone must evaluate
another basis pinned by the profileYes, 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 timeNo — 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.

D8.4 and D8.6 split the same way, and that is not a coincidence. Each has a world law a replayer can establish from the ledger — one pair, one intent here; first observation binds there — and each has a local obligation nothing outside the participant can observe: do not restore a grant, do not emit before the reservation is durable. Both local obligations exist for one purpose, which is to stop an honest participant from generating a true diagnostic about itself. Where a rule has both halves, saying which is which is most of the work; the earlier versions of both rules stated the unverifiable half and let it stand for the whole.
Three of the "equivalent sources" were not equivalent. An earlier sentence here offered a replica identity plus a restart incarnation, a deterministic spawn path, or a preallocated namespace range as interchangeable ways to satisfy D8.4. Only the last is inherently non-overlapping. A restart incarnation is safe only if incarnation numbers cannot themselves fork under stale restoration — which is the original failure, one level down. A deterministic spawn path is safe only if the parent's spawn operation is itself serialised or deduplicated; otherwise two live parents compute the same path. So the list is replaced by the obligation it was standing in for: these may serve as allocation sources only where the system separately proves that no two live allocators can hold the same source identity. The rule is the guarantee; an example does not inherit it by being named alongside one that has it.

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.

FactHome
what range exists, and until whenAllocationGrant
which policy governs allocations from itAllocationGrant — it constrains future behaviour
who proposed the grant, and whyGrantIssuedprovenance
evidence offered for the grantGrantIssuedoffered_evidence
why the checker accepted itthe §8 acceptance receipt
which grant minted a given relationGrantedAllocation.grant_id
that no two intents shared one drawn namea 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:

The nonce law. A nonce must never depend on ambient, unrecorded randomness or wall-clock state. It must be reproducible from sealed state or from prior recorded observations.

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.

Draft rule D8.6 — an allocation burns on first observation. Once an allocation appears in any observed proposal it is permanently bound to that proposal's 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.
SituationOutcome
same attach retried at a later boundarypermitted — 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 attachedpermitted — same birth key and schedule, a different proposal envelope. An audit trail is not a lifecycle event
same allocation, a different initial revisionallocation collision — a different birth key, so a diagnostic, never a race
attach succeeds, then the same birth appears againrefused — not as a collision but because the relation already exists. Same birth, already spent
proposal rejectedallocation stays consumed. A refusal is an outcome, not an erasure
proposal times out or the proposer crashesallocation stays consumed, for the same reason
relation later retiredallocation stays consumed — retirement is not deletion (historical representation)
a retired relation is later reactivateda 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:

HalfThe ruleWhat 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 collisionReplay, 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 durableNothing 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
This record was first written as { 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.
The next free counter is derived, not stored. A grant's next available 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:

ProfileThe reservation isFirst observation isReplay can checkPrice
Local — the defaulta proposer-private durable recordthe proposalD8.6-aone local flush before emit
Publisheditself an observed ledger eventthe reservationD8.6-a and the ordering — a proposal whose allocation has no earlier reservation entry is refuseda round trip before every attach, and a name spent whether or not the attach is ever proposed
The published profile is not simply the better one, and saying so is the point of listing the price. It burns an allocation on intent rather than on proposal, so a proposer that reserves and then thinks better of the attach has spent a name for nothing. Where allocations are drawn from a bounded grant range (§D8.4) that is a real cost paid in a scarce resource, which is why the local profile is the default and not a weaker fallback. A rule that cannot be verified is not automatically a rule that should be made verifiable at any price.
This is nonce reuse, in the cryptographic sense. The prohibition is the same one and it fails the same way: a nonce that is reused under a second message does not merely weaken a property, it makes two different things indistinguishable under one name. The parallel is worth naming because it supplies the right intuition for the awkward cases — a rejected proposal, a crashed proposer — where "it was never really used" is a tempting and wrong argument in both settings.

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:

EntityStable identityReplaceable value
Actorthe object id — durable across every config change, which is what "durable identity" means in §1role config, and under draft §26 the behaviour
Relationrelation_id (this rule — previously missing)RelationRevision
Boundarythe wall idits open/closed state, changed by operation 7
Worldthe sem- id, which §D9 keeps fixed across every topology changethe 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:

Draft rule D8.2 — one home for lifecycle. A 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:

A revision id now describes a value, not a path to it. With a predecessor in the preimage, a capacity of 3 reduced to 2 and a capacity of 1 raised to 2 hash differently despite being the same relation in the same state — so two worlds that have genuinely converged cannot be recognised as having converged, and a diff between them reports a difference that is not there. Without it, they share one revision id. History is not lost: it is in the ledger and in the film, which is where §10 already says the record of what occurred belongs. The revision hash goes back to answering the question a content address should answer — what is this? — rather than how did it get here?

…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.

Draft rule D8.3 — provenance is not part of a value. 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.
InformationHomeHashed into
what the relation currently meansRelationRevisionthe revision id
how the revision came to be proposedthe ledger eventthe event id
evidence the proposer supplied to justify itthe ledger event, as an operation argumentthe event id
evidence the checker produced in decidingthe acceptance receipt (§8)the receipt
how the relation evolved over timethe ledger sequence— (an ordering, not a value)
what a replay actually observedthe 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.

This section used to answer it by restating the preimage, and the restatement went stale. It printed 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.
VariantHow world_id reaches the id
NamedInitialAllocationDirectly. It is a field of the allocation and a term of the hash, visible in one line
LegacyEdgeAllocationDirectly, 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
GrantedAllocationTransitively, 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":

ChangeDoes world_id move?Do relation ids survive?
Attach, revise or retire a relation at runtime — §D9 operations 3–5No. §D9's central move is that structure changes while identity does notYes. This is the whole point of D8.1
Re-author the source and re-sealYes — a different artifact is a different sem- id, by constructionNo, 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.

Draft rule D8.5 — relation identity is world-scoped. A 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 scopethe sealed worlda longer-lived namespace — an asset registry, a fleet, an organisation
Verifiable againstthe artifact alonethe artifact and an external authority
Survives re-sealingonly through a migration mapyes, by construction
Failure modereferences break loudly at upgradetwo 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:

The projection's unstated precondition, now stated. The V1 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:

FieldValue for a frozen route
domainsignal
kindSignalWire 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
orientationdirected
attributesempty
derived cardinality1:1 — computed, not written
The canonical-form obligation, which is the whole cost. §11 says a world's meaning is the hash of its canonical bytes. If broadening the entity changes how a plain --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 artifactGeneral recordWhy 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:

Normative — the ForgeSemanticArtifactV1 edge projection. When a relation has 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

PrecedentWhat 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.

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.

FieldsEffect on relation_idWhat that means
kind, endpointsmoves itthese two are the name in V1
policyfreemay change without touching identity — this is §D8.1's live content
domain, orientation, texture, attributesV1-fixedV1 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:

FieldWhat V1 fixes it to
domainsignal
orientationdirected
texturesolid
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.

What is deliberately still pending. Nothing in the kernel mints a grant, schedules a transition, or survives a crash. §D8.4 and §D8.6 stay awaiting in the register below, at the 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.

Draft rule D8.8 — the V1 import contract. The V1 artifact family is 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:
  1. 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 derived policy, it is then sealed into rev- 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 moving rel-; it does not say any rulepack names a family this adapter can read.
  2. 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.
  3. 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.
  4. Mints one allocation variant. LegacyEdgeAllocation only. NamedInitialAllocation and 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.
  5. 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.
  6. Moves no sem-. Checked as byte equality against sealWorld, for every fixture, before anything else in the suite runs.
  7. 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 fieldV1 sourceUnder V2
domainresolved — the namespace the profile declares: signalinherited or authored
kindread — edge.kindauthored
endpointsread — edge.src and edge.dst, each qualified by the kind's frozen port pairauthored terminals, general arity
policyread — semantic_policies.rulepack_idinherited
orientationsupplied — always directedauthored, one of three
texturesupplied — always solid, which V1 elides rather than lacksauthored; required for directed
attributessupplied — always emptyauthored, 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.

What must not be done. Preserving relation ids across the break by keeping the V1 world id in the V2 preimage. It buys continuity at the cost of both laws that make the id mean anything: the relation's name would no longer be derivable from the world it is in, and the fact that this world was imported from that one — pure provenance — would be sitting inside a value (§D8.3).

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.

Both properties run, and did not have to wait for V2. They were first registered as awaiting it, which was one step too cautious. The break V2 causes — every 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.
The derived pairing is not the ledger fact, and the first implementation returned it as if it were. Its output field was called 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:

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-fixeddomain, 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.

Draft rule D8.9 — the V2 relation encoding. ir_version "2.0" encodes topology as relations, and:
  1. relations replaces edges. They do not coexist. edges in a V2 artifact is WRL_LEGACY_EDGES_IN_V2; relations in a V1 artifact is WRL_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.
  2. A relation is { identity_seed, revision }, exactly. world_id, relation_id and revision_id are never stored.
  3. 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.
  4. Initial bytes admit two variants. named-initial, the native one, and legacy-edge, which is import-only: it records that a relation crossed the V1 migration without ever being named — its rel- 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. granted is 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.
  5. A missing name is an error. WRL_MISSING_RELATION_NAME, never a fall back to a legacy-edge seed 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.
  6. relations sorts 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.
  7. 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.
Admission is the whole source tuple, as it is for V1: an artifact's (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.

Draft rule D8.10 — deriving V2 identity.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. A rel- is world-scoped and a rev- is not. Editing one relation moves every relation's rel-, because the world id moved and the world id is in every allocation; only the edited relation's rev- 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.
  6. 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.

Two encodings, two sort keys, and one wrong conclusion drawn from that twice. The round-trip check was first written as “a downgrade returns the original bytes”, and it failed. It was weakened to a multiset comparison under a first explanation — V1 leaves its edge order to whoever typed the world, and V2 tidies it up — which is simply false: sealing a V1 source with its lines reversed produces the same 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.

Draft rule D8.11 — the V1 → V2 migration.
  1. An import mints only unnamed seeds. legacy-edge, never named-initial — a V1 artifact has no field a name could come from. The authority is the kernel's IMPORTABLE_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.
  2. Only the encoding moves. Every key the migration does not own passes through byte-identically. A migration that also tidied objects would 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.
  3. Structure survives; identity does not. Every rev- recurs and no rel- does. A migration reporting a preserved rel- would be claiming the thing §D8.5 exists to deny.
  4. The claim is checked by the checker that already exists. The cross-family correspondence has the same shape as the V1 one, so candidateImportedFacts and checkRelationImported verify 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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 canonicalizeGraph rather 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 the sem-.

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.

The re-attach is the only hard part, and line order is the wrong way to do it. Re-attaching a name by line index is wrong twice over. Sugar expands one authored line into several emitted ones, and 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.

Why there is no 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.
Draft rule D8.12 — the named-relation surface.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. The pairing follows provenance, never order. The note above.
  6. 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.
  7. Renaming moves identity and nothing else. Every rev- recurs, because §D8.3 makes a revision standalone and a name is not in it; every rel- moves, and so does the world. A rename that moved a rev- would mean the name had leaked into the revision; one that moved no rel- would mean the name is not the preimage.
  8. An author cannot write a legacy seed. The surface mints named-initial only. legacy-edge records 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.

One world this surface cannot write — and one that was filed under the same heading by mistake.

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.

Draft rule D8.13 — writing a V2 world back out.
  1. 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.
  2. 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.
  3. 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.
  4. Formatting is normalisation, never a gate. An unformatted world seals to the same id as a formatted one, in both encodings.
  5. 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.
  6. 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.
  7. 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.

Draft rule D8.14 — the V2 world gate.
  1. 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.
  2. 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.
  3. Derived fields are checked, not accepted. Every key the world derives — semantic_policies, schemas, and each object's ports and state_schema_ref — must equal what the derivation produces; a disagreement is WRL_V2_WORLD_MISMATCH. An artifact may not state a fact its own content contradicts. ir_version is the exception, and the reason is that it is the coordinate that moved.
  4. 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.

Draft rule D8.15 — the encoding declaration.
  1. A V2 source declares its encoding. ir 2.0, as the second non-comment declaration, on the line after profile. A source that does not is not a V2 source: WRL_MISSING_IR_HEADER.
  2. One declaration, in one place, in one spelling. Declared twice is WRL_DUPLICATE_IR_HEADER; declared after anything but the profile is WRL_MISSING_IR_HEADER; anything other than exactly one version token is WRL_MALFORMED_IR_HEADER; a version outside the V2 family is WRL_UNSUPPORTED_IR_VERSION.
  3. The surface emits what it reads. formatNamedWorld writes the declaration and parseNamedWorld reads it back, so the round trip is a fixed point over the header too, and the artifact's ir_version is the one the source declared rather than the one the encoder assumed.
  4. 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).

Draft rule D8.16 — adopting a migrated relation.
  1. 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.
  2. 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 is WRL_UNKNOWN_RELATION; selecting one twice is WRL_DUPLICATE_ADOPTION. A relation that already has a name is not adoptable — replacing it would be a rename.
  3. Every adopted seed becomes named-initial, and every rev- is unchanged. Adoption changes what a relation is called, not what it connects.
  4. The world sem- moves, and every rel- with it — including the relations that were not adopted. A correspondence pairing the two worlds on rev- is returned, and it reports identityPreserved: false rather than letting a caller assume otherwise.
  5. Adoption is exhaustive, or it is refused. The assignment must carry exactly one name for every legacy-edge relation in the world. A leftover is WRL_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.
  6. 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 every rel- 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.
Why partial adoption is refused rather than allowed. An earlier form of clause 5 read adoption may be partial, and a partly adopted world is still unwritable. Every word of that was true, it had a green check under it, and it was the wrong behaviour to pin — which is a distinction a register cannot make on its own, because a passing check never asks whether the thing it pins should exist.

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 badlyir 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.

Draft rule D8.17 — admitting a world source.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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). The sem- 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.
  6. 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.

Draft rule D8.18 — the runtime projection. A projection of a sealed world is { 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.
  1. semantic_world_id is 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.
  2. execution_view_id identifies projected bytes and nothing else. It is the id of execution_artifact under 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.
  3. 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.
  4. 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.
  5. execution_view_id is 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". Every relation_id in a projection is minted under semantic_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.
  6. Every binding is independently recomputable. A consumer recomputes binding i from semantic_artifact alone — 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.
  7. 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.

Draft rule D8.19 — the projection on the wire.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

Draft rule D8.20 — the profile table.
  1. A world's profile_id selects a row of a frozen table keyed by profile id. An id with no row is refused as WRL_UNSUPPORTED_PROFILE before any byte is hashed; the refusal names the rows that exist and is the only place the list of them is read.
  2. 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 to graphToIr. 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.
  3. 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, no schemas, no state_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 as WRL_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.
  4. The world gate (§D8.14) holds every relation to its profile. Under any row, revision.policy must 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.
  5. 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.
  6. The kernel mints. A static world's sem- is the hash of its canonical bytes under §D8.10, and every rel- and rev- is the kernel's allocation and revision id over that sem-. 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.
  7. forge.world.core.v1 remains 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

  1. A canonical form that leaves every existing sem- id where it is (the note above).
  2. 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. NamedInitialAllocation now 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's AUTHORABLE_VARIANTS stays [] 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_VARIANTS makes a claim about every encoding, and once V2 shipped a surface that emits named-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_VARIANTS is [] — permanently, because V1 has no field a name could live in — and V2_AUTHORABLE_SEED_VARIANTS is 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.
  3. An attribute type system. capacity=3 is meaningless without a unit and a range. This is the same requirement as typed ports (§D2), and neither should ship without the other.
  4. 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 source endpoints reaching it is still an error, and must still be caught before the seal. A terminal declaring resolved(f) is the one exemption, and the exemption is visible on the terminal.
  5. A well-formedness rule per orientation. Roles and orientation can contradict each other and the checker must say so: directed requires at least one source and at least one target and admits no other role; symmetric admits only peer; acausal admits only terminal and carries no texture. Each violation needs its own code.
    These rules now run — §D8.7, with WRL_ENDPOINT_ROLE_ILLEGAL, WRL_INCOMPLETE_ORIENTATION and WRL_ACAUSAL_TEXTURE as the three codes. What remains of this item is the other half of the texture row: texture is required for directed, and no V1 artifact can supply one (§D8.8). It becomes enforceable when the artifact version moves, and not before.
  6. A settlement rule per orientation. directed settles 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 a parallel_fold opt-in). acausal does not settle by passing values at all and requires a solver wall (§D4). symmetric has no settlement rule yet, which is the honest state of it.
  7. 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.

The identity question comes first, because everything else depends on the answer. If a world's topology can change at runtime, what does its 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 configuration boundary. A topology operation accepted during period n affects the active topology used by period n+1. Nothing takes effect mid-period, and there are no exceptions to qualify.

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.

#OperationChangesLedger fact
1create identityadds an object with a role, config and portsIdentityCreated
2retire identitymarks an object non-participating; its history stays addressableIdentityRetired
3attach relationadds a relation over existing terminals (§D8), minting its relation_id from a claimant-owned granted allocationRelationAttached { allocation, revision_id, … }
4detach relationends a relation's participation; the id stays addressableRelationRetired { relation_id, expected_prior_revision }
5replace relation attributeschanges capacity, cost, latency … without changing endpoints or the relation_id (§D8.1)RelationRevised { relation_id, expected_prior_revision, next_revision }
6transfer ownershipmoves an owned terminal's single writer from one identity to another, atomically — no period has zero writers or twoOwnershipTransferred
7open / close boundaryadmits or refuses crossings at a wallBoundaryStateChanged
8expire leasethe timed form of 2, 4 or 7, scheduled at attach timethe corresponding fact, with the deadline as its cause
Operations 3–5 depend on D8.1 and cannot be specified without it. An earlier version of this table wrote the three relation facts as bare verbs — 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:

ObligationThe general rule
Authorisationwhich 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 identitya 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 semanticsthe 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 ruleswhat 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 representationa 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 consequencesthe 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 behaviourwhich 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 behaviourwhen 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.

Classification precedes acceptance. For one 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)
The first version of this rule was a single 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.
Draft rule D9.2 — a transition, its schedule and its proposal are three identities. The 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.
LayerAnswersCarries a period?Consulted by
TransitionIntentKeywhat change is proposedNo, by constructionretry recognition; §D8.6's burn
ScheduledTransitionfor which boundary it is proposedYes — that is its entire content beyond the intentconflict classification, which is boundary-scoped
event_id over ProposalEventwhich envelope was observedinherits the schedule'sledger 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:

A relation birth is single-use. Later observations of the same 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.

Draft rule D9.4 — the schedule may say when, never what. An 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 boundaryOutcome
same ScheduledTransition — attach, revise or retire, whatever the envelopesdeduplicate — 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 TransitionIntentKeyallocation 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_revisioncompeting-revision conflict — the case above
different expected_prior_revisionsstale / branched-revision conflict
revision and retirement from the same current revisionlifecycle conflict — the relation cannot both continue and end
attach after a prior retirementreject, 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.

The conservative first version. At most one distinct lifecycle transition per relation may be accepted for one configuration boundary. Everything else in the table is a diagnostic. That is more restrictive than the eventual rule needs to be — commuting revisions on disjoint attributes could safely merge — but it is deterministic, it is stateable today, and it can be relaxed later without invalidating a film. A rule that starts permissive cannot be tightened without breaking replays that already happened.

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.

Draft rule D9.1 — replay closure. Every content id referenced transitively by the initial artifact, the accepted ledger events, the frames, the policies, the receipts, the admission decisions, the allocations or the topology revisions must resolve — either inside the replay package, or through a content store whose identity and retrieval contract are pinned by the package. A package containing an unresolved referenced hash is not a smaller package; it is an incomplete one, and it must fail as such rather than replay until it reaches the missing byte.

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.1What 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 frozennothing 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 objecta 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 contractreplay 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 closedAnd they are different bytes
each referencing event embeds its own copy of a revisiona revision referenced five times appears five times
one shared copy is emitted up frontonce, in some order
duplicates are kept but ordered differentlysame set, different sequence
some objects are embedded, others left as bare referencesa 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 objectsWhat it removes
1each referenced content id appears exactly oncecopy-per-reference, and the question of which copy is authoritative
2keys are sorted by raw content-id bytesemission order as a degree of freedom
3the bytes stored under a key must hash to that keya table that lies, and makes it checkable rather than trusted
4every transitively required id appearsD9.1's own requirement, now a property of a named structure
5unreferenced objects are excluded by canonicalization, or rejecteda bundle that carries private payload and still hashes as conforming
6entries reference objects only by idthe embedded/referenced mixture in the fourth row above
7closure traversal is deterministic and finitean 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.

The paragraph that stood here asserted both sides of a contradiction, in the commit that created the contradiction. It said an external-store package "publishes the same 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:

ModelFilm identity coversWhat follows
Manifest film — a manifest of content ids, shipped beside a separate object packthe ids alonefilms 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 filmthe full closed bytesfilms 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.

Draft rule D9.3 — one canonical film, and transport is not identity. The canonical 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:

ArtifactWhat it containsReplays alone?
Canonical filmthe complete canonical object closureYes
Compressed filma losslessly compressed canonical filmYes, after decompression — the bytes are recovered exactly
Transport packa film minus objects held remotely or sharedNo, until rehydrated
Manifestids and retrieval informationNo, 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 needsThe film does not need
the canonical requestthe provider's model weights
the adapter and policy identitiesits inference cluster or operating system
the observed responsethe physical network packets
the acceptance and effect receiptsany state private to the far side
any state WRL used after the responsea 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:

LevelThe promiseRequired of
Replay-completereconstruct every WRL state transition from recorded observations and outcomesevery conforming film
Re-execution-completerecompute designated derivations from their implementations and inputsparticular profiles, for the derivations they designate
Draft rule D9.5 — closure stops at the effect wall, and two levels say where. Every conforming film is replay-complete: its closure contains the request, the adapter and policy identities, the observed response, the receipts, and everything WRL read afterwards — but not the internals of whatever produced the response. A profile may additionally declare designated derivations re-execution-complete, which pulls their implementations and inputs into the closure and is the only way to claim independent recomputation. A film must not be described as re-execution-complete for a derivation whose implementation it does not carry.
Why this keeps D9.3 strong rather than qualifying it away. The two levels are not two grades of closure — both are fully closed, and neither permits a dangling reference. They differ in what was promised about the far side of a wall. A film that consulted a model is complete with respect to every decision WRL made, and says nothing about whether the model would answer the same way twice; a re-execution-complete derivation says exactly that and carries the code to prove it. Without the distinction, D9.3 forces hundreds of gigabytes of weights into every film that ever consulted a model behind a gate, which is how a correct rule gets abandoned rather than followed.

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
The seventh name was found by a check, and it was not a miscount. This paragraph read "Six sections have each introduced a name" above a block listing seven, and the review caught it where no instrument could — a number stated in prose is invisible to every reader on this page. The obvious repair was to write seven. The check written to hold the sentence against the block then failed on something else entirely: 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.

LayerWhat it settles
modelthe semantic layer: what an identity is, provable from sealed structure alone
surfaceneeds syntax an author can write that does not exist yet
runtimeneeds a running world: grants, proposals, acceptance, crash and restart
filmneeds 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.

PropertyExpected resultFalsifiesLayer · status
every field of a revision mutated in turn, and the id re-derived through the projectiononly 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 unclassifiedD8.1model · executable
a RelationRevision carrying a pointer to its predecessorrefused as WRL_REVISION_BACKPOINTER — lifecycle has one home, and it is the ledgerD8.2model · executable
the same relation structure sealed into two different worldsthe same revision id — which world described it is provenance, and provenance rides the event, not the valueD8.3model · executable
the same local_counter minted under two different grant_idstwo different relation_ids — the separation is a property of the preimage, and it holds before any allocator exists to exploit itD8.4model · executable
two grants issued with overlapping counter_range, both actually drawn from by a running worldno collision — which needs issuance, validity and draw, none of which exist; the arithmetic half of this is the model row aboveD8.4runtime · awaiting
the same relation_name allocated in two sealed worldstwo different relation_ids — a NamedInitialAllocation carries world_id, so world-scoping is decided by the preimage, not by the surface that types the nameD8.5model · 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 routetwo 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 rowD8.5surface · executable
the same (kind, src, dst) edge key in two sealed worldstwo different relation_ids — and the same revision id, because identity is world-scoped and description is notD8.5model · executable
a world re-authored and re-sealed, every edge key unchangeda 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 pairsD8.5model · executable
a relation present in one sealed world and absent from the otherreported unpaired, in whichever direction it is missing — never handed a counterpart the map invented for itD8.5model · executable
same allocation, different initial revisionallocation collision, deterministicallyD8.6runtime · awaiting
a grant whose validity is evaluated against something the film does not recordrefused — a replayer must reach the same verdict about a draw as the original runD8.4runtime · awaiting
local reservation, crash, restartthe same allocation and birth key are retried, never a fresh counterD8.6runtime · awaiting
every endpoint of a relation derived from a sealed worlda 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 knewD8.7model · executable
every endpoint's roleone of the four declared roles — a port name in the role slot is the 0.1 defect, and it type-checks against nothingD8.7model · executable
a revision whose endpoints array is written in reversethe same revision_id — the roles carry the direction, so array position may notD8.7model · executable
canonicalisation of a revision with endpoints in arbitrary orderendpoints sorted by role in the declared enumeration order, then by terminal — sorting roles alphabetically would put peer first and disagree with the tableD8.7model · executable
a revision naming the same terminal twice, in either the same or a different rolerefused as WRL_DUPLICATE_TERMINAL — the normative rule was stated and unenforcedD8.7model · executable
a directed relation carrying a peerrefused as WRL_ENDPOINT_ROLE_ILLEGALD8.7model · executable
a symmetric relation carrying a sourcerefused — a symmetric relation with a writer is a directed one that has not admitted itD8.7model · executable
an acausal relation carrying a targetrefused — an acausal connection has no writer to nameD8.7model · executable
an acausal relation carrying a texturerefused as WRL_ACAUSAL_TEXTURE — the solver wall of §D4 already governs settlement, and a texture would be a second account of itD8.7model · executable
a directed relation stating no texture at allrefused 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 rowD8.7model · executable
the set of textures the implementation admits, against the one V1 can writeexactly §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 passesD8.7model · executable
an importer that reverses the role enumeration and widens the legal role sets, then re-runs canonicalisationunmoved — 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 commentD8.7model · executable
a directed relation with sources but no targetsrefused as WRL_INCOMPLETE_ORIENTATION — a hyperarc is an ordered pair of vertex sets and neither may be emptyD8.7model · executable
the set of orientations the implementation admitsexactly directed, symmetric, acausal — the 0.1 kernel branched on an undirected that appears in no table on this pageD8.7model · executable
every V1 edge of a sealed world taken to a revision and backthe 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.7model · executable
every field of every relation imported from both pinned worlds, classified as read, resolved, supplied or absentread 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 listD8.8model · executable
a named-initial or granted allocation offered at the importer's boundary, and again at the authoring surface'srefused 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 askingD8.8model · executable
a page that shows the derived view, given a world V1 refusesV1's frozen answer — the derived layer describes a world that sealed and does not decide whether one seals, however much better its diagnostic isD8.8model · executable
the structural correspondence between two sealed worlds, recomputed from the two artifacts aloneevery id in it recomputable by each side on its own — a correspondence that has to be authored is one that can be maintained wronglyD8.8model · executable
the imported world derived in ignorance of the import, and re-sealed after itthe 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 provenanceD8.8model · executable
each of the three allocation variants, with a field added and a field removedrefused 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 nameD8.1model · executable
a well-formed artifact handed to the derivation alongside a sem- it does not hash torefused as WRL_SEMANTIC_ID_MISMATCH — a world-scoped id whose scope is a claim the caller supplies is world-scoped to whatever the caller saysD8.5model · executable
the derivation run with no claimed id at allthe 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 inD8.5model · executable
an artifact declaring an ir_version outside the frozen setrefused 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 importD8.8model · executable
an artifact declaring an undeclared rulepack_id, or a foreign profile_id, under a recognised ir_versionrefused 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 familyD8.8model · executable
the four V1-fixed fields, checked against every derived revision and then re-projected under an alternative valueeach 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 importD8.8model · executable
a world with no relations, mapped against itselfidentity-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 worksD8.5model · executable
the derived correspondence, inspected for anything shaped like a RelationImportednone — 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 agreeD8.8model · executable
a RelationImported fact naming a pair the two artifacts do not producerefused 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 derivedD8.8model · executable
a V2 artifact grown an edges key, and a V1 artifact grown a relations keyboth refusedWRL_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 itD8.9model · executable
a V2 artifact with a foreign version, profile or rulepack, and the declared family table held against a world that really sealedeach 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 nothingD8.9model · executable
the seed field table and the allocation field table, compared as an equation, plus a seed carrying a world_idseed 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'sD8.9model · executable
a relation record carrying a world_id, a relation_id or a revision_idrefused — 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 believedD8.9model · executable
a granted seed written into a world's initial relationsrefused 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 ranD8.9model · executable
a native V2 relation written with no name — key absent, and key emptyrefused 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, silentlyD8.9model · executable
a world whose relations array is written in reversebyte-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 worldD8.9model · executable
two relations in one world under the same identity_seedrefused 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 revisesD8.9model · executable
a legacy-edge seed offered as admissible bytes, and again at the authoring boundaryadmitted 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 ownD8.9model · executable
every revision imported from a V1 edge, canonicalised standing alone and again inside a V2 relationidentical 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 checkableD8.9model · executable
every exported V2 table, written to from outside the module — and the refusal that depends on one, retried afterwardsunmoved, 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 claimD8.9model · executable
a world whose relations and objects are both written in reversethe 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 typedD8.10model · executable
a real artifact paired with a forged sem-000…0, and the allocations that derivation actually builtrefused as WRL_SEMANTIC_ID_MISMATCH, and no allocation minted under the forgery — V1 shipped the believing version, which returned real-looking ids without complaintD8.10model · executable
an expanded seed held against §D8.1's own constructor, and a world id that is not a sem- id smuggled through the expansionbyte-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 wrongD8.10model · executable
one topology sealed in both encodingstwo 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 artifactD8.10model · executable
one relation's attributes edited, and every relation's pair of ids compared across the editevery 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 encodingD8.10model · executable
two worlds whose relations differ only in name, compared as multisets because renaming re-sorts the canonical arrayevery 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 renamedD8.10model · executable
the artifact's bytes before and after deriving from it, and the canonical output searched for a derived idunmoved, and no rel-, rev- or world_id present — a derivation that memoised an id into the artifact would move the artifact that produced itD8.10model · 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 allocationD8.10model · executable
the seed variants a migration emits, a named-initial seed offered to the importer, and a migrated seed offered to an authoronly 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 nameD8.11model · executable
every key the migration does not own, compared across the changebyte-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 tidyingD8.11model · executable
the two id multisets across a real migrationevery rev- recurs, no rel- does — a migration reporting a preserved rel- would be claiming the thing §D8.5 exists to denyD8.11model · executable
the V1 → V2 correspondence run through the kernel's existing RelationImported checker, honestly and with a forged to_relationverified 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-bearingD8.11model · executable
the artifact across V1 → V2 → V1, and a second pass over the resultbyte-identical to the original, and the second pass a fixed point — a change of encoding costs nothing, so there is no normalisation to boundD8.11model · executable
each side's own sort key, whether the keys disagree on this world, and where the disagreement ends upV1 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.11D8.11model · executable
a named relation added to a migrated world, put through the correspondenceadded, 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 hadD8.11model · executable
a forged sem- on the V1 side, and again on the V2 sideboth 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 themD8.11model · executable
a named source, stripped back down, and the artifact the spine validatedthe 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 todayD8.12model · executable
an unnamed route under the V2 parser, and the same source under the V1 oneWRL_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 renamedD8.12model · 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 routeD8.12model · executable
one name over a two-way fan-out, and a name on a declarationWRL_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 routesD8.12model · executable
a world whose authored order, parsed order and V2 order are three different ordersevery 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 silentlyD8.12model · executable
two routes under one name, and the code tableWRL_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 elseD8.12model · executable
one relation renamed, and the three ids re-derived either sideevery 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 preimageD8.12model · executable
the variants the surface actually minted, and a legacy seed offered by an authornamed-initial only, every seed through the authoring gate, and the legacy one refusedlegacy-edge is a fact about an import, not something an author can assert about a relation they are writingD8.12model · executable
a named world put through format → parse → formatthe same canonical bytes, the same world id, the same text — the surface is a surface and not a lossy viewD8.13model · executable
the V2 output with its names stripped back offbyte-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 driftD8.13model · executable
a migrated world offered to the formatter, and the same world offered to the deriverWRL_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 identityD8.13model · executable
two named relations over one pair of terminals, offered as an artifact and again as the two source lines that would write themWRL_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 worldD8.13model · executable
a profile whose controller law admits more than one signal-wire input, and the two-relation world written under itaccepted, 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 trueD8.13surface · awaiting
the world id either side of a format, in both encodingsunmoved, and the V1 world underneath still seals to the pinned demo sem- — formatting is normalisation, never a compiler gateD8.13model · executable
the order the formatter emits routes in, against V1'sthe 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 keyD8.13model · executable
the V1 artifact a V2 world is consumed asbyte-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 elseD8.13model · executable
the V1 world underneath a named source, sealedthe 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 againstD8.13model · 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_configno 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 downstreamD8.14model · executable
the codes the V2 gate answers with, against the frozen spine's vocabulary and against V2's ownevery 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 meetD8.14model · executable
a route-free source — a profile line and nothing else — handed to both parsers, and the same bytes offered undeclaredtwo 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 doorD8.15model · executable
absent, doubled, versionless, over-long, from a future family, declared after something else, with no profile at all, and commented outa 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 implementationD8.15model · executable
a migrated world adopted under supplied names, and the formatter offered it before and afterevery 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 textD8.16model · 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 keya 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 choseD8.16model · executable
one relation of several offered for adoption; then all of them at once; then an already-adopted relation offered againWRL_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 pinD8.16model · executable
the published editing surface, swept for the path by which each of the two operations is reachedeach 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 belowD8.16surface · 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 outthree 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 onceD8.16surface · 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 timetwo 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 blankD8.17model · executable
five sources that declare an encoding badly — bald, doubled, over-long, an unsupported version, and one declared too laterefused 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 jobD8.17model · executable
a V2 world's own sem-, beside the sem- of the V1 artifact it projects totwo 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 oneD8.17model · executable
an undeclared world admitted, compared key by key against a direct sealWorld; and an undeclared world that is refusedthe 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 migrationD8.17model · executable
the published surface a reader types world source into, swept for a control that would decide the encodingthere 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 itD8.17surface · executable
a published, one-click example that is a well-formed V1 world carrying one broken ir declarationadmitted 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 wrongD8.17surface · 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…0each 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 toldD8.18model · executable
the view id of a V2 world's projection, recomputed from execution_artifact by the same function a runtime would use on itthe 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 worldD8.18model · executable
what crosses the runtime boundary, compared against the seal the spine already performed on the way inV1 bytes, byte-equal to the spine's own readingedges 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 radiusD8.18model · executable
the same four edges derived twice — once as the projected world's own relations, once as the V2 world's bindingsevery 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 tellingD8.18model · executable
this world's own seeds expanded under the view id — the forbidden thing, done deliberatelyrelation 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 validationD8.18model · executable
every binding recomputed from semantic_artifact alone, in both encodings, and the key set of a binding recordthe 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 itselfD8.18model · executable
a V1 world's projection, beside a V2 world'scoincident: 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 unlessD8.18model · executable
the binding list and the projected edge list of one world, compared index by indexthe 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 happensD8.18model · executable
the published surface, swept for how it arrives at the execution view it shows — and for whether it takes the short way to oneone 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 needD8.18surface · executable
a projection of each encoding serialised, crossed, and receivedthe 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 wireD8.19model · executable
the transmitted record's key set, and the fields an envelope has that it does notfive 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 detectableD8.19model · executable
three messages a sender could send: the world id filed as the view id, the bindings reordered, and a forged world idWRL_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 derivedD8.19model · executable
an extra key, a missing key, an unknown version, unparseable bytes, and an arrayWRL_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 arithmeticD8.19model · executable
one world serialised twice, and the bytes re-serialised through the spine's own serializeridentical 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 failD8.19model · executable
the committed vector file, every source re-admitted, re-derived, re-serialised and re-receivedbyte-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 ruleD8.19model · executable
a bare artifact handed to the serializerrefused. 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 rightD8.19model · executable
a frozen V1 world whose rotor lane is 263−1, serialised and read backthe 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 containedD8.19model · executable
duplicate keys, a fraction, leading whitespace, trailing bytes, -0each 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 themD8.19model · executable
a record whose keys are re-ordered — same value, different bytesrefused, 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 listD8.19model · executable
the pinned world round-tripped through the exact readerboth 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 valuesD8.19model · executable
a two-object world under graphonomous.semantic.v0: RECEIPT → WITNESSES → CLAIM, in the application's own shapea 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 ownD8.20model · executable
the same static world offered to the downgrade, the runtime projection and the text surfaceeach 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 3D8.20model · executable
a static world that writes an admit policy, a film schema or a state_schema_ref into its own bytesWRL_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 submitterD8.20model · executable
the table and every row in it, probed for mutability, and the static derivation, probed for any name of a profiledeep-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 costumeD8.20model · 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 declarefive distinct codesWRL_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 4D8.20model · 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 reversedv0 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 5D8.20model · 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 moveD8.20model · executable
a world declaring a profile_id the table has no row forWRL_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 acceptableD8.20model · executable
the encoding's default profile and its rulepack, read from both tablesone 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 7D8.20model · executable
the static world's derived relation view, held against the kernel called directly on the same seed and revisionthe 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 goD8.20model · executable
the static world submitted with its objects and relations in a different orderthe 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 downstreamD8.20model · executable
the world gate's dispatch, under a lowered row and under a static rowdelegates, 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 rulesD8.20model · executable
a package whose reference resolves only through a service it does not pinrefused — replay must not depend on a contract the package never statedD9.1film · awaiting
same birth proposed at period 20 and at period 21same RelationBirthKey, different ScheduledTransitionD9.2runtime · awaiting
same schedule, different offered_evidenceone structural candidate, two proposal envelopesD9.2runtime · awaiting
transport pack plus object storerehydrates to a byte-identical canonical filmD9.3film · awaiting
a closure object is absentan incomplete package — never a valid filmD9.3film · awaiting
a value that depends on the activation period, left implicit in the schedulerefused — two schedules must not share a birth key while meaning different thingsD9.4runtime · awaiting
a film claims a derivation is re-execution-complete but omits its implementationrefused — replay-complete is the most it may advertiseD9.5film · awaiting
The register is checked, which is the only reason to write one. The suite verifies that every row names a rule this page actually states, that no two rows share a name, and — the row that matters — that a row declaring itself executable names a check that ran — and that a row declaring itself awaiting names no live check. Without the first condition the table could mark itself green as the surface lands, which is precisely the drift every other instrument on this page exists to prevent. Without the second, a property could be quietly settled and left advertised as open, which understates the page in the one direction nobody audits. The printed 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.
Six of these rows exist because the check demanded them, not because I thought of them. The register began with the eight properties that came out of review, all of them about the newest rules — and the coverage check immediately failed with 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.

The cost, stated rather than discovered. A film that records refusals grows with load, not with state, so a component retrying a forbidden operation in a loop inflates the film without changing the world. That is a real operational problem and this draft does not solve it. It is named here because the alternative — discovering it in production and quietly dropping refusals — converts an audit record into a record of successes, which is the one thing it must not become.

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.

ClassExampleCoordination required
Monotone additionattaching an unconstrained observation relationnone — §9's join is sufficient
Addition under a bounded resourcereserving a charger; occupying one of three capacity units on a road segmentan 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 transitiontransferring ownership of a terminalconsensus, or a unique lease authority holding the right to transfer
Removal-like transitionretiring an identity; detaching a relationnone for the fact itself — append the tombstone and let the deterministic active-view rules settle the outcome
Scheduled transitiona lease expiringnone, 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 artifactAppears later, in the ledger and the film
the trusted certificate rootsthe concrete certificate a vehicle presented
the identity scheme (what counts as a name)the vehicle's actual public key
the schema new participants must satisfythe instance that satisfied it
the admission checker, by content addressits verdict, and the evidence for it
the quota and resource policythe quota state at the period of admission
the namespace rulesthe 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

  1. 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.
  2. A capability model. "Authorised by" presupposes capabilities, which are drafted (draft §23) and not built.
  3. Attributed relations (§D8). Operation 5 changes attributes that do not yet exist.
  4. 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.
  5. A declared bounded-resource kind. The escrow class above cannot be applied to an attribute that is merely a number. capacity has 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.
  6. An admission wall. The admit rule 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

  1. [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.
  2. [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.
  3. [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.
  4. [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.
  5. [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.
  6. [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.
  7. [M] Modelica Language Specification, ch. 9, Connectors and Connections. — potential variables equated, flow variables summed to zero, acausal connection semantics.
  8. [V] IEEE 1164 std_logic_1164: std_ulogic (unresolved) vs std_logic (resolved, with a resolution function).
  9. [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.
  10. [R] ROS 2 documentation, About Quality of Service settings. — reliability, durability, history, depth, deadline, lifespan, liveliness as independent policies.
  11. [S] Eclipse SUMO documentation, Randomness. — deterministic by default; --seed / --random.
  12. [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.