@norskvideo/ctl-dev-kit 0.2.53 → 0.2.55
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/conventions/check-drift.ts +56 -1
- package/conventions/docs-site.md +96 -17
- package/conventions/docs.md +52 -5
- package/conventions/personas.md +25 -0
- package/conventions/publish-docs-site.yml +247 -0
- package/conventions/sync-drift.ts +53 -2
- package/create-product/docs-site.ts +9 -2
- package/create-product/turnkey.ts +121 -14
- package/doc-guide/doc-guide.d.ts +104 -9
- package/doc-guide/doc-guide.js +210 -25
- package/doc-guide/guides-config.d.ts +6 -0
- package/doc-guide/guides-config.js +4 -0
- package/doc-guide/live-browser.d.ts +2 -0
- package/doc-guide/live-browser.js +5 -1
- package/docs/guide-stubs.ts +304 -0
- package/docs/journeys.ts +217 -0
- package/docs/light-pages.ts +205 -0
- package/docs-theme/components/InstallProduct.astro +51 -0
- package/package.json +6 -5
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
|
|
12
12
|
// - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
|
|
13
13
|
// - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
|
|
14
|
+
// - publish-docs-site.yml -> verbatim; CREATED where the repo has a
|
|
15
|
+
// docs site and is not a turnkey
|
|
14
16
|
// Structural per-repo files (root tsconfig.json, deployment/build-image.sh,
|
|
15
17
|
// manifest.seed.json) have NO canonical bytes to apply — the gate checks their
|
|
16
18
|
// shape, not their content — so they are left untouched.
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
CTL_VERSION_LINE,
|
|
31
33
|
carriesIntegrationMarker,
|
|
32
34
|
DOCS_SITE_DIR,
|
|
35
|
+
DOCS_SITE_WORKFLOW,
|
|
33
36
|
END,
|
|
34
37
|
GITIGNORE_BEGIN,
|
|
35
38
|
GITIGNORE_END,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
type ProductShape,
|
|
41
44
|
PUBLISH_IMAGE_LINE,
|
|
42
45
|
RUNNER_LINE,
|
|
46
|
+
resolveRepoName,
|
|
43
47
|
resolveShape,
|
|
44
48
|
splitAtMarkers,
|
|
45
49
|
} from "./check-drift.ts";
|
|
@@ -49,6 +53,13 @@ export interface SyncReport {
|
|
|
49
53
|
skipped: string[];
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export interface SyncDriftOptions {
|
|
57
|
+
/** The GitHub repo name (`norsk-ctl-product-probe`), which decides whether the
|
|
58
|
+
* docs site's publish may be CREATED here (never in a turnkey). The CLI
|
|
59
|
+
* resolves it; a fixture passes it. Unknown ⇒ nothing is created. */
|
|
60
|
+
repoName?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
function writeIfChanged(path: string, content: string, rel: string, written: string[]): void {
|
|
53
64
|
const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
54
65
|
if (current !== content) {
|
|
@@ -267,6 +278,39 @@ function syncWithDisplay(repoRoot: string, canonical: string, r: SyncReport): vo
|
|
|
267
278
|
chmodSync(path, 0o755);
|
|
268
279
|
}
|
|
269
280
|
|
|
281
|
+
// publish-docs-site.yml: the docs SITE's publish. Verbatim (the project name is
|
|
282
|
+
// a repo variable, the credentials are secrets), so an existing copy is always
|
|
283
|
+
// re-derived whole -- the gate, not this writer, judges whether the repo may
|
|
284
|
+
// carry one. It is CREATED only where the gate would accept it: a repo with a
|
|
285
|
+
// site (there is something to publish), named, and not a turnkey (a turnkey has
|
|
286
|
+
// no public site, ADR-0012). That is how a product that grows a site acquires
|
|
287
|
+
// its publish: the next sync-dev-kit run installs it, green until the Pages
|
|
288
|
+
// project exists. A name this writer cannot determine creates nothing, since
|
|
289
|
+
// the gate could only refuse the result.
|
|
290
|
+
const TURNKEY_REPO = /^norsk-ctl-turnkey-/;
|
|
291
|
+
|
|
292
|
+
function syncPublishDocsSite(repoRoot: string, canonical: string, repoName: string | undefined, r: SyncReport): void {
|
|
293
|
+
const path = join(repoRoot, DOCS_SITE_WORKFLOW);
|
|
294
|
+
if (existsSync(path)) {
|
|
295
|
+
writeIfChanged(path, canonical, DOCS_SITE_WORKFLOW, r.written);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!hasDocsSite(repoRoot)) {
|
|
299
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (no docs site to publish)`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (repoName === undefined) {
|
|
303
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (cannot tell which repo this is; a turnkey may not carry it)`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (TURNKEY_REPO.test(repoName)) {
|
|
307
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (${repoName} is a turnkey: no public site)`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
311
|
+
writeIfChanged(path, canonical, DOCS_SITE_WORKFLOW, r.written);
|
|
312
|
+
}
|
|
313
|
+
|
|
270
314
|
// The docs site's theme copies — the writer behind check-drift's docsThemeProblems.
|
|
271
315
|
// Only for a repo that HAS a site: sync never creates one (a site is a nested
|
|
272
316
|
// standalone package with its own install, not a file this writer can conjure),
|
|
@@ -286,7 +330,12 @@ function syncDocsTheme(repoRoot: string, r: SyncReport): void {
|
|
|
286
330
|
for (const rel of adrift) r.written.push(`${DOCS_SITE_DIR}/${rel}`);
|
|
287
331
|
}
|
|
288
332
|
|
|
289
|
-
export function syncDrift(
|
|
333
|
+
export function syncDrift(
|
|
334
|
+
repoRoot: string,
|
|
335
|
+
canonical: CanonicalBytes,
|
|
336
|
+
shape: ProductShape = "studio",
|
|
337
|
+
opts: SyncDriftOptions = {},
|
|
338
|
+
): SyncReport {
|
|
290
339
|
const r: SyncReport = { written: [], skipped: [] };
|
|
291
340
|
// Shape-selected canonicals: an sdk-app's flake + checks are its own variants
|
|
292
341
|
// (the gate holds it to those). Everything else is shared across shapes.
|
|
@@ -349,6 +398,7 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes, shape: Pr
|
|
|
349
398
|
syncDemoShim(repoRoot, canonical.demoShim, r);
|
|
350
399
|
syncWithDisplay(repoRoot, canonical.withDisplay, r);
|
|
351
400
|
syncDocsTheme(repoRoot, r);
|
|
401
|
+
syncPublishDocsSite(repoRoot, canonical.publishDocsSite, opts.repoName, r);
|
|
352
402
|
|
|
353
403
|
return r;
|
|
354
404
|
}
|
|
@@ -366,6 +416,7 @@ if (import.meta.main) {
|
|
|
366
416
|
botPrs: readFileSync(join(dir, "bot-prs.yml"), "utf8"),
|
|
367
417
|
publishDocs: readFileSync(join(dir, "publish-docs.yml"), "utf8"),
|
|
368
418
|
buildDocs: readFileSync(join(dir, "build-docs.yml"), "utf8"),
|
|
419
|
+
publishDocsSite: readFileSync(join(dir, "publish-docs-site.yml"), "utf8"),
|
|
369
420
|
withDisplay: readFileSync(join(dir, "..", "doc-guide", "with-display.sh"), "utf8"),
|
|
370
421
|
checks: readFileSync(join(dir, "checks.yml"), "utf8"),
|
|
371
422
|
biome: readFileSync(join(dir, "biome.base.json"), "utf8"),
|
|
@@ -381,7 +432,7 @@ if (import.meta.main) {
|
|
|
381
432
|
checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
|
|
382
433
|
upgradeLatestSdkApp: readFileSync(join(dir, "upgrade-latest.sdk-app.yml"), "utf8"),
|
|
383
434
|
};
|
|
384
|
-
const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot));
|
|
435
|
+
const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot), { repoName: resolveRepoName(repoRoot) });
|
|
385
436
|
if (report.written.length === 0) {
|
|
386
437
|
console.log("sync-drift: all shared-convention copies already match @norskvideo/ctl-dev-kit.");
|
|
387
438
|
} else {
|
|
@@ -307,10 +307,17 @@ ${SITE_DIR}/dist-public/
|
|
|
307
307
|
${SITE_DIR}/.astro/
|
|
308
308
|
`;
|
|
309
309
|
|
|
310
|
+
/** Before either build: a placeholder page for every recorded guide the
|
|
311
|
+
* site symlinks from docs/generated whose capture has not run (dev-kit
|
|
312
|
+
* docs/guide-stubs.ts), so the site builds on a checkout with no captures --
|
|
313
|
+
* as ctl's and the Manager's do. A site that declares no recorded guides is
|
|
314
|
+
* a no-op. The publish workflow refuses to deploy a placeholder. */
|
|
315
|
+
const STUB_GUIDES = `bun run node_modules/@norskvideo/ctl-dev-kit/docs/guide-stubs.ts stub ${SITE_DIR}/src/content/docs/guides`;
|
|
316
|
+
|
|
310
317
|
/** The root package.json scripts that drive it. Named the same in every repo
|
|
311
318
|
* that has a site, including the two in the norsk-ctl monorepo. */
|
|
312
319
|
export const DOCS_SITE_SCRIPTS: Record<string, string> = {
|
|
313
320
|
"docs:dev": `bun run --cwd ${SITE_DIR} dev`,
|
|
314
|
-
"docs:site":
|
|
315
|
-
"docs:site:public":
|
|
321
|
+
"docs:site": `${STUB_GUIDES} && bun run --cwd ${SITE_DIR} build`,
|
|
322
|
+
"docs:site:public": `${STUB_GUIDES} && ASTRO_BUILD=public bun run --cwd ${SITE_DIR} build`,
|
|
316
323
|
};
|
|
@@ -302,8 +302,9 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
302
302
|
"test:unit": `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
|
|
303
303
|
"test:image": "bun test tests/image/",
|
|
304
304
|
"check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
|
|
305
|
-
"docs:check":
|
|
306
|
-
|
|
305
|
+
"docs:check":
|
|
306
|
+
"bun run node_modules/@norskvideo/ctl-dev-kit/docs/path-existence.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/light-pages.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/journeys.ts",
|
|
307
|
+
"docs:manual": `bun run node_modules/@norskvideo/ctl-dev-kit/doc-guide/prose-bundle.js --brand "${ctx.name}" --title "${ctx.name} — Documentation" --link "norsk.video=https://norsk.video/" --out docs/generated/bundle docs/handover.md docs/operating.md docs/known-limitations.md`,
|
|
307
308
|
"docs:handover": "bash scripts/docs-handover.sh",
|
|
308
309
|
"docs:handover:check": "bash scripts/docs-handover.sh --check",
|
|
309
310
|
// The docs SITE (website/) — a standalone package, so its install is its
|
|
@@ -1896,8 +1897,8 @@ function exampleInputJson(_ctx: ShapeContext): string {
|
|
|
1896
1897
|
|
|
1897
1898
|
// ---- The canonical doc set (fleet review 02 s9, backend-only turnkey) ------
|
|
1898
1899
|
// README (developer), docs/README (the index), docs/handover (the customer's
|
|
1899
|
-
// deployment document: prose around a block norsk-ctl generates), docs/
|
|
1900
|
-
// limitations (operator). Prose only: a turnkey's bundle is built from these
|
|
1900
|
+
// deployment document: prose around a block norsk-ctl generates), docs/operating
|
|
1901
|
+
// and docs/known-limitations (operator). Prose only: a turnkey's bundle is built from these
|
|
1901
1902
|
// files by docs:manual, which the image build runs, so the image serves them.
|
|
1902
1903
|
// Bundle-internal file names are written without backticks on purpose: the
|
|
1903
1904
|
// path-existence lint would look for them in the repo.
|
|
@@ -1961,7 +1962,8 @@ norsk-ctl and the pinned Norsk SDK:
|
|
|
1961
1962
|
Two channels, deliberately:
|
|
1962
1963
|
|
|
1963
1964
|
- **The bundle the image serves.** \`bun run docs:manual\` builds it from
|
|
1964
|
-
\`docs/handover.md\` and \`docs/known-limitations.md\`;
|
|
1965
|
+
\`docs/handover.md\`, \`docs/operating.md\` and \`docs/known-limitations.md\`;
|
|
1966
|
+
the image build runs
|
|
1965
1967
|
it, so every image serves them at /docs at the version that is running
|
|
1966
1968
|
(linked from the product's page in norsk-ctl). \`bun run docs:handover\`
|
|
1967
1969
|
regenerates the handover's generated block from a daemon that has this
|
|
@@ -1985,7 +1987,8 @@ served at /docs at the version that is running.
|
|
|
1985
1987
|
|
|
1986
1988
|
| Document | Audience | What it is |
|
|
1987
1989
|
| ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
1988
|
-
| \`docs/handover.md\` |
|
|
1990
|
+
| \`docs/handover.md\` | whoever gets this product running | How norsk-ctl arrives, the one command that adds this product, and the bundle route for platform teams |
|
|
1991
|
+
| \`docs/operating.md\` | operators | What a running deployment looks like from the operator's chair: the controls, the checks, the signs |
|
|
1989
1992
|
| \`docs/known-limitations.md\` | operators | What the product does not do yet, with the consequence and the workaround |
|
|
1990
1993
|
|
|
1991
1994
|
The **docs site** is a separate channel and lives in \`website/\`: the public
|
|
@@ -1995,18 +1998,84 @@ manual, on the shared Norsk theme, not built from these files. See
|
|
|
1995
1998
|
\`bun run docs:manual\` builds the bundle from these files. The tables in the
|
|
1996
1999
|
handover are generated by norsk-ctl (\`bun run docs:handover\`) — regenerate
|
|
1997
2000
|
them, never edit them.
|
|
2001
|
+
|
|
2002
|
+
\`docs/personas.md\` is for writers, not readers, and is not in the bundle: it
|
|
2003
|
+
maps this product's audiences onto the four shared personas and carries the
|
|
2004
|
+
journey table \`bun run docs:check\` verifies.
|
|
2005
|
+
`;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// The per-product persona mapping and the journey table the journeys lint
|
|
2009
|
+
// reads (conventions/personas.md). The routes name the pages the skeleton
|
|
2010
|
+
// ships; a product rewrites them as its pages arrive, and docs:check keeps
|
|
2011
|
+
// every named page real.
|
|
2012
|
+
function personasMd(ctx: ShapeContext): string {
|
|
2013
|
+
return `# Who ${ctx.name}'s documentation is for
|
|
2014
|
+
|
|
2015
|
+
The four personas are shared across every Norsk product and described in
|
|
2016
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/personas.md\`. This page maps
|
|
2017
|
+
${ctx.name}'s own audiences onto them and says where each reader goes.
|
|
2018
|
+
|
|
2019
|
+
## The mapping
|
|
2020
|
+
|
|
2021
|
+
<!-- ctl-docs-template: fill me -->
|
|
2022
|
+
_Name this product's audiences (the people who set it up, run it, and are
|
|
2023
|
+
invited into it) and say which of the four personas each one is. Runtime
|
|
2024
|
+
actors are not personas: an operator at the console is the Builder wearing
|
|
2025
|
+
the Administrator's hat._
|
|
2026
|
+
|
|
2027
|
+
## The journey table
|
|
2028
|
+
|
|
2029
|
+
| Persona | Starts at | Then, in order | Hands off to |
|
|
2030
|
+
| ------------- | ------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
|
|
2031
|
+
| Evaluator | \`/docs/\` | \`/docs/guides/\` | [norsk-ctl's manual](https://ctl.norsk.video/), to run it for real |
|
|
2032
|
+
| Builder | \`/docs/guides/\` | \`/docs/how-to/\` | \`/docs/reference/\`, for the settings they just used |
|
|
2033
|
+
| Integrator | \`/docs/reference/\` | \`/docs/concepts/\` | [norsk-ctl's CLI and API reference](https://ctl.norsk.video/) |
|
|
2034
|
+
| Administrator | \`handover.md\` | \`operating.md\`, \`known-limitations.md\` | [norsk-ctl's manual](https://ctl.norsk.video/), for everything about the machine |
|
|
2035
|
+
|
|
2036
|
+
Site paths resolve under website/src/content/docs; a bare name resolves under
|
|
2037
|
+
docs/. Each route is at most five pages and ends in a hand-off, which is the
|
|
2038
|
+
one place "see norsk-ctl for details" lives.
|
|
1998
2039
|
`;
|
|
1999
2040
|
}
|
|
2000
2041
|
|
|
2001
2042
|
function handoverMd(ctx: ShapeContext): string {
|
|
2002
|
-
return `# Deploying ${ctx.name}
|
|
2043
|
+
return `# Deploying ${ctx.name}
|
|
2044
|
+
|
|
2045
|
+
How ${ctx.name} reaches you and how to get it running. Nearly every deployment
|
|
2046
|
+
goes through norsk-ctl, and that is the first two sections. The rest of this
|
|
2047
|
+
page is for a platform team running the exported bundle in its own
|
|
2048
|
+
orchestration instead.
|
|
2049
|
+
|
|
2050
|
+
## How norsk-ctl arrives
|
|
2051
|
+
|
|
2052
|
+
\`\`\`sh
|
|
2053
|
+
curl -fsSL https://norsk.video/install/ctl | bash
|
|
2054
|
+
\`\`\`
|
|
2055
|
+
|
|
2056
|
+
When it finishes, norsk-ctl is running on the machine and its web page is where
|
|
2057
|
+
products are added, launched and watched. Everything about the machine itself
|
|
2058
|
+
is in [norsk-ctl's manual](https://ctl.norsk.video/): ports, TLS, cloud hosts,
|
|
2059
|
+
a box behind another proxy.
|
|
2060
|
+
|
|
2061
|
+
## Add this product
|
|
2062
|
+
|
|
2063
|
+
\`\`\`sh
|
|
2064
|
+
norsk-ctl product add --image <the image you were given> --license-file <file>
|
|
2065
|
+
\`\`\`
|
|
2066
|
+
|
|
2067
|
+
One command. ${ctx.name} then appears on norsk-ctl's page, ready to launch;
|
|
2068
|
+
operating.md says what to do with it once it is running.
|
|
2069
|
+
|
|
2070
|
+
## Deploying without norsk-ctl
|
|
2003
2071
|
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2072
|
+
Some platform teams run the product from a bundle norsk-ctl exports, inside
|
|
2073
|
+
their own orchestration. The tables in the generated block below come from
|
|
2074
|
+
norsk-ctl's own rendering of this product's default template
|
|
2075
|
+
(\`bun run docs:handover\`), so they are the launch truth of the version this
|
|
2076
|
+
document ships with. Regenerate them; never edit them.
|
|
2008
2077
|
|
|
2009
|
-
|
|
2078
|
+
### What you receive
|
|
2010
2079
|
|
|
2011
2080
|
A portable bundle exported by norsk-ctl: compose.yml (the flat compose, every
|
|
2012
2081
|
path relative), HANDOVER.md (the same tables as below), the working-directory
|
|
@@ -2017,13 +2086,13 @@ placeholder to replace with a production licence before the first start.
|
|
|
2017
2086
|
_Not generated yet. Run \`bun run docs:handover\` against a daemon that has this product added._
|
|
2018
2087
|
<!-- END generated -->
|
|
2019
2088
|
|
|
2020
|
-
|
|
2089
|
+
### Verifying a deployment
|
|
2021
2090
|
|
|
2022
2091
|
- The control plane answers on its port: GET /healthz returns ok.
|
|
2023
2092
|
- The media engine is licensed: its log names the licence's product entry, not a placeholder.
|
|
2024
2093
|
- The default template's outputs are reachable from where the audience is.
|
|
2025
2094
|
|
|
2026
|
-
|
|
2095
|
+
### Not in this artefact
|
|
2027
2096
|
|
|
2028
2097
|
Anything norsk-ctl-specific: the daemon, product registration, the instance
|
|
2029
2098
|
proxy and its authentication, templates-as-storage. The bundle is the launch
|
|
@@ -2031,6 +2100,42 @@ truth of one product template and needs none of them.
|
|
|
2031
2100
|
`;
|
|
2032
2101
|
}
|
|
2033
2102
|
|
|
2103
|
+
// The operator's page, in the shape of the plan's exemplar (reuters'
|
|
2104
|
+
// operating.md). Each section opens with a marker so a half-filled page is
|
|
2105
|
+
// grep-able, and the fill-me lines say what the exemplar put there.
|
|
2106
|
+
function operatingMd(ctx: ShapeContext): string {
|
|
2107
|
+
return `# Operating ${ctx.name}
|
|
2108
|
+
|
|
2109
|
+
What an operator does with a running ${ctx.name}, and where. For getting one
|
|
2110
|
+
running, see handover.md; this page assumes it is up.
|
|
2111
|
+
|
|
2112
|
+
## What it is
|
|
2113
|
+
|
|
2114
|
+
<!-- ctl-docs-template: fill me -->
|
|
2115
|
+
_One paragraph: what the deployment takes in, what it puts out, and the one
|
|
2116
|
+
thing an operator adjusts while it runs._
|
|
2117
|
+
|
|
2118
|
+
## Where the controls are
|
|
2119
|
+
|
|
2120
|
+
<!-- ctl-docs-template: fill me -->
|
|
2121
|
+
_Which screen the operator works in, and what the product's own page at /docs
|
|
2122
|
+
and its health check at /healthz are for._
|
|
2123
|
+
|
|
2124
|
+
## Checking it is doing its job
|
|
2125
|
+
|
|
2126
|
+
<!-- ctl-docs-template: fill me -->
|
|
2127
|
+
1. _The health check answers._
|
|
2128
|
+
2. _Each input is arriving. A listening input with nothing connected is idle, not failed; the sender is the thing to check._
|
|
2129
|
+
3. _Each output is connected to its destination._
|
|
2130
|
+
|
|
2131
|
+
## When something looks wrong
|
|
2132
|
+
|
|
2133
|
+
<!-- ctl-docs-template: fill me -->
|
|
2134
|
+
- _The picture is absent: which input or output to look at first._
|
|
2135
|
+
- _The picture is wrong rather than absent: what is a runtime control and what needs a rebuilt template._
|
|
2136
|
+
`;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2034
2139
|
const KNOWN_LIMITATIONS_MD = `# Known limitations
|
|
2035
2140
|
|
|
2036
2141
|
What this product does not do yet, or does with a caveat an operator must know
|
|
@@ -2105,7 +2210,9 @@ export const turnkey: ShapeModule = {
|
|
|
2105
2210
|
...docsSiteFiles(ctx),
|
|
2106
2211
|
{ path: "docs/README.md", content: docsReadmeMd(ctx) },
|
|
2107
2212
|
{ path: "docs/handover.md", content: handoverMd(ctx) },
|
|
2213
|
+
{ path: "docs/operating.md", content: operatingMd(ctx) },
|
|
2108
2214
|
{ path: "docs/known-limitations.md", content: KNOWN_LIMITATIONS_MD },
|
|
2215
|
+
{ path: "docs/personas.md", content: personasMd(ctx) },
|
|
2109
2216
|
{ path: ".github/workflows/build-docs.yml", content: ctx.canon.buildDocs },
|
|
2110
2217
|
{ path: "scripts/doc-guide/regen-manual.sh", content: REGEN_MANUAL_SH, executable: true },
|
|
2111
2218
|
{ path: "scripts/with-display.sh", content: ctx.canon.withDisplay, executable: true },
|
package/doc-guide/doc-guide.d.ts
CHANGED
|
@@ -24,6 +24,21 @@ export interface UiFixture {
|
|
|
24
24
|
* whose cwd is the repo root (the engine tier). Deferred, not module-level, so a
|
|
25
25
|
* config that sets the env var at eval time is honoured. */
|
|
26
26
|
export declare function docsRoot(): string;
|
|
27
|
+
/** The spoken ceiling for one step. The video spikes measured a step narrating
|
|
28
|
+
* at about 2.5 words/s: 35 words is 14 s on one still, 50 is 20 s, and the
|
|
29
|
+
* renderer's own lint warns at 35. This is the hard stop. */
|
|
30
|
+
export declare const DEFAULT_CAPTION_WORDS = 50;
|
|
31
|
+
/** Why a step's text cannot be narrated as written, or null when it can. Pure,
|
|
32
|
+
* so the wording is pinned without a browser. Checks the budget first: an
|
|
33
|
+
* 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
|
+
/** Device scale factor for a capture: 2 in a DOCS_GENERATE run, 1 otherwise.
|
|
37
|
+
* The video spike found the manual's component crops 383 to 778 px wide; in
|
|
38
|
+
* a 1080p frame they are soft, and no renderer recovers detail that was never
|
|
39
|
+
* captured. A plain run writes no captures, so it keeps 1x and is unchanged.
|
|
40
|
+
* One place for both tiers: guidesConfig and launchLiveBrowser read it. */
|
|
41
|
+
export declare function captureScaleFactor(): number;
|
|
27
42
|
/** Per-step screenshot treatment. `crop` shoots one component at its own bounds;
|
|
28
43
|
* `spotlight` keeps the whole screen for context but dims everything except the
|
|
29
44
|
* named region and outlines it. Omit both for a plain full-page capture.
|
|
@@ -40,6 +55,9 @@ export interface CaptureTreatment {
|
|
|
40
55
|
/** How long `livePicture` may take to arrive. Default 60s: a WebTransport
|
|
41
56
|
* session plus a catalog plus a keyframe, on a cold encoder. */
|
|
42
57
|
livePictureTimeoutMs?: number;
|
|
58
|
+
/** The rest of the live-picture gate: the product's own slate markers, the
|
|
59
|
+
* motion thresholds. `livePictureTimeoutMs` is the shorthand for its timeout. */
|
|
60
|
+
livePictureGate?: LivePictureGate;
|
|
43
61
|
}
|
|
44
62
|
/** The attributes Studio's shared MoQ player (norsk-studio-built-ins
|
|
45
63
|
* `lib/shared/moq-player`) stamps on the container it mounts into. `live`
|
|
@@ -49,19 +67,83 @@ export interface CaptureTreatment {
|
|
|
49
67
|
export declare const MOQ_PHASE_ATTR = "data-moq-phase";
|
|
50
68
|
export declare const MOQ_ERROR_ATTR = "data-moq-error";
|
|
51
69
|
export type MoqPhase = "connecting" | "live" | "error";
|
|
70
|
+
/** Slates and modals a tile must not be showing. Products add their own via
|
|
71
|
+
* `occluderSelectors`; the text pattern catches a slate rendered as DOM text. */
|
|
72
|
+
export declare const DEFAULT_OCCLUDER_SELECTORS: readonly string[];
|
|
73
|
+
export declare const DEFAULT_OCCLUDER_TEXT: RegExp;
|
|
74
|
+
export interface LivePictureGate extends LivePictureMotion {
|
|
75
|
+
/** Default 60s: a WebTransport session plus a catalog plus a keyframe, on a cold encoder. */
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
/** Selectors that mark a slate or modal when visible inside the tile. Default DEFAULT_OCCLUDER_SELECTORS. */
|
|
78
|
+
occluderSelectors?: readonly string[];
|
|
79
|
+
/** Visible text inside the tile that marks a slate; null disables. Default DEFAULT_OCCLUDER_TEXT. */
|
|
80
|
+
occluderText?: RegExp | null;
|
|
81
|
+
/** Gap between the two frame samples the motion check compares. Default 500ms. */
|
|
82
|
+
frameGapMs?: number;
|
|
83
|
+
}
|
|
84
|
+
/** What the browser reports about a tile, for `judgeLivePicture` to rule on.
|
|
85
|
+
* Gathering is a browser matter; the ruling is pure, so each check and its
|
|
86
|
+
* wording are pinned without a launch. */
|
|
87
|
+
export interface LivePictureEvidence {
|
|
88
|
+
/** `data-moq-phase` on the tile; null when nothing stamped it. */
|
|
89
|
+
phase: string | null;
|
|
90
|
+
/** `data-moq-error`, when the player reported one. */
|
|
91
|
+
error: string | null;
|
|
92
|
+
/** What `elementFromPoint` found at the tile's centre when it was neither
|
|
93
|
+
* the tile nor inside it (as `tag#id.class`); null when the tile is on top. */
|
|
94
|
+
coveredBy: string | null;
|
|
95
|
+
/** Slate or modal matches visible inside the tile: the selector that hit,
|
|
96
|
+
* or `text "..."` for the text pattern. Empty when none. */
|
|
97
|
+
occluders: string[];
|
|
98
|
+
/** Two luma samples of the tile's drawable (video or canvas), downsampled to
|
|
99
|
+
* FRAME_SAMPLE_W x FRAME_SAMPLE_H and taken `frameGapMs` apart; null when
|
|
100
|
+
* the tile has nothing to draw. */
|
|
101
|
+
frames: [number[], number[]] | null;
|
|
102
|
+
frameGapMs: number;
|
|
103
|
+
}
|
|
104
|
+
export type LivePictureCheck = "phase" | "covered" | "occluded" | "frozen" | "undrawable";
|
|
105
|
+
/** What counts as motion between the two samples. A slate is still; a live
|
|
106
|
+
* picture moves somewhere. Per-pixel on 0..255 luma, after the downsample
|
|
107
|
+
* has averaged out decoder noise. */
|
|
108
|
+
export interface LivePictureMotion {
|
|
109
|
+
/** A sampled pixel has moved when its luma changed by more than this. Default 6. */
|
|
110
|
+
pixelDelta?: number;
|
|
111
|
+
/** The fraction of sampled pixels that must have moved. Default 0.02. */
|
|
112
|
+
minChangedFraction?: number;
|
|
113
|
+
}
|
|
114
|
+
export declare const FRAME_SAMPLE_W = 32;
|
|
115
|
+
export declare const FRAME_SAMPLE_H = 18;
|
|
116
|
+
export type LivePictureVerdict = {
|
|
117
|
+
live: true;
|
|
118
|
+
} | {
|
|
119
|
+
live: false;
|
|
120
|
+
check: LivePictureCheck;
|
|
121
|
+
reason: string;
|
|
122
|
+
};
|
|
123
|
+
/** Rule on whether the tile is showing a live picture, naming the first
|
|
124
|
+
* check that says it is not. */
|
|
125
|
+
export declare function judgeLivePicture(evidence: LivePictureEvidence, motion?: LivePictureMotion): LivePictureVerdict;
|
|
52
126
|
/** The failure a guide sees when a tile never shows a picture: the tile, the
|
|
53
127
|
* phase it was stuck in and, for an error, the player's own reason — so a
|
|
54
128
|
* broken preview reads as what it is rather than as a timeout. Pure, so the
|
|
55
129
|
* wording is pinned without a browser. */
|
|
56
130
|
export declare function livePictureFailure(selector: string, phase: string | null, error: string | null, timeoutMs: number): string;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
131
|
+
/** The failure for whichever check the tile never passed. The phase check
|
|
132
|
+
* keeps its original wording; the composite checks name themselves. */
|
|
133
|
+
export declare function livePictureGateFailure(selector: string, verdict: Exclude<LivePictureVerdict, {
|
|
134
|
+
live: true;
|
|
135
|
+
}>, timeoutMs: number): string;
|
|
136
|
+
/** Block until the tile at `selector` shows a live picture, or fail saying
|
|
137
|
+
* which check it never passed. This is the wait a guide puts between "the
|
|
138
|
+
* source connected" and the capture: the player mounts at that moment, and a
|
|
139
|
+
* screenshot taken then is of a spinner. A wall-clock sleep would hide a
|
|
140
|
+
* broken preview; this turns it into a named failure.
|
|
141
|
+
*
|
|
142
|
+
* Live means: the player stamped `live`, nothing else sits at the tile's
|
|
143
|
+
* centre, no slate or modal is visible inside it, and its picture moved
|
|
144
|
+
* between two samples. The last two are what let a standby slate and a
|
|
145
|
+
* modal over the hero through the phase check alone (2026-10-07). */
|
|
146
|
+
export declare function awaitLivePicture(page: Page, selector: string, opts?: LivePictureGate): Promise<void>;
|
|
65
147
|
/** How long a capture waits for web fonts before shooting anyway. */
|
|
66
148
|
export declare const FONTS_READY_TIMEOUT_MS = 10000;
|
|
67
149
|
/** `document.fonts.ready`, bounded. True when the fonts settled in time.
|
|
@@ -82,15 +164,28 @@ export declare function screenshotFor(page: Page, treat?: CaptureTreatment): Pro
|
|
|
82
164
|
/** Pure markdown assembly — exposed so a render step can re-emit markdown from a
|
|
83
165
|
* stored entries.json without re-running the browser. */
|
|
84
166
|
export declare function renderUiMarkdown(title: string, entries: UiEntry[]): string;
|
|
167
|
+
export interface DocGuideOptions {
|
|
168
|
+
/** Spoken-word budget for one step (heading + description) or text() block.
|
|
169
|
+
* Default: NORSK_DOCS_CAPTION_WORDS (which guidesConfig exports), else 50. */
|
|
170
|
+
captionWords?: number;
|
|
171
|
+
}
|
|
172
|
+
/** The budget a guide is held to: an explicit option, else the env var the
|
|
173
|
+
* config factory exports, else the default. Read at call time, like docsRoot. */
|
|
174
|
+
export declare function captionBudget(option?: number): number;
|
|
85
175
|
export declare class DocGuide {
|
|
86
176
|
private slug;
|
|
87
177
|
private title;
|
|
178
|
+
private options;
|
|
88
179
|
private entries;
|
|
89
180
|
private captureIndex;
|
|
90
|
-
constructor(slug: string, title: string);
|
|
181
|
+
constructor(slug: string, title: string, options?: DocGuideOptions);
|
|
91
182
|
section(heading: string): void;
|
|
92
183
|
step(heading: string, description?: string): void;
|
|
93
184
|
text(prose: string): void;
|
|
185
|
+
/** Only the DOCS_GENERATE run is published and narrated, so only it is held
|
|
186
|
+
* to the spoken budget; a plain run of the guide asserts the UI and says
|
|
187
|
+
* nothing about the prose. */
|
|
188
|
+
private lintCaption;
|
|
94
189
|
/** Screenshot the page and write it immediately under the slug's docs dir. Pass
|
|
95
190
|
* a treatment to crop to one component or spotlight a region in context. */
|
|
96
191
|
capture(page: Page, caption: string, treat?: CaptureTreatment): Promise<void>;
|