docs/semantic-ownership.md
Semantic ownership
Renderers are ignored here. The unit of audit is the semantic quantity, not the visual property.
Invariant:
A semantic quantity should have exactly one canonical computation. Every projection should consume that computation rather than reproduce it.
The distinction is no longer agreement versus disagreement. It is computed semantics versus projected semantics. A projection may transform representation, geometry, interaction or presentation. It may not determine organizational meaning.
The eight duplicated quantities are therefore not implementation duplication. They are semantic ownership violations.
Verification is bounded by its semantic root
verifyWorld(world: World, facts: WorldFacts): Violation[]
No assertion added inside this function can ever prove coherence with CityMap,
because the second derivation never enters its input. This reframes what a
verifier is:
A verifier does not establish truth. It establishes truth relative to one semantic root. Once a quantity is reconstructed elsewhere, the guarantee ends unless that reconstruction is independently verified.
Coverage of the second root: 0%, and not fixable by more rules.
The ownership table
| Quantity | Canonical | Independent computations | Consumers | Verifier coverage | Class |
|---|---|---|---|---|---|
weight |
build.ts:176 |
2 (state.ts:407, identical formula) |
3D, panel, API / CityMap | 1 of 2 | Accidental |
load / workload |
build.ts:205 |
2 (state.ts:462, different formula) |
3D, panel, API / CityMap | 1 of 2 | Accidental — DIVERGED |
status |
build.ts:440 |
2 (state.ts:452, same ternary) |
3D, API / CityMap | 1 of 2 | Accidental |
accent |
build.ts:438 |
2 (page.tsx:82, no reserved branch) |
3D, API / CityMap | 1 of 2 | Accidental — DIVERGED |
queue |
build.ts:173 |
2 (state.ts:436, mayor aggregation) |
3D, panel, API / CityMap | 1 of 2 | Accidental — DIVERGED |
blocked |
build.ts:179 |
2 (state.ts:448) |
panel, beacon / CityMap | 1 of 2 | Accidental |
focus |
build.ts:188 |
2 (state.ts:463) |
inspect / CityMap | 1 of 2 | Accidental |
lastCompleted |
state.ts:278 |
1 | CityMap only | 0 of 1 | Unowned by the World |
health |
build.ts:88 |
1 | 3D, API, verifier | 1 of 1 | Structural |
activity |
build.ts + substrates |
1 | 3D, API, verifier | 1 of 1 | Structural |
Two of ten quantities are Structural. Every other operational quantity is computed twice.
Three live divergences — three distinct failure modes
The significance is that none required a bug in new code. The architecture permitted each.
1. load/workload — drift after a one-sided repair
Article IV.4 was repaired in build.ts (linear → logarithmic). state.ts kept
the original. Beyond peak ≈ 600 an agent with real work reads 0.0100 in
CityMap and 0.3041 in the World.
Failure mode: a correct repair applied to one implementation.
2. accent — a special case added to one implementation
build.ts:438 returns UNDEVELOPED_HUE for reserved agents. page.tsx:82 is
unconditional hueFor(id). A role nobody holds renders as a staffed agent in the
SVG city, violating Article I.5 — while hue-provenance.test.ts passes, because
it observes the World.
Failure mode: a correct special case added to one implementation.
3. queue — an aggregation rule present in one implementation
// state.ts:433
const mayorQueue = all.filter((i) => i.state === "blocked");
build.ts contains zero mayor special-cases. So in CityMap the mayor holds
every blocked item in the organization; in the World the mayor holds only items
assigned to them.
Because five further quantities derive from queue, this single divergence
propagates to blocked, weight, workload, status and focus for that
agent. The mayor is a different organizational entity in the two projections.
Failure mode: an aggregation rule defined in one implementation only.
What focus shows about method
focus is computed twice by visibly different code — build.ts scans for a
best item; state.ts sorts and takes queue[0]. Both order by priority
ascending, then title, so they agree.
Measured, not assumed — and it does not change the class. focus remains
Accidental: two implementations that currently agree, with nothing preventing
the next one-sided change. Agreement was checked and found; ownership was still
violated.
Target state
Every semantic quantity is computed exactly once. Everything else is a projection.
Eight quantities move from Accidental to Structural by deleting the second
computation, not by reconciling formulas. lastCompleted is the reverse case —
computed only in the second root, so the World cannot currently express it; it
needs promoting rather than deleting.
Only after that property holds does projection coherence become a useful optimization target, because only then does a disagreement necessarily indicate a rendering defect rather than semantic drift.
APPLIED 2026-08-03 — six second origins eliminated
CityNode now reads the World. toCityNodes(world, agents) in hq/view.ts is
a pure function of the canonical semantic model, extracted from page.tsx
so the rule is checkable rather than conventional.
| Quantity | Before | After |
|---|---|---|
accent |
hueFor(id) in page.tsx |
World.agents[].accent |
workload |
state.ts linear normalisation |
World.agents[].load |
status |
state.ts ternary |
World.agents[].status |
blocked |
state.ts filter |
World.agents[].blocked |
queue |
state.ts, with a mayor rule |
World.agents[].queue |
focus |
state.ts queue[0] |
World.agents[].focus |
weight |
state.ts |
deleted — read by nothing |
All three live divergences close as a class, not as instances. weight was
measured dead: assigned once, consumed by no component, while a doc comment
claimed it drove road pulse.
Still second origins, and recorded as such: metrics, products and
lastCompleted are telemetry-derived and the World does not own them.
Finding 3.2 remains untouched per standing instruction.