@norskvideo/ctl-dev-kit 0.1.15 → 0.1.17

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.
@@ -65,5 +65,16 @@
65
65
  (`norsk-ctl-product-playout`, `-commentary`) or beside the code they guard
66
66
  (`-probe`, `-turnkey-funke-pegasus`) is the repo's choice — the contract is
67
67
  the IDs and the guard, not the layout.
68
+ - **Know who reads it — name the persona before writing docs or a
69
+ persona-facing surface.** Every Norsk ctl product writes for the same four
70
+ reader personas — Evaluator, Builder, Integrator, Operator (SRE) — defined in
71
+ `@norskvideo/ctl-dev-kit/conventions/personas.md`, with this product's own
72
+ audiences mapped onto them in its `docs/personas.md`. Before writing or
73
+ restructuring docs, a configure/dashboard surface, onboarding material, or any
74
+ copy a persona will read, name that reader and honour the ownership handoff
75
+ (Evaluator/Builder -> product repo, Operator -> ctl, Integrator -> both).
76
+ Runtime actors (the roles people play in the deployed product) are NOT
77
+ doc-reader personas: the operator who runs it is the Builder; guests who are
78
+ invited in receive onboarding kit, they don't read the manual.
68
79
 
69
80
  <!-- END ctl-shared-conventions v1 -->
@@ -211,6 +211,7 @@ export interface CanonicalBytes {
211
211
  core: string;
212
212
  flake: string;
213
213
  upgradeLatest: string;
214
+ syncDevKit: string;
214
215
  checks: string;
215
216
  biome: string;
216
217
  tsconfigBase: string;
@@ -336,6 +337,22 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes): DriftRe
336
337
  );
337
338
  }
338
339
 
340
+ // sync-dev-kit.yml is OPTIONAL for the same reason as upgrade-latest.yml — a
341
+ // product may not carry it yet — but when present it is the shared,
342
+ // single-sourced workflow that keeps this repo's dev-kit pin fresh, and must
343
+ // not diverge. Only its `product:` dispatch key is per-repo.
344
+ const syncPath = join(repoRoot, ".github", "workflows", "sync-dev-kit.yml");
345
+ if (existsSync(syncPath)) {
346
+ push(
347
+ workflowProblem(
348
+ ".github/workflows/sync-dev-kit.yml",
349
+ "conventions/sync-dev-kit.yml",
350
+ readFileSync(syncPath, "utf8"),
351
+ canonical.syncDevKit,
352
+ ),
353
+ );
354
+ }
355
+
339
356
  return { ok: problems.length === 0, problems };
340
357
  }
341
358
 
