docs/projection-coherence-matrix.md
The Projection Coherence Matrix
Measurement only. The generated organization is the canonical truth. Every projection is evaluated independently against it, not pairwise against another projection.
Invariant under test:
No projection should reconstruct semantics that another projection receives explicitly. Where one derives meaning from presentation and another from structured data, coherence is accidental.
1. The projection inventory
Five projections — but only two derivation roots, which is the finding that matters.
| # | Projection | Derives from |
|---|---|---|
| 1 | deriveAnswers → panel text |
the World |
| 2 | WorldScene → 3D scene graph |
the World |
| 3 | /api/world → JSON |
the World |
| 4 | verifyWorld → constitutional verdict |
the World |
| 5 | CityMap → SVG city |
lib/hq/state.ts — a parallel derivation |
Projections 1–4 share a source, so their agreement is structural. Projection 5 does not.
2. The matrix — "Which agent is busiest?"
Canonical answer computed directly from the facts, independent of any
projection: weight = priority × (blocked ? 2 : 1).
| Projection | Answer | Agrees with canonical |
|---|---|---|
| canonical (from facts) | i > m > z |
— |
World.load |
i > m > z |
yes |
panel (deriveAnswers) |
names i |
yes |
| 3D department height | i > m > z |
yes |
CityMap pod height |
not measured — needs HqState |
unknown |
Agreement among 1–4 is guaranteed by construction: all read World.load. The
matrix confirms it rather than discovering it.
3. Findings — meaning reconstructed rather than received
A systematic audit for the invariant found five instances, one fixed and four open.
3.1 FIXED — an answer derived from prose
answers.ts matched label.includes("quiet"). Relabelling an identical
incident flipped the organizational answer while the render still showed the
problem. Repaired by carrying IncidentFact.condition (core's ratified
stalled). Committed with a regression witness and a mutation.
3.2 OPEN — a constitutional verdict derived from prose
constitution.ts:191:
const silentFallback = world.buildings.filter(
(b) => b.height <= 0 && !b.evidence.includes("UNKNOWN"),
);
Article VI enforcement — "an unbacked dimension must say so rather than sit at
a default" — decides compliance by grepping a human-readable evidence string
for a magic word, which the composer writes at build.ts:348.
The composer already knows this structurally. build.ts:324 computes:
const unmeasured =
b.heightRule.kind === "measure" &&
index.measure.get(`${b.id}::${b.heightRule.metric}`) == null;
It has the fact, writes it into prose, and the verifier parses the prose back out. Whether the world is judged constitutionally compliant depends on wording. Rewording that evidence string would silently change a constitutional verdict.
This is the same defect as 3.1 in the highest-consequence location, and it is the sharpest instance of the invariant being violated: meaning received explicitly, reconstructed from presentation.
Recorded, not repaired — a fix was drafted and declined.
3.3 OPEN — operational status classified twice
types.ts states status is "classified ONCE here", citing a past defect where
"the same agent could read as active in one and indistinguishable from steady
in the other."
Measured — it is classified twice:
| Site | Rule |
|---|---|
build.ts:440 |
unstaffed → unstaffed, blocked>0 → active, queueDepth>0 → steady, else idle |
state.ts:452 |
reserved → reserved, blocked>0 → active, queue.length>0 → steady, else idle |
The three shared branches are identical, so the projections currently agree
— but nothing enforces that. They agree because the same ternary was written
twice. The fourth branch already diverges in vocabulary: unstaffed versus
reserved, from different sources (placement.kind versus agent.kind).
This is textbook accidental coherence. The comment asserting single classification is false as measured.
3.4 OPEN — workload derived twice
| Site | Formula |
|---|---|
build.ts:177 |
weight += priority × (blocked ? 2 : 1) |
state.ts:462 |
workload = min(1, weight / peak) |
CityMap pod height reads the second; WorldScene, the panel and the API read
the first. Two independent derivations of how busy is this agent, feeding
different renderers. Whether they rank agents identically is unverified —
CityMap could not be measured here because it consumes HqState, which needs
telemetry.
3.5 OPEN — structure reconstructed from identifier shape
| Site | Reconstruction | Explicit field that already exists |
|---|---|---|
answers.ts:35 |
homeBuildingId.startsWith("dept-") |
Building.kind === "department" |
build.ts:300 |
b.id.startsWith("dept-") ? b.id.slice(5) |
structure.agentHomes (inverted) |
build.ts:320 |
same | same |
Three projections recover an entity's kind by parsing its id string, while the kind is carried explicitly on the object. Renaming an id prefix would silently change organizational answers.
4. Where meaning should move
The instruction is to move meaning earlier, not to teach a projection more heuristics. For each open finding, the earlier location already exists:
| Finding | Meaning currently recovered from | Already available at |
|---|---|---|
| 3.2 | evidence prose | build.ts:324 — unmeasured, a computed boolean |
| 3.3 | duplicated ternary | World.agents[].status — already classified |
| 3.4 | parallel formula | World.agents[].load — already derived |
| 3.5 | id string shape | Building.kind, structure.agentHomes |
Not one of these requires a new concept. In every case the structured fact exists upstream and the projection declines to read it. That is why the invariant is the right diagnostic: it names a class of defect where the information is already present and the consumer chooses prose or string shape instead.
5. Status
- Questions audited: 1 of 7 in full matrix form; the invariant audit is repository-wide.
- Coherence defects found: 5 — 1 fixed, 4 open.
- Highest consequence open: 3.2, a constitutional verdict that depends on wording.
- Projections sharing a derivation root: 4 of 5.
CityMapis the exception and is the only projection whose agreement is unverified.