ADR-114
`nika-cadence` — the arming registry grammar and the pure next-slot calculator, at L0
accepted · 2026-08-15 · L0 L4 · cites 2
0ADR-114: `nika-cadence` — the arming registry grammar, at L0 #
Context #
This ADR is written after the crate landed, and the reason it is late is itself worth recording.
nika-cadence arrived on main in 508b61ae1 (#928) carrying no ADR. The
repository has a gate for exactly this — adr-coverage-new-crate — and it did
not fire. Two independent reasons, both measured:
- The gate is warn-only (it exits 0 even on a miss; its own header says « Promote to fail when coverage… »).
- Its one branch that does block reads
git diff --cached, so it lives in a local pre-commit hook — and a squash-merge performed on GitHub never runs a local hook.
A gate that judges the staged diff is blind to the tree, and a gate that only exists in a local hook is blind to the merge path.
The crate's rationale was never missing — it is written at length in
crates/nika-cadence/src/lib.rs, citing its own locks. What was missing is the
ADR file the admission process expects. This document transcribes that
rationale and adds what has been measured since.
Decision #
nika-cadence is admitted at L0 · pure · zero I/O · zero async. It owns two
things and nothing else:
- the grammar of the arming registry (the
arm:block ofnika.yaml, per D-2026-08-10-N3), - the pure next-slot calculator (hand-counted 5-field cron, embedded IANA tzdb).
Why L0 and not inside a CLI crate #
Two L4 consumers are intended to read this registry — nika arm today,
nika serve at ②. Shared logic beneath more than one L4 consumer belongs at
L0; putting it in a CLI crate would make the second consumer an upward
dependency. This follows the precedent recorded in nika-check's manifest
(« THREE L0 consumers make any higher layer an upward-dep violation »).
Honest state of that argument: only ONE of the two consumers exists today. The layering choice is therefore carried by one consumer plus a stated intention, and it is listed as a follow-up rather than presented as settled.
The four locks — one law at four moments #
The crate encodes D-2026-08-11-N1→N4, which are four faces of a single law · the file proposes, the machine disposes:
- Lock · Law
- **N1 · DST** · A slot that does not exist fires at the FIRST VALID instant (02:00 absent ⇒ 03:00); a doubled slot fires ONCE, at its first occurrence. Written policy, never a guess.
- **N2 · no resume** · A beat starts from ZERO; every tick is a new run. This crate computes slots and never carries run state.
- **N3 · identity** · The MACHINE's key authorizes. `par:` DECLARES the human and proves nothing — a merge arms nothing.
- **N4 · absence** · Removing a line does NOT disarm. That gesture is `arm --disarm`, an L4 act this crate knows nothing about.
The three hermeticity constraints #
- No kernel `Clock` dependency — that trait has no civil surface. The
calculator takes a
jiff::Zoned; the clock lives at the L4 edge. Determinism becomes trivial: tests use literal instants, with no clock to drive. - The calculator never sleeps — a virtual clock's
sleepdoes not advance time, so a sleeping loop would spin forever under the very mode meant to prove it. The caller sleeps; the calculator never does. - `jiff`'s `TimeZone::get` is forbidden here — it prefers the host's
/usr/share/zoneinfo, which is a hermeticity hole. Zones resolve from the EMBEDDED tzdb only (jiff_tzdb::get+TimeZone::tzif).
Refusals teach #
Every law is validated at parse, and every refusal is named and carries its
fix. The grammar publishes 23 spec codes, all under the cadence. namespace —
a test pins that prefix, so a code cannot silently escape it.
Round 1 refuses two keys by name rather than ignoring them: signature:
(verification belongs to ②) and budget: (it waits on a measured lack). An
unknown key is a refusal, not a shrug — the grammar is closed.
Consequences #
Measured consumers today: nika-cli (the nika arm verb) and nika-tui-core.
Two findings recorded since the crate landed, both from building its first consumer:
- Eight cadence keys are unreachable through the project file. They parse in
the registry grammar but no path through
nika.yamlcan carry them. This is pinned by a test innika-cliso it cannot drift silently. It is not yet resolved in either direction — the keys may gain a path, or lose their place in the grammar. - The two readers must agree on the beat count.
nika armchecksproject.arm().len()against the registry's own count, because the project file and the registry are parsed by two different paths over the same bytes. A divergence there would be the same class of defect as the tworegistry:parsers that disagreed in both directions (fixed in #936).
One defect surfaced since admission and has been repaired (#940):
the WorkflowPath refusal taught its fix with a path borrowed from the private
monorepo tree — a leak in a public engine, and an example meaningless to anyone
outside the studio. The repair carries a test pinning the general law ·
a remedy a gate displays must itself pass the rule that gate teaches.
read at v0.109.2 · the decision record ships with the engine