@norskvideo/ctl-dev-kit 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,7 +84,22 @@
84
84
  the doc lives and whether rules sit in one sweep suite
85
85
  (`norsk-ctl-product-playout`, `-commentary`) or beside the code they guard
86
86
  (`-probe`, `-turnkey-funke-pegasus`) is the repo's choice — the contract is
87
- the IDs and the guard, not the layout.
87
+ the IDs and the guard, not the layout. A **customer-specific** workflow (a
88
+ reconciled `input.json` under `customers/<slug>/`) carries its **own**
89
+ `customers/<slug>/INVARIANTS.md`, scoped to that customer, seeded from
90
+ `testing/INVARIANTS.customer.template.md` and guarded by a parity test whose
91
+ `testDirs` is scoped to that customer's tests; assert against the composed
92
+ graph so resolved defaults (segment durations, resolutions, output count) are
93
+ guarded, not just literal config.
94
+ - **Invariants are a developer-managed contract — never edit them autonomously.**
95
+ An `INVARIANTS.md` (product or customer) records what a human, or a paying
96
+ customer, has decided must not regress. Claude / automated agents MUST NOT add,
97
+ edit, remove, or renumber invariant rows — or weaken the tests that guard them
98
+ — on their own initiative; those changes come only through deliberate
99
+ interaction with a developer. If a code change appears to conflict with an
100
+ invariant, STOP and surface it to a developer rather than "fixing" the
101
+ invariant to make a test pass. This is precisely what protects against
102
+ accidental regressions to the things that matter most.
88
103
  - **Know who reads it — name the persona before writing docs or a
89
104
  persona-facing surface.** Every Norsk ctl product writes for the same four
90
105
  reader personas — Evaluator, Builder, Integrator, Operator (SRE) — defined in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -0,0 +1,65 @@
1
+ # <customer> workflow invariants
2
+
3
+ <!-- Template from @norskvideo/ctl-dev-kit testing/INVARIANTS.customer.template.md.
4
+ Copy to customers/<slug>/INVARIANTS.md, replace the example rows with the rules
5
+ THIS customer cares about, and add the parity-guard test below. This is the
6
+ customer-scoped sibling of INVARIANTS.template.md (which is product-scoped): use
7
+ this one for a reconciled per-customer input.json under customers/, and the
8
+ product template for the shipped product's own composed workflow. -->
9
+
10
+ > **CUSTOMER CONTRACT — developer-managed.** These are the load-bearing rules
11
+ > this customer deems important about their workflow. They change **ONLY**
12
+ > through deliberate interaction with a developer. Claude / automated agents MUST
13
+ > NOT add, edit, remove, or renumber the rows below — or weaken their guarding
14
+ > tests — on their own initiative. If a code change appears to conflict with an
15
+ > invariant, **stop and surface it to a developer** rather than "fixing" the
16
+ > invariant. This is what protects the customer from accidental regressions to
17
+ > the things they care about.
18
+
19
+ Each rule has a stable ID (`INV-<AREA>-<NNN>`; never renumber, never reuse a
20
+ retired ID) and at least one test that cites the ID verbatim — the parity guard
21
+ fails CI when a row has no citing test or a test cites an unknown ID. Prefer
22
+ asserting against the **composed Studio graph** (compose the customer's
23
+ `input.json` and inspect the emitted workflow), not just the raw config — that
24
+ way resolved defaults (segment durations, as-rendered resolutions, output count)
25
+ are guarded too, not only literal fields. When a code comment carries one of
26
+ these rules, cite the ID there too instead of restating the rule.
27
+
28
+ | ID | Invariant | Guarded by |
29
+ | ---------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- |
30
+ | 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` |
31
+ | 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` |
32
+ | INV-OUT-001 | Exactly three delivery rungs in a single named CMAF group, one playlist per rung | `<slug>/<slug>-invariants.test.ts` |
33
+ | INV-RES-001 | The delivery resolutions are exactly the ladder the customer signed off (e.g. 1280x720 / 640x360 / 320x180) | `<slug>/<slug>-invariants.test.ts` |
34
+ | INV-MOQ-001 | The low-latency return / monitor rides MoQ, never a WHIP/RTMP fallback | `<slug>/<slug>-invariants.test.ts` |
35
+
36
+ The rows above are worked examples (drawn from the datum commentary deployment);
37
+ delete them when seeding a new customer.
38
+
39
+ ## Parity guard
40
+
41
+ Copy into `customers/<slug>/`-adjacent test dir as `<slug>-invariants-parity.test.ts`
42
+ (keep it beside the invariant tests so `testDirs` stays scoped to this customer's
43
+ IDs — other customers and the product's own invariants use their own guards):
44
+
45
+ ```ts
46
+ import { expect, test } from "bun:test";
47
+ import { join } from "node:path";
48
+ import { invariantParityFromDisk } from "@norskvideo/ctl-dev-kit/testing/invariants";
49
+
50
+ // Adjust the number of "../" hops to reach the repo root from this test's dir.
51
+ const repoRoot = join(import.meta.dir, "..", "..", "..", "..");
52
+
53
+ test("every <slug> invariant has a citing test and every cited ID is tabled", () => {
54
+ const report = invariantParityFromDisk({
55
+ invariantsPath: join(repoRoot, "customers", "<slug>", "INVARIANTS.md"),
56
+ testDirs: [import.meta.dir],
57
+ });
58
+ expect(report.problems).toEqual([]);
59
+ });
60
+ ```
61
+
62
+ A runtime-only invariant the composed graph can't show (e.g. a timing
63
+ guarantee) still gets a table row — declare its ID in the guard's `untestableIds`
64
+ option; the guard rejects unknown entries and flags the marker as stale once a
65
+ test does cite the ID.