@norskvideo/ctl-dev-kit 0.2.58 → 0.2.60

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.
@@ -5,7 +5,8 @@
5
5
  // tsconfig.base.json, the .gitignore core block, scripts/demo (only a
6
6
  // product with tests/demo.spec.ts is asked for it), publish-docs-site.yml
7
7
  // (only a repo with a docs site may carry it, and never a turnkey)
8
- // - masked per-repo line: checks.yml + upgrade-latest.yml (`product:` key)
8
+ // - masked per-repo line: checks.yml, upgrade-latest.yml, publish-docs.yml
9
+ // and the other dispatching workflows (`product:` key)
9
10
  // - base + sanctioned extension block: dprint.json (repo-specific excludes)
10
11
  // - must import the shared module: flake.nix (otherwise the product's own)
11
12
  // - structural: root tsconfig.json (shape-dependent includes stay free),
@@ -789,18 +790,21 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
789
790
  }
790
791
 
791
792
  // publish-docs.yml is OPTIONAL — a product carries it once it has an
792
- // illustrated manual to publish nightly. Unlike the workflows above it has NO
793
- // per-repo line: it is deliberately product-agnostic (per-repo bits live in
794
- // scripts/doc-guide/regen-manual.sh + an optional prepull-extra.sh), so it is a
795
- // verbatim byte-for-byte copy of the canonical.
793
+ // illustrated manual to publish nightly. Everything per-repo about the manual
794
+ // lives in scripts/doc-guide/regen-manual.sh (+ an optional prepull-extra.sh);
795
+ // the one per-repo line is the `product:` key its `docs` result is dispatched
796
+ // under, masked like checks.yml's. It was held verbatim when that dispatch was
797
+ // added, so the only passing copy was one that sent the placeholder.
796
798
  const publishDocsPath = join(repoRoot, ".github", "workflows", "publish-docs.yml");
797
799
  if (existsSync(publishDocsPath)) {
798
- const actual = readFileSync(publishDocsPath, "utf8");
799
- if (actual !== canonical.publishDocs) {
800
- push(
801
- `.github/workflows/publish-docs.yml has drifted from @norskvideo/ctl-dev-kit conventions/publish-docs.yml (${firstDiffLine(actual, canonical.publishDocs)}). ${RESYNC}`,
802
- );
803
- }
800
+ push(
801
+ workflowProblem(
802
+ ".github/workflows/publish-docs.yml",
803
+ "conventions/publish-docs.yml",
804
+ readFileSync(publishDocsPath, "utf8"),
805
+ canonical.publishDocs,
806
+ ),
807
+ );
804
808
  }
805
809
 
806
810
  // build-docs.yml is the embedded twin of publish-docs.yml: optional, verbatim.
@@ -13,12 +13,12 @@
13
13
  # whenever it builds its docs.
14
14
  #
15
15
  # This workflow is SINGLE-SOURCED in @norskvideo/ctl-dev-kit
16
- # (conventions/publish-docs.yml) and copied verbatim, same as checks.yml. It is
17
- # deliberately product-agnostic: everything per-repo lives in repo-local scripts
18
- # it calls —
16
+ # (conventions/publish-docs.yml) and copied verbatim, same as checks.yml: only
17
+ # the `product:` value below is per-repo (set it to this repo's dashboard key).
18
+ # Everything else per-repo lives in repo-local scripts it calls —
19
19
  # - scripts/doc-guide/regen-manual.sh : the guide tiers + slug list + assemble
20
20
  # - scripts/doc-guide/prepull-extra.sh : OPTIONAL, any images beyond media+studio
21
- # so the drift gate can keep every product's copy byte-identical.
21
+ # so the drift gate can keep every product's copy otherwise byte-identical.
22
22
  #
23
23
  # Nightly-only by design: a guide's engine tier stands up real instances (slow,
24
24
  # and subject to teardown flakes), so it does not ride every push. The
@@ -7,7 +7,8 @@
7
7
  // It only touches copies whose convergent form is mechanical — there is no
8
8
  // judgement anywhere:
9
9
  // - verbatim (biome.json, tsconfig.base.json) -> overwrite with canonical
10
- // - masked workflows (checks / upgrade / sync) -> canonical, repo's product: key kept
10
+ // - masked workflows (checks / upgrade / sync / -> canonical, repo's product: key kept
11
+ // publish-docs)
11
12
  // - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
12
13
  // - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
13
14
  // - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
@@ -86,6 +87,28 @@ function syncWorkflow(repoRoot: string, rel: string, canonical: string, r: SyncR
86
87
  writeIfChanged(path, canonical.replace(PRODUCT_LINE, `$1${key}`), rel, r.written);
87
88
  }
88
89
 
