# <customer> workflow invariants

<!-- Template from @norskvideo/ctl-dev-kit testing/INVARIANTS.customer.template.md.
Copy to customers/<slug>/INVARIANTS.md, replace the example rows with the rules
THIS customer cares about, and add the parity-guard test below. This is the
customer-scoped sibling of INVARIANTS.template.md (which is product-scoped): use
this one for a reconciled per-customer input.json under customers/, and the
product template for the shipped product's own composed workflow. -->

> **CUSTOMER CONTRACT — developer-managed.** These are the load-bearing rules
> this customer deems important about their workflow. They change **ONLY**
> through deliberate interaction with a developer. Claude / automated agents MUST
> NOT add, edit, remove, or renumber the rows below — or weaken their guarding
> tests — on their own initiative. If a code change appears to conflict with an
> invariant, **stop and surface it to a developer** rather than "fixing" the
> invariant. This is what protects the customer from accidental regressions to
> the things they care about.

Each rule has a stable ID (`INV-<AREA>-<NNN>`; never renumber, never reuse a
retired ID) and at least one test that cites the ID verbatim — the parity guard
fails CI when a row has no citing test or a test cites an unknown ID. Prefer
asserting against the **composed Studio graph** (compose the customer's
`input.json` and inspect the emitted workflow), not just the raw config — that
way resolved defaults (segment durations, as-rendered resolutions, output count)
are guarded too, not only literal fields. When a code comment carries one of
these rules, cite the ID there too instead of restating the rule.

| ID               | Invariant                                                                                                   | Guarded by                         |
| ---------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| INV-PLAYLIST-001 | This customer serves legacy playlists only — every composed CMAF playlist is non-low-latency, never LL-HLS  | `<slug>/<slug>-invariants.test.ts` |
| INV-SEG-001      | CMAF segment duration is 4s on every rendition (a resolved default — only guardable via the composed graph) | `<slug>/<slug>-invariants.test.ts` |
| INV-OUT-001      | Exactly three delivery rungs in a single named CMAF group, one playlist per rung                            | `<slug>/<slug>-invariants.test.ts` |
| INV-RES-001      | The delivery resolutions are exactly the ladder the customer signed off (e.g. 1280x720 / 640x360 / 320x180) | `<slug>/<slug>-invariants.test.ts` |
| INV-MOQ-001      | The low-latency return / monitor rides MoQ, never a WHIP/RTMP fallback                                      | `<slug>/<slug>-invariants.test.ts` |

The rows above are worked examples (drawn from the datum commentary deployment);
delete them when seeding a new customer.

## Parity guard

Copy into `customers/<slug>/`-adjacent test dir as `<slug>-invariants-parity.test.ts`
(keep it beside the invariant tests so `testDirs` stays scoped to this customer's
IDs — other customers and the product's own invariants use their own guards):

```ts
import { expect, test } from "bun:test";
import { join } from "node:path";
import { invariantParityFromDisk } from "@norskvideo/ctl-dev-kit/testing/invariants";

// Adjust the number of "../" hops to reach the repo root from this test's dir.
const repoRoot = join(import.meta.dir, "..", "..", "..", "..");

test("every <slug> invariant has a citing test and every cited ID is tabled", () => {
  const report = invariantParityFromDisk({
    invariantsPath: join(repoRoot, "customers", "<slug>", "INVARIANTS.md"),
    testDirs: [import.meta.dir],
  });
  expect(report.problems).toEqual([]);
});
```

A runtime-only invariant the composed graph can't show (e.g. a timing
guarantee) still gets a table row — declare its ID in the guard's `untestableIds`
option; the guard rejects unknown entries and flags the marker as stale once a
test does cite the ID.
