Skip to content
← Documentation

docs/decomposition-experiment.md


Can the decompositions be implemented without changing core?

Empirical result. The question was whether the decompositions in types-classification.md can be implemented without extending the core vocabulary — and if not, what exact phenomenon the vocabulary cannot express.

Answer: yes, core needs no change. But one of my stated claims was wrong, and the implementation is what proved it.


What was measured first

Before implementing, the blast radius:

Measurement Result
DistrictKind referenced outside types.ts 0 files
BuildingKind referenced outside types.ts 0 files
LinkKind referenced outside types.ts 1 (facts.ts)
WorldEvent.kind values produced alert (3), handoff (2)
"deploy" / "commit" producers or consumers none — dead members

Consumers compare string literals directly (kind === "department") rather than importing the taxonomy types. deploy and commit are declared, never produced, never consumed.

They were not deleted. Deleting a member because it is presently unused would reduce intended expressive power on the basis of current usage alone — the same error as inferring a boundary from a single consumer.


Step 1 — derive the union from a const (kept: zero cost)

core/condition.ts derives ConditionId from a CONDITIONS const by indexed access. The same construction applied to events:

export const EVENT_KINDS = ["alert", "handoff", "meeting", "deploy", "commit"] as const;
export type EventKind = (typeof EVENT_KINDS)[number];

Result: tsc clean, no other file touched. The pattern transfers from core unchanged. This is retained.

But it does not perform the decomposition. The platform still names the members. It only changes where they are written.


Step 2 — the application supplies the members (reverted: both variants cost)

The actual decomposition requires the vocabulary to come from the application. Implemented as a type parameter, in the two available variants.

Variant A — with a default (K extends string = string)

tsc clean. And that cleanliness is the defect:

const bogus: WorldEvent = { kind: "banana", /* … */ };   // COMPILES

Exhaustiveness is lost. The default silently widens the contract to any string. Nothing constrains a consumer to the declared vocabulary, and no error is produced anywhere.

Variant B — no default (K extends string)

Exhaustiveness is preserved, and the parameter must then be threaded through every consumer:

112 errors across 16 filesWorldScene, WorldView, CityConsole, RuntimePanel, useWorld, build.ts, answers.ts, constitution.ts, the stress route, the page, and six test files.

The claim that was wrong

types-classification.md §12.1 states:

Expressive power is preserved, and exhaustiveness is not lost, because the application's set is still closed: it is closed by declaration rather than by being written into the platform.

That is false as implemented. Variant A loses exhaustiveness outright; variant B preserves it only by propagating a type parameter to every consumer. There is no zero-cost variant. The claim was reasoning about the mechanism, and the measurement contradicts it.


The phenomenon, stated precisely

The thing that resists implementation is not expressible-vs-inexpressible in the architectural vocabulary. It is narrower:

A closed vocabulary whose members are chosen by the consumer cannot be expressed in a shared TypeScript type without either losing closure (a default type parameter) or propagating that parameter to every consumer.

Core never encounters this because core's vocabulary is universal. The 20 conditions and 6 subject kinds apply to a hospital and a fleet unchanged, so core owns them outright and never needs to be parametric. world/types.ts has a domain taxonomy, which is why the same construction does not transfer.

This is a type-system constraint, not an architectural gap. The architectural vocabulary describes the situation without strain: the World is a projection whose codomain is application-determined; the taxonomy is an application-owned information-bearing structure; the platform's obligation is to validate rather than to enumerate.

By the standing rule — architectural change requires a measured phenomenon the current vocabulary cannot faithfully representthis does not qualify. The vocabulary represents it faithfully. Only the type system makes one encoding of it expensive.


Where exhaustiveness actually belongs

The 112 errors came from trying to make the platform type carry the application's exhaustiveness. That is the wrong location, and core already demonstrates the right one:

  • ingest validates emissions against a declared manifest at runtime;
  • the compile-time union is derived from a const owned by whoever declares it.

Applied here: the platform type carries kind: string and validates against the application's declared vocabulary; the application's renderer switches exhaustively on its own const, with no generics, because HQ's renderer is HQ's code and already knows HQ's vocabulary.

Exhaustiveness is therefore not lost — it moves, from the shared contract to the consumer that owns the taxonomy. That is the same split the boundary document reached from the other direction.

Not implemented here. It requires deciding the runtime-validation seam for the world layer, and that is a design decision rather than a measurement.


Status of the other decompositions

Unattempted, and each is now known to carry a different risk profile:

Decomposition Expected friction
12.2 productIdSubjectRef low — a field swap; core already has SubjectRef/urn
12.3 revenueMeasureFact[] low — World.measurements already exists
12.5 stats → keyed counts medium — every consumer reads stats.* by name
12.6 sources.ok → three-valued low, and it recovers a distinction rather than preserving one

12.6 is the one to do first if any: it fixes a measured defect, and unlike 12.1 it increases expressive power rather than trading it.