# Workflow invariants

<!-- Template from @norskvideo/ctl-dev-kit testing/INVARIANTS.template.md.
Copy to the product repo root as INVARIANTS.md, replace the example rows with
this product's real invariants, and add the parity-guard test below. -->

The load-bearing rules this product's composed workflow must never break. Each
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. When a code
comment carries one of these rules, cite the ID there too instead of restating
the rule.

| ID           | Invariant                                                                                                  | Guarded by                 |
| ------------ | ---------------------------------------------------------------------------------------------------------- | -------------------------- |
| INV-SCTE-001 | The SCTE-35 side-car subscribes the programme audio only (reference clock) — it never sits in the AV path  | `components.test.ts`       |
| INV-AD-002   | Scheduled ad insertions are emitted only onto pushed destinations, never the local pull                    | `components.test.ts`       |
| INV-ENC-003  | Every ladder rung carries an `nvidia` encode block only — no software/quadra fallback rung                 | `components.test.ts`       |
| INV-GPU-004  | No raw in-memory video reaches an NVIDIA surface without a videoCompose upload (engine segfault otherwise) | `compose-workflow.test.ts` |
| INV-LL-005   | The low-latency rung feeds WHEP only — it never reaches the HLS ladder                                     | `compose-workflow.test.ts` |

The rows above are worked examples (funke-pegasus's real invariants); delete
them when seeding a new product.

## Parity guard

Copy into `tests/unit/invariants-parity.test.ts`:

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

const root = join(import.meta.dir, "..", "..");

test("every invariant has a citing test and every cited ID is tabled", () => {
  const report = invariantParityFromDisk({
    invariantsPath: join(root, "INVARIANTS.md"),
    testDirs: [join(root, "shared"), join(root, "tests")],
  });
  expect(report.problems).toEqual([]);
});
```

Point `testDirs` at every tree whose `*.test.ts` files guard invariants. A
runtime-only invariant the emitted 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.
