@norskvideo/ctl-dev-kit 0.2.70 → 0.2.72
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.
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The doc-guides' spoken-budget lint, read from the source so a PR meets it.
|
|
2
|
+
//
|
|
3
|
+
// DocGuide.step() and .text() throw when a caption cannot be narrated
|
|
4
|
+
// (captionFault), but only under DOCS_GENERATE=1, and an engine-tier guide
|
|
5
|
+
// regenerates only in the nightly docs run. So an over-long caption merged
|
|
6
|
+
// green and failed the next night, one per night, since the lint stops at the
|
|
7
|
+
// first (commentary, 2026-10-11/12). checks.yml runs this instead:
|
|
8
|
+
//
|
|
9
|
+
// bun node_modules/@norskvideo/ctl-dev-kit/conventions/caption-check.ts
|
|
10
|
+
//
|
|
11
|
+
// For each step()/text() call whose caption has literal text, it takes the
|
|
12
|
+
// worst case (every optional clause, the longer arm of a ternary, one word per
|
|
13
|
+
// substitution) and applies captionFault with the guide's budget: a literal
|
|
14
|
+
// `captionWords` in the file, else captionBudget(). A caption with no literal
|
|
15
|
+
// text (a wrapper passing variables through) cannot be judged here; the
|
|
16
|
+
// runtime lint still holds it.
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import ts from "typescript";
|
|
20
|
+
|
|
21
|
+
import { captionBudget, captionFault } from "../doc-guide/doc-guide.js";
|
|
22
|
+
|
|
23
|
+
export interface CaptionFault {
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
what: string;
|
|
27
|
+
fault: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const count = (s: string) => s.split(/\s+/).filter(Boolean).length;
|
|
31
|
+
|
|
32
|
+
/** The longest text an expression can narrate, or null when it has no literal
|
|
33
|
+
* text to judge (a variable, a function, a call). */
|
|
34
|
+
function worstCase(e: ts.Expression): string | null {
|
|
35
|
+
if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text;
|
|
36
|
+
if (ts.isTemplateExpression(e)) return e.head.text + e.templateSpans.map((s) => ` x ${s.literal.text}`).join("");
|
|
37
|
+
if (ts.isParenthesizedExpression(e)) return worstCase(e.expression);
|
|
38
|
+
if (ts.isConditionalExpression(e)) {
|
|
39
|
+
const [a, b] = [worstCase(e.whenTrue), worstCase(e.whenFalse)];
|
|
40
|
+
if (a === null && b === null) return null;
|
|
41
|
+
return count(a ?? "x") >= count(b ?? "x") ? (a ?? "x") : (b ?? "x");
|
|
42
|
+
}
|
|
43
|
+
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
44
|
+
const [l, r] = [worstCase(e.left), worstCase(e.right)];
|
|
45
|
+
return l === null && r === null ? null : `${l ?? " x "}${r ?? " x "}`;
|
|
46
|
+
}
|
|
47
|
+
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
48
|
+
return worstCase(e.right);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every caption in one source file that cannot be narrated within budget. */
|
|
53
|
+
export function captionFaultsIn(source: string, file: string, budget: number): CaptionFault[] {
|
|
54
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
55
|
+
const own = /captionWords\s*:\s*(\d+)/.exec(source);
|
|
56
|
+
const limit = own ? Number(own[1]) : budget;
|
|
57
|
+
const out: CaptionFault[] = [];
|
|
58
|
+
const visit = (n: ts.Node) => {
|
|
59
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
60
|
+
const method = n.expression.name.text;
|
|
61
|
+
const [first, second] = n.arguments;
|
|
62
|
+
let what: string | null = null;
|
|
63
|
+
let narrated: string | null = null;
|
|
64
|
+
if (method === "step" && first && (!second || worstCase(second) !== null)) {
|
|
65
|
+
const heading = worstCase(first);
|
|
66
|
+
const description = second ? worstCase(second) : "";
|
|
67
|
+
if (heading !== null) {
|
|
68
|
+
what = `step "${heading}"`;
|
|
69
|
+
narrated = description ? `${heading} ${description}` : heading;
|
|
70
|
+
}
|
|
71
|
+
} else if (method === "text" && first && n.arguments.length === 1) {
|
|
72
|
+
const prose = worstCase(first);
|
|
73
|
+
if (prose !== null) {
|
|
74
|
+
what = `text "${prose.split(/\s+/).slice(0, 6).join(" ")} ..."`;
|
|
75
|
+
narrated = prose;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const fault = narrated === null ? null : captionFault(narrated, limit);
|
|
79
|
+
if (what && fault)
|
|
80
|
+
out.push({ file, line: sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1, what, fault });
|
|
81
|
+
}
|
|
82
|
+
ts.forEachChild(n, visit);
|
|
83
|
+
};
|
|
84
|
+
visit(sf);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The repo's guide sources: tracked .ts files that are guides or use DocGuide. */
|
|
89
|
+
function guideFiles(): string[] {
|
|
90
|
+
const listed = Bun.spawnSync(["git", "ls-files", "*.ts"]).stdout.toString().split("\n").filter(Boolean);
|
|
91
|
+
return listed.filter(
|
|
92
|
+
(f) =>
|
|
93
|
+
!f.includes("node_modules/") && (f.endsWith(".guide.test.ts") || readFileSync(f, "utf8").includes("DocGuide")),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (import.meta.main) {
|
|
98
|
+
const files = guideFiles();
|
|
99
|
+
const faults = files.flatMap((f) => captionFaultsIn(readFileSync(f, "utf8"), f, captionBudget()));
|
|
100
|
+
for (const f of faults)
|
|
101
|
+
console.log(`::error file=${f.file},line=${f.line}::doc-guide ${f.what} cannot be narrated: ${f.fault}`);
|
|
102
|
+
if (faults.length) {
|
|
103
|
+
console.error(
|
|
104
|
+
`caption-check: ${faults.length} caption(s) in ${files.length} guide file(s) cannot be narrated (worst case: every optional clause included).`,
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log(`caption-check: ${files.length} guide file(s), every caption within budget.`);
|
|
109
|
+
}
|
|
@@ -209,6 +209,10 @@ jobs:
|
|
|
209
209
|
bun "$q" check
|
|
210
210
|
BUN_OPTIONS="$(bun "$q" bun-options)" bun run test:unit
|
|
211
211
|
bun run docs:check
|
|
212
|
+
# Every doc-guide caption within the spoken budget, read from the
|
|
213
|
+
# source (conventions/caption-check.ts): the runtime lint only runs
|
|
214
|
+
# when a guide regenerates, which for the engine tier is the nightly.
|
|
215
|
+
bun node_modules/@norskvideo/ctl-dev-kit/conventions/caption-check.ts
|
|
212
216
|
# The tree must resolve ONE zod 4 (conventions/zod-singleton.ts).
|
|
213
217
|
# studio guards this in-repo but never shipped the guard; products
|
|
214
218
|
# are where studio/ctl/moq-watch zod ranges meet, so a split slips
|
package/conventions/checks.yml
CHANGED
|
@@ -212,6 +212,10 @@ jobs:
|
|
|
212
212
|
bun "$q" check
|
|
213
213
|
BUN_OPTIONS="$(bun "$q" bun-options)" bun run test:unit
|
|
214
214
|
bun run docs:check
|
|
215
|
+
# Every doc-guide caption within the spoken budget, read from the
|
|
216
|
+
# source (conventions/caption-check.ts): the runtime lint only runs
|
|
217
|
+
# when a guide regenerates, which for the engine tier is the nightly.
|
|
218
|
+
bun node_modules/@norskvideo/ctl-dev-kit/conventions/caption-check.ts
|
|
215
219
|
# The tree must resolve ONE zod 4 (conventions/zod-singleton.ts).
|
|
216
220
|
# studio guards this in-repo but never shipped the guard; products
|
|
217
221
|
# are where studio/ctl/moq-watch zod ranges meet, so a split slips
|
package/doc-guide/doc-guide.d.ts
CHANGED
|
@@ -28,7 +28,8 @@ export declare function docsRoot(): string;
|
|
|
28
28
|
* at about 2.5 words/s: 35 words is 14 s on one still, 50 is 20 s, and the
|
|
29
29
|
* renderer's own lint warns at 35. This is the hard stop. */
|
|
30
30
|
export declare const DEFAULT_CAPTION_WORDS = 50;
|
|
31
|
-
/** The part of a step's text that is narrated: everything but its fenced
|
|
31
|
+
/** The part of a step's text that is narrated: everything but its fenced
|
|
32
|
+
* blocks, with each markdown link read as its label. */
|
|
32
33
|
export declare function spokenText(text: string): string;
|
|
33
34
|
/** Why a step's text cannot be narrated as written, or null when it can. Pure,
|
|
34
35
|
* so the wording is pinned without a browser. Checks the budget first: an
|
package/doc-guide/doc-guide.js
CHANGED
|
@@ -20,9 +20,12 @@ const SLASH_BETWEEN_WORDS = /\S+\/\S+/;
|
|
|
20
20
|
/** A fenced block (``` or ~~~, closed or running to the end as markdown has
|
|
21
21
|
* it) is a transcript or a file to copy: the page shows it, nobody speaks it. */
|
|
22
22
|
const FENCED_BLOCK = /^(`{3,}|~{3,})[^\n]*$[\s\S]*?(?:^\1[ \t]*$|(?![\s\S]))/gm;
|
|
23
|
-
/**
|
|
23
|
+
/** A markdown link: the narration reads its label, the page carries the target. */
|
|
24
|
+
const MARKDOWN_LINK = /\[([^\]]*)\]\([^)\s]*\)/g;
|
|
25
|
+
/** The part of a step's text that is narrated: everything but its fenced
|
|
26
|
+
* blocks, with each markdown link read as its label. */
|
|
24
27
|
export function spokenText(text) {
|
|
25
|
-
return text.replace(FENCED_BLOCK, "");
|
|
28
|
+
return text.replace(FENCED_BLOCK, "").replace(MARKDOWN_LINK, "$1");
|
|
26
29
|
}
|
|
27
30
|
/** Why a step's text cannot be narrated as written, or null when it can. Pure,
|
|
28
31
|
* so the wording is pinned without a browser. Checks the budget first: an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-dev-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.72",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./create-product": "./create-product/create-product.ts",
|
|
@@ -34,14 +34,15 @@
|
|
|
34
34
|
"marked": "18.0.11"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@playwright/test": "*"
|
|
37
|
+
"@playwright/test": "*",
|
|
38
|
+
"typescript": ">=5"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@norskvideo/ctl-oas-to-ts": "^0.1.0",
|
|
41
|
-
"@norskvideo/norsk-sdk": "1.3.1-2026-10-
|
|
42
|
-
"@norskvideo/norsk-studio": "1.30.0-2026-10-
|
|
43
|
-
"@norskvideo/norsk-studio-builder": "1.30.0-2026-10-
|
|
44
|
-
"@norskvideo/norsk-studio-built-ins": "1.30.0-2026-10-
|
|
42
|
+
"@norskvideo/norsk-sdk": "1.3.1-2026-10-11-6f132471",
|
|
43
|
+
"@norskvideo/norsk-studio": "1.30.0-2026-10-12-34c09bda",
|
|
44
|
+
"@norskvideo/norsk-studio-builder": "1.30.0-2026-10-12-34c09bda",
|
|
45
|
+
"@norskvideo/norsk-studio-built-ins": "1.30.0-2026-10-12-34c09bda",
|
|
45
46
|
"@playwright/test": "^1.59.0",
|
|
46
47
|
"@types/bun": "latest",
|
|
47
48
|
"@types/cors": "^2.8.17",
|