@@ -345,6 +362,7 @@ if (import.meta.main) {
345
362
  core: readFileSync(join(import.meta.dir, "CLAUDE.core.md"), "utf8"),
346
363
  flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
347
364
  upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
365
+ syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
348
366
  checks: readFileSync(join(import.meta.dir, "checks.yml"), "utf8"),
349
367
  biome: readFileSync(join(import.meta.dir, "biome.base.json"), "utf8"),
350
368
  tsconfigBase: readFileSync(join(import.meta.dir, "tsconfig.base.json"), "utf8"),
@@ -0,0 +1,127 @@
1
+ # Documentation personas (shared across all Norsk ctl product repos)
2
+
3
+ The shared reader vocabulary. Every Norsk ctl product — probe, commentary,
4
+ playout, the turnkeys — writes docs for the same four personas, so a reader who
5
+ learns one product's docs knows how to navigate the next. Read this before
6
+ writing or restructuring a product's `docs/`; then keep the product's own
7
+ `docs/personas.md`, which maps *this* product's named audiences and runtime
8
+ roles onto the four below.
9
+
10
+ This is a **shipped reference**, not a drift-gated copy: you consume it from
11
+ `node_modules/@norskvideo/ctl-dev-kit/conventions/personas.md`, you do not copy
12
+ it into the repo. The per-product mapping legitimately differs product to
13
+ product, so there is nothing byte-identical to gate — the shared thing is the
14
+ vocabulary, not the mapping.
15
+
16
+ ## The four personas
17
+
18
+ Two extremes (casual / deep-tech) miss the two middle personas that most docs
19
+ actually serve. Each persona has a fixed sub-structure — Skills / Wants / Doc
20
+ needs / Failure mode — so a product can fill it in the same shape.
21
+
22
+ ### 1. Evaluator
23
+
24
+ Just heard about the product. A developer, a PM at a broadcaster, a technical
25
+ founder — or a **sales engineer demoing it on their behalf**. Wants to know
26
+ "does this do what I need?" in 15 minutes.
27
+
28
+ - **Skills**: variable — assume no Docker / ffmpeg / transport-protocol knowledge.
29
+ - **Wants**: a result on screen within a few minutes; clarity on what just
30
+ happened; confidence they won't hit a wall going further. A demo-driver also
31
+ wants it to read as credible and legible live on a shared screen.
32
+ - **Doc needs**: one quick start, zero-to-running. Happy path only, no flags. A
33
+ gallery that shows the product's range at a glance.
34
+ - **Failure mode**: opens a page with twelve sidebar entries (or a dense form),
35
+ bounces.
36
+
37
+ ### 2. Builder
38
+
39
+ Building or standing up a real deployment *with* the product. A developer or a
40
+ technical producer/operator. Wants it to work; does not want to become a DevOps
41
+ expert, and does not want to hand-write config/graph YAML.
42
+
43
+ - **Skills**: can run commands, knows what a container is. May be deeply fluent
44
+ in the product's *domain* (broadcast transports, say) while still not wanting
45
+ ctl/Docker internals — a product refines this in its own mapping.
46
+ - **Wants**: task-oriented hand-holding; a UI that exposes everything; good
47
+ errors with fix hints; sensible defaults.
48
+ - **Doc needs**: task guides ("how to do X") with screenshots. A troubleshooting
49
+ checklist. Not compose YAML, not env vars, not architecture diagrams.
50
+ - **Failure mode**: hits an error, can't tell if it's their fault or a bug,
51
+ gives up.
52
+ - **Usually the most underserved persona**, and usually the product's sweet
53
+ spot — the Integrator has generated reference, the Evaluator has zero-to-hero,
54
+ the Operator is largely ctl's; the Builder's task guides are the product's own
55
+ work and the thing most worth investing in.
56
+
57
+ ### 3. Integrator
58
+
59
+ Automating against the product or wiring it into a larger system. Scripted
60
+ launches, CI/CD, programmatic control.
61
+
62
+ - **Skills**: comfortable with CLIs, API calls, shell; trusts tools, reads flags.
63
+ - **Wants**: reference that matches reality; copy-pastable invocations; scripting
64
+ examples; clear error codes; idempotent operations for CI.
65
+ - **Doc needs**: CLI reference (generated), API reference (from OpenAPI), a
66
+ config-file/schema reference, a short recipes page.
67
+ - **Failure mode**: the reference has drifted from the tool; trust is lost; they
68
+ read the source instead.
69
+
70
+ ### 4. Operator (SRE)
71
+
72
+ Running the product in production. Responsible for uptime, cost, security.
73
+
74
+ - **Skills**: deep Docker, nginx/networking/TLS; reads source when docs are thin.
75
+ - **Wants**: what the platform does to the host — ports bound, files touched,
76
+ where state lives, secrets, crash/restart, backup/restore, upgrade/DR.
77
+ - **Doc needs**: an honest architecture doc, a production deployment guide, a
78
+ security reference.
79
+ - **Failure mode**: docs are too happy-path, omit failure modes; can't tell if
80
+ the thing is production-ready.
81
+
82
+ ### On decision-makers
83
+
84
+ CFO / commercial personas are a marketing concern, not a docs one. The useful
85
+ middle is a **decision-maker evaluating for their team** — "should we build on
86
+ this?" — served by one honest overview page (scale, maintenance, lock-in, CI),
87
+ no marketing copy. Don't build a separate doc track for it.
88
+
89
+ ## Ownership: the persona handoff
90
+
91
+ The persona a page serves decides which repo owns it, once products are split
92
+ out of ctl:
93
+
94
+ | Persona | Owner repo |
95
+ | ---------- | ------------- |
96
+ | Evaluator | **product** |
97
+ | Builder | **product** |
98
+ | Integrator | **both** — the product owns its config schema + CLI subtree + API fragment; ctl owns the daemon verbs and the shared API |
99
+ | Operator | **ctl** — host footprint, ports, TLS, upgrade/DR are the platform's story, not any one product's |
100
+
101
+ The test: **if the answer changes when you swap products, it's product docs; if
102
+ it changes when you upgrade ctl, it's ctl docs.** Ownership is not presentation —
103
+ a single rendered site can still interleave both (see norsk-ctl's
104
+ `_planning/docs-after-the-split.md`).
105
+
106
+ ## Runtime actors are not doc-reader personas
107
+
108
+ A deployed product has **runtime actors** — the roles people play in the running
109
+ system (an operator at a console, a guest who joins, an analyst who reads a
110
+ report). These are *not* a fifth, sixth, seventh persona, and indexing docs by
111
+ them is the most common way a product's docs sprawl.
112
+
113
+ Reconcile them like this:
114
+
115
+ - The actor who **sets the deployment up and runs it** is the **Builder** (and,
116
+ live, the Operator in the ctl sense) — the same human, wearing hats. Their
117
+ material is the Builder task guides.
118
+ - Actors who are **invited into** the running deployment (a guest, a
119
+ contributor) rarely read the manual at all — they receive a link, and maybe an
120
+ onboarding message. Material authored *for* them but *handed out by* the
121
+ Builder — join-page guides, onboarding email templates — is **distribution /
122
+ onboarding kit**, indexed separately from the Builder's own task guides, not
123
+ as manual chapters.
124
+
125
+ Map each product's named audiences (from its `PRODUCT.md`) onto the four
126
+ personas above; don't invent a parallel scheme. Keep the per-product mapping in
127
+ the product's `docs/personas.md`.
@@ -0,0 +1,181 @@
1
+ # Keep this product's shared conventions fresh against @norskvideo/ctl-dev-kit.
2
+ # The dev-kit single-sources the files every ctl product would otherwise
3
+ # copy-and-drift (the fenced CLAUDE.md core, flake.nix, the biome/tsconfig/dprint
4
+ # bases, the shared workflows, the build bootstrap). check:drift fails CI when a
5
+ # copy diverges from the *installed* dev-kit — so a product goes stale two ways:
6
+ # its pin lags the newest published dev-kit, and once the pin bumps its copies
7
+ # need re-syncing. This workflow closes the first gap and surfaces the second.
8
+ #
9
+ # It bumps the @norskvideo/ctl-dev-kit pin to the newest published version,
10
+ # reinstalls, and runs this product's own gate (check:drift + lint + typecheck +
11
+ # unit tests) against the new bytes:
12
+ #
13
+ # - gate green -> a clean freshness bump; commit straight to main, like
14
+ # upgrade-latest.yml. The common case: a dev-kit release that doesn't touch
15
+ # this product's copies (or only verbatim ones already reconciled).
16
+ # - gate red -> the new dev-kit needs a human: a drifted copy to re-sync, or
17
+ # a convention change that breaks this product. Push a branch and open a PR;
18
+ # check:drift's own output names each drifted file and the first diffing line.
19
+ # Re-sync on the branch until green, then merge.
20
+ #
21
+ # The re-sync is deliberately NOT automated. check:drift already emits the exact
22
+ # fix, the masked / marker-block copies (the flake ctl pin, the CLAUDE core, the
23
+ # dprint and gitignore blocks) need judgement, and a bot that blindly rewrote
24
+ # them could commit a subtly-wrong splice. Detection plus a ready branch is the
25
+ # leverage; the fix stays human.
26
+ #
27
+ # Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-dev-kit.yml) and
28
+ # copied verbatim into each product repo; the check:drift gate fails CI if a copy
29
+ # diverges. Only the `product:` value below is per-repo (set it to this repo's
30
+ # dashboard key); everything else must match the dev-kit source. Never hand-edit
31
+ # the copy -- edit the dev-kit source and re-sync.
32
+ name: sync-dev-kit
33
+
34
+ on:
35
+ schedule:
36
+ - cron: "37 4 * * *"
37
+ workflow_dispatch:
38
+
39
+ permissions:
40
+ contents: write
41
+ pull-requests: write
42
+
43
+ jobs:
44
+ sync:
45
+ runs-on: x64
46
+ steps:
47
+ - uses: actions/checkout@v5
48
+
49
+ - name: Resolve the newest published dev-kit and bump the pin
50
+ id: sync
51
+ run: |
52
+ set -euo pipefail
53
+ reg="https://registry.npmjs.org"
54
+ pkg="@norskvideo/ctl-dev-kit"
55
+ enc="@norskvideo%2Fctl-dev-kit"
56
+
57
+ # The runner has no npm; read the dist-tag off the registry with curl+jq
58
+ # (same as upgrade-latest.yml resolves the Norsk nightly).
59
+ new="$(curl -fsSL "$reg/$enc" | jq -r '."dist-tags".latest')"
60
+ [ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
61
+
62
+ # The current pin, whatever its range operator. A repo with no dev-kit
63
+ # dep has nothing to sync.
64
+ old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" package.json | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
65
+ if [ -z "$old" ]; then
66
+ echo "$pkg is not pinned in package.json -- nothing to sync."
67
+ echo "changed=0" >> "$GITHUB_OUTPUT"
68
+ exit 0
69
+ fi
70
+ old_bare="${old#^}"; old_bare="${old_bare#~}"
71
+ echo "dev-kit: $old -> ^$new"
72
+ if [ "$old_bare" = "$new" ]; then
73
+ echo "already on the newest dev-kit ($new)."
74
+ echo "changed=0" >> "$GITHUB_OUTPUT"
75
+ exit 0
76
+ fi
77
+ # Surgical single-line rewrite so package.json keeps its formatting
78
+ # (a jq reserialize would reflow the whole file and trip the formatter).
79
+ sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" package.json
80
+ echo "changed=1" >> "$GITHUB_OUTPUT"
81
+ echo "old=$old_bare" >> "$GITHUB_OUTPUT"
82
+ echo "new=$new" >> "$GITHUB_OUTPUT"
83
+
84
+ - name: Reinstall and run the product gate against the new dev-kit
85
+ id: gate
86
+ if: steps.sync.outputs.changed == '1'
87
+ continue-on-error: true
88
+ run: |
89
+ nix develop .#build --command bash -c '
90
+ set -euo pipefail
91
+ bun install
92
+ bun run check:drift
93
+ bun run lint
94
+ bun run typecheck
95
+ bun run test:unit
96
+ '
97
+
98
+ - name: Land the freshness bump on main (gate green)
99
+ if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'success'
100
+ run: |
101
+ set -euo pipefail
102
+ git config user.name "github-actions[bot]"
103
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
104
+ git add -A
105
+ git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
106
+
107
+ @norskvideo/ctl-dev-kit pin bumped to the newest published version;
108
+ check:drift + lint + typecheck + unit tests green against the new bytes.
109
+ Lockfile regenerated."
110
+ # A GITHUB_TOKEN push does not retrigger checks (Actions' recursion
111
+ # guard) -- fine, the gate above already ran them. Rebase-and-retry so a
112
+ # commit landing mid-run does not red the sync; a real conflict aborts.
113
+ for attempt in 1 2 3 4 5; do
114
+ if git push origin HEAD:main; then exit 0; fi
115
+ echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
116
+ git fetch origin main
117
+ git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
118
+ done
119
+ echo "push still failing after 5 rebase attempts"; exit 1
120
+
121
+ - name: Open a reconciliation PR (gate red)
122
+ if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'failure'
123
+ env:
124
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125
+ run: |
126
+ set -euo pipefail
127
+ branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
128
+ git config user.name "github-actions[bot]"
129
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
130
+ git checkout -b "$branch"
131
+ git add -A
132
+ git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }} (needs reconciliation)"
133
+ git push -f origin "$branch"
134
+ title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
135
+ body="$(printf '%s\n' \
136
+ "Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest" \
137
+ "published version, but this product's gate is **red** against the new bytes," \
138
+ "so the sync needs a human." \
139
+ "" \
140
+ "Run the gate locally to see what to fix:" \
141
+ "" \
142
+ " bun install" \
143
+ " bun run check:drift # names each drifted copy and the first diffing line" \
144
+ " bun run lint && bun run typecheck && bun run test:unit" \
145
+ "" \
146
+ "Re-sync the named copies from node_modules/@norskvideo/ctl-dev-kit (never" \
147
+ "hand-edit against memory), or resolve the convention change this pin" \
148
+ "introduced, pushing to this branch until green -- then merge.")"
149
+ if existing="$(gh pr list --head "$branch" --state open --json number -q '.[0].number')" && [ -n "$existing" ]; then
150
+ gh pr edit "$existing" --title "$title" --body "$body"
151
+ else
152
+ gh pr create --base main --head "$branch" --title "$title" --body "$body"
153
+ fi
154
+
155
+ - name: Report "already fresh"
156
+ if: steps.sync.outputs.changed != '1'
157
+ run: echo "dev-kit pin already current -- nothing to sync."
158
+
159
+ # Report the sync result to the aggregated product CI dashboard
160
+ # (id3as/ci-workflows). always() so a failed sync shows a sad pony. A gate-red
161
+ # run that opened a PR is still a successful sync -- the red lives on the PR's
162
+ # own checks run, not here.
163
+ notify:
164
+ needs: sync
165
+ if: always()
166
+ runs-on: x64
167
+ steps:
168
+ - uses: actions/checkout@v5
169
+ - id: meta
170
+ run: |
171
+ if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
172
+ echo "state=failure" >> "$GITHUB_OUTPUT"
173
+ else
174
+ echo "state=success" >> "$GITHUB_OUTPUT"
175
+ fi
176
+ - uses: ./.github/actions/ci-status-dispatch
177
+ with:
178
+ token: ${{ secrets.CI_DISPATCH_TOKEN }}
179
+ product: __PRODUCT__
180
+ pipeline: dev-kit
181
+ status: ${{ steps.meta.outputs.state }}
@@ -17,6 +17,7 @@ export interface Canon {
17
17
  flake: string;
18
18
  flakeLock: string;
19
19
  upgradeLatest: string;
20
+ syncDevKit: string;
20
21
  checks: string;
21
22
  biome: string;
22
23
  tsconfigBase: string;
@@ -32,6 +33,7 @@ export function loadCanon(): Canon {
32
33
  flake: readFileSync(join(buildDir, "flake.nix"), "utf8"),
33
34
  flakeLock: readFileSync(join(buildDir, "flake.lock"), "utf8"),
34
35
  upgradeLatest: readFileSync(join(conventionsDir, "upgrade-latest.yml"), "utf8"),
36
+ syncDevKit: readFileSync(join(conventionsDir, "sync-dev-kit.yml"), "utf8"),
35
37
  checks: readFileSync(join(conventionsDir, "checks.yml"), "utf8"),
36
38
  biome: readFileSync(join(conventionsDir, "biome.base.json"), "utf8"),
37
39
  tsconfigBase: readFileSync(join(conventionsDir, "tsconfig.base.json"), "utf8"),
@@ -110,6 +110,7 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
110
110
  },
111
111
  { path: ".github/workflows/checks.yml", content: fillProduct(canon.checks) },
112
112
  { path: ".github/workflows/upgrade-latest.yml", content: fillProduct(canon.upgradeLatest) },
113
+ { path: ".github/workflows/sync-dev-kit.yml", content: fillProduct(canon.syncDevKit) },
113
114
  { path: ".github/actions/ci-status-dispatch/action.yml", content: loadAsset("ci-status-dispatch.yml") },
114
115
  { path: "manifest.seed.json", content: seedJson(ctx) },
115
116
  { path: "deployment/build-image.sh", content: buildImageWrapper(ctx), executable: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",