The row grammar, the contract, and conformance
How every REST entity is built, how a surface finds out what it may do without being told in advance, and how the repo keeps both of those true mechanically rather than by review.
The row grammar
Every entity the console serves — a fader, a bus, a DCA, a mute group, a matrix point, a
session — is one instance of a single grammar, held to mechanically by
row-grammar-conformance.test.ts:
row = mold(address, fields | derive, station, [ripples], [refusal wording], [lifecycle], [policy])
- A mold from core —
ScalarRecordResource,CollectionResource,LatchResource,OperationResourceorDerivedResource. No row subclassesConsoleResourcedirectly. - An address codec from core —
CHANNEL_ADDRESS,SINGLETON_ADDRESS,fixedKindChannelAddress(kind), or a named codec added to core when a genuinely new address shape appears. A row never spells out a{ parse, canonical }literal for a shape core already owns. - Field codecs from core —
bool,finiteNumber,enumOf,text,tokenArray,nodeIdArray,refArray,nullableChannelRef, each with mandatory example vectors. A new value shape gets a new named core codec, never an inline literal in a row file. - A station over the engine's one doorway — the ONE store's read/write path. Server stations
extend
EngineDoorStation,StripScopedStation/ChannelScopedStation, orJsonFileStation. A shape that fits none of these is a new base, added beside them with its own tests. - Registration at a sanctioned site only —
console-resources.ts(composition policy), the constructor registration block ofserver.ts, ordemo-resources.ts. A row registered anywhere else is invisible to the composition reader. - Lifecycle, when the row is a collection —
create/removedoors declared on the mold from@freemixer/core'sresource-lifecycle.ts, never hand-rolled.OPTIONSadvertisesDELETE/POSTonly where the door exists; a door that doesn't exist answers405, not a refusal code dressed up as one. - Policy —
{ dirtiesSession?, undoable?, undoBarrier?, undoLabel, undoCoalesce? }, executed once by the registry for every door.dirtiesSessiondefaultstrue;undoabledefaultsfalseand requires a codedundoLabelwhen set — the registry replays the inverse from the PRIOR field values it read before the write, never a guess.
Laws every row inherits rather than re-deciding: one store (never a second ledger for a fact
another component already owns), broadcast only through broadcastResource, refusals as codes
(never English on the wire — the client translates), absence is a fact (no instance is
not-found, never a fabricated zero), latches are kept honest by query-and-compare, and an
operation is the only place "do" semantics live.
The row-grammar test refuses a new AddressCodec, FieldCodec, or unbased *Station outside
@freemixer/core / the shared server bases. The fix is never a suppression: promote the new
shape into core, name it, give it example vectors, and instantiate it from there.
The contract drives the surface — nothing is hardcoded
The console generates its URLs, address spaces and OPTIONS from the contract; a surface asks,
it does not decide. GET reads a row, PATCH writes it, OPTIONS reports what THIS instance
currently allows — which fields are writable, what range they accept, whether DELETE/POST
exist — and any GET takes ?watch=1 to become the same body as a live stream. There is no
second, hand-held list of what a desk supports: a client that imports a constant where the
contract already publishes the fact is a deviation, because it is exactly how two surfaces end up
disagreeing about the same desk.
This is enforced, not aspirational: contract-derivation-conformance.test.ts scans the web UI for
places that decided something the console already declares — a hardcoded channel count, a
capability assumed rather than read, a range checked against a literal instead of the published
one — and keeps a shrink-only work list of what is left to move. mixer-standard.md (NORMATIVE)
states the rule twice: nothing about a specific desk or show is hardcoded, and a desk does not get
its own classes — it maps its capabilities onto the standard ones.
Practically, this means: never write down "the available options are A, B, C." Describe how
to ask — OPTIONS /channel/input/3/headAmp, or read the range a GET already carries — because
the contract row is free to change and the prose is not.
Where the declared values live, and why you cannot import them
The numbers behind those published limits are in @freemixer/declarations — every travel, span,
count, allowed value and declared default the desk asserts about itself, each carrying the
reasoning that fixed it. The package depends on nothing, and @freemixer/core depends on it and
must never re-export it.
A surface cannot reach it, by construction. @freemixer/web-ui does not list it as a
dependency, so a client's import of a declared value fails to resolve rather than becoming a lint
warning somebody suppresses; declarations-withheld.test.ts in core is the gate on the re-export
half. This is the mechanism that makes "derive from the contract" true rather than merely
requested — a surface has no way to hardcode a limit it can only receive.
The line the package draws: what a value MAY BE is declared here — travels, spans, counts, allowed values. How a value is REALISED is not — a fader taper, tick geometry, density tokens, a wire encoding belong to the surface or the adapter that draws or speaks them. Server defensive, client generative, one source: the write door refuses out-of-travel values by these declarations, and a client generates its controls from the same ones received over the wire. Neither trusts the other, and both are built from one statement of the fact.
One caution for anything device-dependent: a range that depends on the DEVICE must come from the device. A channel's gain travel is a property of the preamp patched behind it — a REAC box declares 0..55 dB, an RME mic input reaches 65 — so a row publishes the patched preamp's own declaration and never a package constant. Only a constant of the desk itself or of the maths (the pan travel, the desk's own DSP gain stage) is declared once and for all.
pnpm docs:api renders all of it, reasoning included; see
the generated API reference.
Conformance ratchets — a sample of what they guard
The repo carries around thirty *-conformance.test.ts files. Each names a real defect that
shipped once, reduces it to a mechanical scan, and — per CONTRIBUTING.md §5 — is checked on
all four ratchet arms (new drift, a fixed-but-still-listed entry, an entry that no longer matches
anything, and an entry whose meaning quietly grew) rather than hand-maintained. A few, to show the
range:
| Ratchet | Guards against |
|---|---|
row-grammar-conformance |
A row inventing its own address/field codec or station shape instead of using core's. |
contract-derivation-conformance |
A surface deciding a capability instead of reading it from the contract. |
dispatch-conformance |
Any socket or verb transport reappearing — REST is the only door, permanently. |
model-ownership-conformance |
Console truth (capacity, capability, recall rules) re-implemented in a client instead of read from @freemixer/core/the server. |
cast-regression-conformance |
The three as-cast shapes that have actually recurred, each swept mechanically. |
comment-narrative-conformance |
A code comment narrating history instead of stating the current contract — git holds history. |
contract-connector-conformance |
A contract column nothing reads — a declared fact with no consumer is a comment pretending to be a law. |
value-in-published-limit-conformance |
A published value that falls outside the range published beside it. |
wire-finite-conformance |
NaN/Infinity reaching the wire as JSON null, which a client then reads as a confident zero. |
silent-write-conformance |
A control that moved on screen while no audio path actually changed. |
restore-announce-conformance |
A restore path (session/scene load) writing a store without also broadcasting it. |
settlement-conformance |
A row answering a write from a read that can still precede it. |
env-var-conformance |
An environment variable the code reads that the admin docs don't document, or vice versa. |
native-capability-conformance |
A capability wired end-to-end in C, N-API and TypeScript that nothing in production ever calls. |
one-intake-conformance |
A second live transport into the web UI — there is exactly one stream and one dialler for it. |
Each file's own header states the defect it was built for in more detail than a table row can; read the file before extending or relaxing one.
Extending the system
The house laws behind all of the above — ownership, where code lives, the one path to audio,
testing the graph rather than the model, the four-armed ratchet, decisions written down as they
happen, house style — are in CONTRIBUTING.md; this page assumes them
and does not restate them.
For the concrete steps to add a console adapter, a plugin, a view module or a discovery probe, see technical manual → Extending. An OSC or other new control surface follows the same rule as the web UI: it reads the contract and instantiates from it, and must never grow a private map of addresses.