docs/types-classification.md
world/types.ts — field classification against three domains
Measurement only. No redesign, no file moves, no code. Every exported type
in src/lib/world/types.ts, field by field, classified against three unrelated
domains: software companies, hospitals, logistics networks.
- universal — all three need it, unchanged
- application-specific — at most one needs it, or each needs a different one
- mixed — the concept is universal, the enumeration or encoding is not
1. Vec3
| Field | Class | Evidence |
|---|---|---|
x, y, z |
universal | Abstract world units, explicitly renderer-scaled (file header rule 1). A ward floorplan, a depot map and a service topology all need position. |
Type verdict: universal.
2. DistrictKind — "civic" | "product"
Type verdict: application-specific. A hospital groups by ward or
speciality; a logistics network by hub, region or lane. Neither has a
"product". "civic" is HQ's own metaphor for its orchestration core.
3. District
| Field | Class | Evidence |
|---|---|---|
id, label |
universal | opaque identity and display |
kind |
application-specific | inherits DistrictKind |
center, radius |
universal | spatial grouping extent |
accent |
universal | derived from identity (hueFor), not chosen |
productId |
application-specific | a typed foreign key to a product. A ward has no product id; a lane has no product id. |
revenue.listedMonthly |
mixed | the pattern (declared vs realised, null ≠ 0) is universal; "revenue on a district" is not. A ward has cost centres; a lane has tariffs. |
revenue.listedOneTime |
mixed | same |
revenue.realisedMonthly |
mixed | same. The comment's own justification — "null until telemetry proves it, never 0, because nothing has reported" — is a measure semantic, not a district semantic. |
activity |
universal | 0–1 aggregate |
health |
universal | see Health |
4. BuildingKind — hall | department | service | datacenter | library | security | conference | lot
Type verdict: application-specific. service and datacenter are software
infrastructure; the doc comments bind library to hq_memory and conference
to hq_meeting, naming HQ's own tables. A hospital needs theatre, ward,
pharmacy; a logistics network needs depot, cross-dock, yard.
lot (allocated ground, unbuilt) is the one member that generalises —
reserved-but-unrealised capacity exists in all three — but a single universal
member does not make the enumeration universal.
5. Health — ok | warn | fail | unknown
Type verdict: universal. All four states apply to a ventilator, a truck and
a service. unknown is load-bearing: it separates not measured from measured
healthy, which is the same distinction the whole provenance contract rests on.
6. Building
| Field | Class | Evidence |
|---|---|---|
id, label, shortLabel |
universal | identity and display |
kind |
application-specific | inherits BuildingKind |
districtId |
universal | grouping reference |
position, footprint, height |
universal | abstract spatial extent |
accent |
universal | derived from identity |
activity |
universal | 0–1, renderer maps it |
health |
universal | see Health |
capacity.used |
universal | bed occupancy, trailer load, request concurrency |
capacity.limit |
universal | null = unenforced. The stated reason — "an unenforced limit is a guess" — holds identically in all three domains |
capacity.basis |
universal | free-text provenance of the limit |
evidence |
universal | why it looks as it does |
detail[] |
universal | {label, value, note}, uninterpreted by the renderer |
Type verdict: mixed — one application-specific field (kind) in an
otherwise universal record.
7. WorldAgent
| Field | Class | Evidence |
|---|---|---|
id |
universal | ActorId = string, deliberately unbounded (Article V) |
code |
universal | short display code |
accent |
universal | derived |
placement |
universal | stationed / transit / attending / occupied / unstaffed — a nurse on a ward, in handover, off-rota; a driver in transit, at a depot |
position, progress |
universal | derived from placement and clock (header rule 2) |
homeBuildingId |
universal | assigned base |
status |
universal | active / steady / idle / unstaffed — operational classification, domain-free |
load, queueDepth, blocked |
universal | workload aggregates |
focus |
universal | current subject |
queue |
universal | WorkItemFact[] — work items exist in all three |
evidence |
universal | why it is where it is |
Type verdict: universal. The only exported record with no application-specific field.
8. LinkKind — road | data | handoff
Type verdict: mixed.
road— "department reports to City Hall": a reporting/structural relation. Universal.handoff— "a task moved between agents": universal (patient handover, load transfer).data— "a service calls another service": as written, software-specific. The generalisation (an information flow) is universal, but the member as documented is not.
9. Link
| Field | Class | Evidence |
|---|---|---|
id, fromBuildingId, toBuildingId |
universal | endpoints |
kind |
mixed | inherits LinkKind |
flow |
universal | 0–1 intensity |
evidence |
universal |
10. WorldEvent
| Field | Class | Evidence |
|---|---|---|
id |
universal | |
unrepresented? |
universal | count of observations an aggregate stands for |
kind |
application-specific | alert / handoff / meeting are universal; deploy and commit are software-delivery concepts. A hospital has no commits. A mixed enumeration in a shared contract is application-specific, because a consumer must switch on members that cannot occur. |
severity |
universal | info / warn / critical |
subjectId |
universal | building or agent reference |
label, detail, at |
universal |
11. World
| Field | Class | Evidence |
|---|---|---|
version |
universal | shape negotiation |
generatedAt |
universal | |
districts, buildings, agents, links, events |
universal containers | element types classified above |
measurements |
universal | MeasureFact[] with unit and evidence |
answers |
universal | {question, answer, detail?, agent?, tone} — the shape is domain-free; the questions are supplied by the application |
stats.districts/buildings/agents |
mixed | counts are universal; the field names hard-code the taxonomy |
stats.agentsWorking/Idle/InTransit/InMeeting |
mixed | same — a fixed partition of placement and status, named per HQ |
stats.openItems/blockedItems |
universal | work-item aggregates |
stats.simulationLive |
universal | "true once anything moves for a real reason" — an honesty flag |
sources[].id |
universal | substrate identity |
sources[].ok |
mixed — measured defect | see below |
sources[].note |
universal | but currently load-bearing, see below |
focus.home |
universal | camera hint |
focus.alertSubjectId |
universal | highest-severity subject |
The sources finding
Measured: src/lib/hq/telemetry.ts carries a three-valued provenance
vocabulary — live, unreachable, not-wired. world/types.ts encodes
provenance as ok: boolean plus a free-text note.
The distinction between the source exists and cannot be reached and the integration was never built is therefore destroyed at the World boundary and survives only as prose. This is the one contract identified elsewhere as the strongest platform candidate, and the shared type is the lossy copy.
12. Minimal decompositions for the mixed types
Stated as decompositions, not designs. Each preserves expressive power.
12.1 The four taxonomy enumerations
DistrictKind, BuildingKind, LinkKind, WorldEvent.kind
Minimal decomposition: the platform keeps the slot — a kind tag plus a
declared vocabulary — and the application supplies the members.
FALSIFIED BY IMPLEMENTATION 2026-08-03 — see decomposition-experiment.md.
This claimed expressive power is preserved and exhaustiveness is not lost. It is
not. Measured: a default type parameter loses closure outright (kind: "banana"
compiles), and removing the default preserves closure only by propagating the
parameter to 112 sites across 16 files. There is no zero-cost variant. The claim
reasoned about the mechanism; the measurement contradicted it.
The repair is not to the vocabulary but to the location: exhaustiveness belongs to the consumer that owns the taxonomy, not to the shared contract — which is core's own runtime-validate/derive-locally pattern.
Evidence that this is minimal, and that the mechanism already exists:
core/ingest.ts already does precisely this. ConnectorManifest declares what a
connector emits, and ingest validates emissions against the declaration,
rejecting undeclared ones. The platform already solves declare-then-validate
for conditions and subject kinds. world/types.ts does not use it.
12.2 District.productId
Minimal decomposition: one generic subject reference in place of one typed foreign key.
core already has SubjectRef and urn, and a product is a subject. A ward and
a lane are subjects too. No expressive power is lost — the district still points
at exactly one thing — and the pointer stops naming one domain's noun.
12.3 District.revenue
Minimal decomposition: three measures instead of one embedded record.
World.measurements: MeasureFact[] already exists, and MeasureFact already
carries metric, value, evidence, and optional limit/enforced.
The null-versus-zero distinction the comment defends is strengthened, not
weakened: an absent MeasureFact is unambiguously not applicable, whereas a
present one with value: 0 is measured as zero. That is the same distinction
the comment says the world previously got wrong, expressed in the vocabulary
that already exists for it.
12.4 Building.kind, Link.kind
Resolved by 12.1; no separate decomposition needed.
12.5 World.stats
Minimal decomposition: counts keyed by the declared vocabulary rather than by fixed field names.
agentsInMeeting is count(placement.kind === "attending"); districts is
count(districts). Every field is already an aggregate over data present in the
World. Keying them by vocabulary member preserves every current value and
extends to a domain whose members differ.
12.6 World.sources[].ok
Minimal decomposition: adopt the three-valued vocabulary that already exists
in hq/telemetry.ts.
This increases expressive power rather than preserving it: ok is derivable
(live → true, otherwise false), while the reverse is not. The decomposition
recovers a distinction the current contract discards.
Summary
| Count | |
|---|---|
| Exported types fully universal | Vec3, Health, WorldAgent — 3 |
| Exported types fully application-specific | DistrictKind, BuildingKind — 2 |
| Exported types mixed | District, Building, LinkKind, Link, WorldEvent, World — 6 |
Four of the six mixed types are mixed for the same reason — a closed
taxonomy enumeration written into a shared contract — and one decomposition
(12.1) resolves all four. Of the remainder, District.revenue and
World.stats are re-expressible in vocabulary the World already carries, and
sources[].ok is a lossy encoding of a contract the codebase already has
elsewhere.
No decomposition proposed here introduces a concept that does not already
exist in core or in the current World.