90
+ // publish-docs.yml: optional, and masked like checks.yml (only re-synced where
91
+ // the repo already carries it). Every copy predating the mask was written
92
+ // verbatim with the sentinel, so a copy with no key of its own takes the one
93
+ // checks.yml (required, and always filled) dispatches under — otherwise no
94
+ // nightly could ever converge those copies.
95
+ function syncPublishDocs(repoRoot: string, canonical: string, r: SyncReport): void {
96
+ const rel = ".github/workflows/publish-docs.yml";
97
+ const path = join(repoRoot, rel);
98
+ if (!existsSync(path)) return;
99
+ const own = readFileSync(path, "utf8").match(/^\s*product:\s*(\S+)/m)?.[1];
100
+ const checksPath = join(repoRoot, ".github/workflows/checks.yml");
101
+ const fromChecks = existsSync(checksPath)
102
+ ? readFileSync(checksPath, "utf8").match(/^\s*product:\s*(\S+)/m)?.[1]
103
+ : undefined;
104
+ const key = own && own !== PRODUCT_SENTINEL ? own : fromChecks;
105
+ if (!key || key === PRODUCT_SENTINEL) {
106
+ r.skipped.push(`${rel} (no product: key to preserve)`);
107
+ return;
108
+ }
109
+ writeIfChanged(path, canonical.replace(PRODUCT_LINE, `$1${key}`), rel, r.written);
110
+ }
111
+
89
112
  // build-image.yml: canonical byte-for-byte except two per-repo lines — the
90
113
  // notify job's `product:` dispatch key and the PUBLISH_IMAGE env (the Docker Hub
91
114
  // repo, or empty for a build-only product). Re-emit the canonical with both of
@@ -376,12 +399,7 @@ export function syncDrift(
376
399
  syncSmoke(repoRoot, canonical.smoke, r);
377
400
  syncIntegration(repoRoot, canonical.integration, r);
378
401
 
379
- // publish-docs.yml is optional and verbatim (no per-repo line) — only re-sync a
380
- // repo that already carries it, unlike biome/tsconfig which every repo must have.
381
- const publishDocsRel = ".github/workflows/publish-docs.yml";
382
- if (existsSync(join(repoRoot, publishDocsRel))) {
383
- writeIfChanged(join(repoRoot, publishDocsRel), canonical.publishDocs, publishDocsRel, r.written);
384
- }
402
+ syncPublishDocs(repoRoot, canonical.publishDocs, r);
385
403
  // build-docs.yml, the embedded variant, the same way: carrying it is the
386
404
  // repo's declaration (ADR-0012); sync only keeps the copy verbatim.
387
405
  const buildDocsRel = ".github/workflows/build-docs.yml";
@@ -28,11 +28,13 @@ 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 blocks. */
32
+ export declare function spokenText(text: string): string;
31
33
  /** Why a step's text cannot be narrated as written, or null when it can. Pure,
32
34
  * so the wording is pinned without a browser. Checks the budget first: an
33
35
  * over-long caption gets rewritten anyway, and the rewrite is where the
34
- * phrasing gets fixed. */
35
- export declare function captionFault(text: string, budget: number): string | null;
36
+ * phrasing gets fixed. Only the spoken part is held to either. */
37
+ export declare function captionFault(caption: string, budget: number): string | null;
36
38
  /** Device scale factor for a capture: 2 in a DOCS_GENERATE run, 1 otherwise.
37
39
  * The video spike found the manual's component crops 383 to 778 px wide; in
38
40
  * a 1080p frame they are soft, and no renderer recovers detail that was never
@@ -17,11 +17,19 @@ export const DEFAULT_CAPTION_WORDS = 50;
17
17
  const SPOKEN_WORD = /[\p{L}\p{N}]/u;
18
18
  const RAW_URL = /https?:\/\/\S+/;
19
19
  const SLASH_BETWEEN_WORDS = /\S+\/\S+/;
20
+ /** A fenced block (``` or ~~~, closed or running to the end as markdown has
21
+ * it) is a transcript or a file to copy: the page shows it, nobody speaks it. */
22
+ const FENCED_BLOCK = /^(`{3,}|~{3,})[^\n]*$[\s\S]*?(?:^\1[ \t]*$|(?![\s\S]))/gm;
23
+ /** The part of a step's text that is narrated: everything but its fenced blocks. */
24
+ export function spokenText(text) {
25
+ return text.replace(FENCED_BLOCK, "");
26
+ }
20
27
  /** Why a step's text cannot be narrated as written, or null when it can. Pure,
21
28
  * so the wording is pinned without a browser. Checks the budget first: an
22
29
  * over-long caption gets rewritten anyway, and the rewrite is where the
23
- * phrasing gets fixed. */
24
- export function captionFault(text, budget) {
30
+ * phrasing gets fixed. Only the spoken part is held to either. */
31
+ export function captionFault(caption, budget) {
32
+ const text = spokenText(caption);
25
33
  const count = text.split(/\s+/).filter((token) => SPOKEN_WORD.test(token)).length;
26
34
  if (count > budget)
27
35
  return `${count} words; the spoken budget is ${budget}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",