docs/ownership-remainder.md
The three remaining exceptions — classified
Invariant, now supported by evidence rather than motivating it:
A semantic quantity has exactly one origin. Every projection consumes it. No projection determines organizational meaning.
The asymmetry that emerged:
- Semantic ownership flows downward. The World owns meaning.
- Presentation ownership flows upward. CityMap, WorldScene, the answers panel and any future renderer own only presentation.
Materially enforced for six quantities. Three remained unclassified. Measured, they are three different cases, and only one wants the obvious repair.
metrics — presentation over a second-origin aggregate
metrics: Array<{ label: string; value: string; note?: string }>
value is a string: String(readable.length), loc.toLocaleString(),
formatBytes(n). Labels, notes and number formatting are unambiguously
presentation and belong exactly where they are.
But metricsFor recomputes the aggregates it formats —
repos.reduce((n, r) => n + r.loc, 0) — while World.measurements already
carries the same quantities as MeasureFacts with units and evidence.
Verdict: split. The formatting is presentation and stays. The aggregate is a
second semantic origin and should read World.measurements.
Not the same risk class as the six repaired quantities. Those fed geometry; these feed an inspection panel. Drift here misreports a number to a reader rather than silently altering the shape of the world.
products — meaning, but already single-origin
productsOwnedBy(agent.id) reads estate.ts:39:
owner: AgentId;
This is a declared relation, not a computation. It is written once, in the
estate register, and read. types.ts contains zero references to owner,
so the World does not carry it — but nothing recomputes it either.
Verdict: meaning, single origin, not a drift risk. Promoting it into the World would make the projection uniform and would not eliminate a second origin, because there is only one. It is an ownership gap, not an ownership violation.
Recorded as optional, and deliberately not urgent.
lastCompleted — meaning, but the derivation is constitutionally forbidden
The instruction was to promote it if it is genuinely organizational meaning. It is. But it must not be promoted as it stands.
// state.ts:279
// A commit is the most honest "completed work" signal available today.
const repos = reposOwnedBy(t, agent).filter(…);
return { title: `Commit to ${newest.name}`, when: … };
lastCompleted is derived from commits. Article II.2:
It is forbidden to derive height from code size, file counts, or any measure of how much has been written rather than done.
The article governs height specifically, but the principle it states is about
the quantity: a commit is written, not done. lastCompleted is literally
"last commit" wearing the name of completed work — the same inversion measured
earlier, where the world renders what was written and is blind to what was done.
Promoting it would import that inversion into the canonical model, and the World is the one place it must not reach. A wrong fact in a projection misleads one renderer; a wrong fact in the World misleads every projection and every future one.
What the lawful source can and cannot supply
The completed measure now emitted from hq_task.state='done' is the lawful
Article II.2 source. It supplies a count, not a last item — there is no
"most recently completed task" fact, because completion is aggregated at
emission.
Verdict: do not promote. Do not delete. It is organizational meaning whose canonical owner does not yet exist. The blocker is observation, not ownership: nothing records which item completed most recently.
Summary
| Quantity | Kind | Origins | Action |
|---|---|---|---|
metrics |
presentation over an aggregate | 2 (aggregate recomputed) | read World.measurements; keep formatting |
products |
meaning | 1 (estate register) | optional promotion; no drift risk |
lastCompleted |
meaning | 1, but unlawful | do not promote — blocked on observation |
Two of the three do not want the obvious repair, and the reason is different in each case. That is the ownership audit behaving as a roadmap rather than a checklist: it says what to do and what not to.
The narrow remaining work:
- Point
metricsForatWorld.measurements— the last aggregate-level second origin. - Record a per-subject "most recent completion" fact if that distinction is
wanted; only then can
lastCompletedbe promoted lawfully. - Continue auditing for second semantic origins rather than individual inconsistencies.
Canonicalization follows truth, not convenience
Recorded as the governing rule the lastCompleted case produced. A quantity
becomes canonical only when all three hold:
- it represents the intended organizational phenomenon;
- it has a lawful observational source;
- multiple projections need it.
lastCompleted fails (1) and (2). Promoting it would make a known-wrong
phenomenon canonical instead of local — reducing semantic integrity rather than
improving it.
Two kinds of gap, two different remedies
| Gap | Remedy |
|---|---|
| Multiple semantic origins | eliminate duplication |
| Missing lawful observation | improve observation |
The six repaired quantities were ownership problems. lastCompleted is an
observation problem. Solving an observation problem with an ownership change
would repeat exactly what Article II.2 has warned against from the start.
Task 1 measured: metricsFor cannot read the World from where it stands
Structural blocker, measured. page.tsx runs:
const state = deriveState(telemetry); // metricsFor runs here
const world = await deriveWorld(state); // the World is derived FROM state
metricsFor lives in state.ts, upstream of the World. It cannot read
World.measurements without a circular dependency. The fix is therefore not
"point it at the World" — it is relocate it downstream into the projection,
which is where presentation belongs anyway.
But relocation is only partly possible. Measured, per branch:
| Branch | Quantities available downstream | Missing |
|---|---|---|
repos |
4 / 5 | codeBytes |
content |
0 / 4 | modules, lessons, totalMinutes, emptyLessons |
security |
1 / 4 | findings, missingEnv, database |
finance |
2 / 4 | activeSubscriptions, coursePurchases |
| total | 7 / 17 |
Ten of seventeen quantities the panel formats do not exist in the World at
all. Moving metricsFor downstream today would lose them.
So task 1 splits, and the split follows the same two-gap distinction:
- 7 quantities — an ownership problem. Relocatable once the move is made.
- 10 quantities — an observation problem. The World does not carry them; no ownership change can supply them.
Not applied. A partial relocation would leave metricsFor reading two
sources — the World for seven quantities and telemetry for ten — which is a
worse ownership position than one clean upstream origin, because it would
create the split-brain the audit exists to remove.
The honest sequence is: extend the World's measurement vocabulary to cover what
the panel needs, then relocate the whole function downstream in one move.
Until then metricsFor is a single-origin presentation function reading
telemetry, which is coherent even though it is not canonical.
Coverage versus readiness
Two different questions, and conflating them is what makes partial migration look safe:
- Coverage — how much of this projection's semantics is represented canonically?
- Readiness — can it become a pure function of the canonical model?
| Projection | Consumes | Coverage | Ready? |
|---|---|---|---|
WorldScene |
world + UI props | 6/6 | yes — already pure |
deriveAnswers |
world | — | yes |
verifyWorld |
world + facts | — | yes — facts are the World's own inputs |
/api/world |
world | — | yes |
toCityNodes |
world + AgentState[] |
6/9 | NO — reads a second source |
metricsFor |
telemetry | 7/17 | NO — upstream of the World |
The migration rule: a projection moves atomically. If it cannot become a pure function of the canonical model, it does not move yet.
Self-assessment: the last change was a partial migration
toCityNodes(world: World, agents: AgentState[]) takes a second source. I
created that in the previous commit, and the rule now records it as the thing
not to do. Stated plainly rather than defended.
What it did and did not do:
| Dual semantic roots eliminated | 6 (accent, workload, status, blocked, queue, focus) |
| Dual semantic roots created | 0 — no quantity gained a second origin |
| Projection made pure | no — three fields still come from AgentState |
| Verifiable from the World alone | no |
So it improved coherence and left the projection impure.
The tension worth weighing
Applied strictly, the atomic rule would have prevented that change — and measurement now shows the blocker is observation (10 of 17 quantities absent from the World), so the wait would be long. The three live constitutional divergences would still be live.
Proposed refinement, offered as argument rather than measurement: the atomic unit is the quantity, not the projection.
- Moving a quantity to its canonical origin is always safe — it strictly reduces the number of origins and can never create a dual root.
- Moving a projection wholesale requires full coverage, because a projection that reads two sources cannot be verified against either alone.
Under that reading both hold simultaneously: the six quantities moved atomically
and correctly; toCityNodes remains not ready, and stays where it is until
coverage reaches 9/9.
That also preserves the rule's real content — never split a single quantity across two roots — while not blocking repairs that eliminate roots one quantity at a time.
The distinction to keep: hybrid sourcing (a projection reading two places)
is a verifiability cost. Dual ownership (one quantity computed twice) is a
correctness cost. The audit has been eliminating the second; the first is what
toCityNodes now carries, and it is recorded rather than hidden.