@norskvideo/ctl-dev-kit 0.2.111 → 0.2.112
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.
- package/package.json +1 -1
- package/testing/INVARIANTS.template.md +8 -0
- package/testing/invariants.ts +63 -18
package/package.json
CHANGED
|
@@ -47,3 +47,11 @@ runtime-only invariant the emitted graph can't show (e.g. a timing guarantee)
|
|
|
47
47
|
still gets a table row — declare its ID in the guard's `untestableIds` option;
|
|
48
48
|
the guard rejects unknown entries and flags the marker as stale once a test
|
|
49
49
|
does cite the ID.
|
|
50
|
+
|
|
51
|
+
An existing product whose doc lists invariants as bullets
|
|
52
|
+
(`- **P01** — rule text`) and proves them with a table of rules rather than
|
|
53
|
+
cited IDs can keep that shape: pass `format: "bold-bullet"` and
|
|
54
|
+
`coveredIds: RULES.map((r) => r.id)` (`testDirs` is then unneeded). Only a
|
|
55
|
+
bullet that starts a line defines an ID, so prose like "see rule **P01**"
|
|
56
|
+
never counts; every bullet needs a covering rule or an `untestableIds` entry,
|
|
57
|
+
and a rule ID the doc lacks fails the guard. New products should use the table.
|
package/testing/invariants.ts
CHANGED
|
@@ -29,6 +29,23 @@ export function parseInvariants(markdown: string): Invariant[] {
|
|
|
29
29
|
return invariants;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// The older doc shape some products keep (commentary, playout): one bullet per
|
|
33
|
+
// invariant, `- **P01** — rule text`, with the IDs proven by a rule sweep rather
|
|
34
|
+
// than cited in test text. Anchored to the line start so a prose mention like
|
|
35
|
+
// "rule **IN02** (below)" never counts as a definition.
|
|
36
|
+
const BOLD_BULLET = /^\s*[-*]\s+\*\*([A-Z]+[0-9]+[a-z]?)\*\*\s*(?:[—–-]\s*)?(.*)$/;
|
|
37
|
+
|
|
38
|
+
export function parseBoldBulletInvariants(markdown: string): Invariant[] {
|
|
39
|
+
const invariants: Invariant[] = [];
|
|
40
|
+
markdown.split("\n").forEach((text, i) => {
|
|
41
|
+
const bullet = text.match(BOLD_BULLET);
|
|
42
|
+
if (bullet?.[1] !== undefined && bullet[2] !== undefined) {
|
|
43
|
+
invariants.push({ id: bullet[1], rule: bullet[2], line: i + 1 });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return invariants;
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
export interface ParityReport {
|
|
33
50
|
ok: boolean;
|
|
34
51
|
problems: string[];
|
|
@@ -38,20 +55,35 @@ export interface ParityOptions {
|
|
|
38
55
|
// Runtime-only invariants no emitted-graph test can show (e.g. a timing
|
|
39
56
|
// guarantee). Still parity-checked: each must exist in the table, and a
|
|
40
57
|
// citing test appearing later flags the marker as stale.
|
|
41
|
-
untestableIds?: string
|
|
58
|
+
untestableIds?: Iterable<string>;
|
|
59
|
+
format?: "table" | "bold-bullet";
|
|
60
|
+
// The IDs a rule sweep covers, when coverage is data rather than test text.
|
|
61
|
+
// Given, it replaces the scan of test sources entirely.
|
|
62
|
+
coveredIds?: Iterable<string>;
|
|
42
63
|
}
|
|
43
64
|
|
|
65
|
+
const EXPECTED_SHAPE = {
|
|
66
|
+
table: "rows must look like `| INV-AREA-001 | rule text | guarded-by |`",
|
|
67
|
+
"bold-bullet": "bullets must look like `- **AREA01** — rule text`",
|
|
68
|
+
};
|
|
69
|
+
|
|
44
70
|
export function invariantParity(
|
|
45
71
|
markdown: string,
|
|
46
72
|
testSources: Record<string, string>,
|
|
47
73
|
opts: ParityOptions = {},
|
|
48
74
|
): ParityReport {
|
|
49
75
|
const problems: string[] = [];
|
|
50
|
-
const
|
|
76
|
+
const format = opts.format ?? "table";
|
|
77
|
+
const invariants = format === "bold-bullet" ? parseBoldBulletInvariants(markdown) : parseInvariants(markdown);
|
|
51
78
|
const untestable = new Set(opts.untestableIds ?? []);
|
|
79
|
+
const covered = opts.coveredIds === undefined ? undefined : new Set(opts.coveredIds);
|
|
52
80
|
|
|
81
|
+
if (format === "bold-bullet" && covered === undefined) {
|
|
82
|
+
// Bullet IDs are too short to scan test text for safely, so coverage has to be given.
|
|
83
|
+
return { ok: false, problems: ["bold-bullet format needs coveredIds — pass the IDs your rule sweep covers."] };
|
|
84
|
+
}
|
|
53
85
|
if (invariants.length === 0) {
|
|
54
|
-
problems.push(
|
|
86
|
+
problems.push(`no invariants parsed — ${EXPECTED_SHAPE[format]}.`);
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
const seen = new Set<string>();
|
|
@@ -60,25 +92,35 @@ export function invariantParity(
|
|
|
60
92
|
seen.add(inv.id);
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
const referenced = new Set<string>();
|
|
64
|
-
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
const referenced = covered ?? new Set<string>();
|
|
96
|
+
if (covered !== undefined) {
|
|
97
|
+
for (const id of covered) {
|
|
98
|
+
if (invariants.length > 0 && !seen.has(id)) {
|
|
99
|
+
problems.push(`covered ID ${id} is not in the invariants doc — add the entry or fix the ID.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
for (const [file, source] of Object.entries(testSources)) {
|
|
104
|
+
for (const token of source.match(ID_TOKEN) ?? []) {
|
|
105
|
+
referenced.add(token);
|
|
106
|
+
if (invariants.length > 0 && !seen.has(token)) {
|
|
107
|
+
problems.push(
|
|
108
|
+
`${file} references ${token}, which is not in the invariants table — add the row or fix the ID.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
69
111
|
}
|
|
70
112
|
}
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
for (const inv of invariants) {
|
|
74
116
|
if (referenced.has(inv.id) && untestable.has(inv.id)) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
117
|
+
const why = covered ? "is in coveredIds" : "a test cites it";
|
|
118
|
+
problems.push(`invariant ${inv.id} is declared untestable but ${why} — remove the stale untestableIds entry.`);
|
|
78
119
|
}
|
|
79
120
|
if (!referenced.has(inv.id) && !untestable.has(inv.id)) {
|
|
121
|
+
const fix = covered ? "add a rule for it (coveredIds)" : "cite its ID in the test that guards it,";
|
|
80
122
|
problems.push(
|
|
81
|
-
`invariant ${inv.id} (${inv.rule}) has no referencing test —
|
|
123
|
+
`invariant ${inv.id} (${inv.rule}) has no referencing test — ${fix} or declare it in untestableIds.`,
|
|
82
124
|
);
|
|
83
125
|
}
|
|
84
126
|
}
|
|
@@ -105,13 +147,16 @@ function collectTestFiles(dir: string, out: string[]): void {
|
|
|
105
147
|
}
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
// testDirs is only read when coverage comes from test text (no coveredIds).
|
|
108
151
|
export function invariantParityFromDisk(
|
|
109
|
-
opts: { invariantsPath: string; testDirs
|
|
152
|
+
opts: { invariantsPath: string; testDirs?: string[] } & ParityOptions,
|
|
110
153
|
): ParityReport {
|
|
111
154
|
const markdown = readFileSync(opts.invariantsPath, "utf8");
|
|
112
|
-
const files: string[] = [];
|
|
113
|
-
for (const dir of opts.testDirs) collectTestFiles(dir, files);
|
|
114
155
|
const sources: Record<string, string> = {};
|
|
115
|
-
|
|
116
|
-
|
|
156
|
+
if (opts.coveredIds === undefined) {
|
|
157
|
+
const files: string[] = [];
|
|
158
|
+
for (const dir of opts.testDirs ?? []) collectTestFiles(dir, files);
|
|
159
|
+
for (const file of files) sources[file] = readFileSync(file, "utf8");
|
|
160
|
+
}
|
|
161
|
+
return invariantParity(markdown, sources, opts);
|
|
117
162
|
}
|