@norskvideo/ctl-dev-kit 0.1.87 → 0.1.89

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.
@@ -55,12 +55,31 @@ on:
55
55
  permissions:
56
56
  contents: read
57
57
 
58
- # Key the group on event_name so a push never cancels an in-flight ctl-candidate
59
- # validation: pushes cancel pushes, candidate validations cancel only newer
60
- # candidates. A cancelled validation would score as a red at release-collect.
58
+ # A ctl-candidate validation must not be cancellable out of a result, because
59
+ # release-collect scores anything other than a literal success as red — a
60
+ # CANCELLED run included and answers a red with one re-dispatch.
61
+ #
62
+ # Keying on event_name alone was not enough: every candidate is the SAME event,
63
+ # so they shared a group and cancelled each other, and the re-dispatch cancelled
64
+ # the very run it was asking for. On 2026-09-05 commentary was dispatched four
65
+ # times in thirteen minutes, every run cancelled before it could start on the
66
+ # backlogged pool, and rc-gate/0.1.0-2026-09-05-98e09d1/FAILED was written for a
67
+ # candidate on which nothing had actually failed — while playout, studio and
68
+ # reuters, fast enough to finish inside the window, all went green.
69
+ #
70
+ # So: separate the candidates (client_payload.version — empty on push/PR, which
71
+ # leaves those groups exactly as they were), and do not cancel a dispatch run.
72
+ # A superseded PENDING run is dropped before it starts, fires no
73
+ # report-candidate, and scores nothing — which is why queueing is safe here
74
+ # where cancelling is not. Pushes still cancel pushes.
75
+ #
76
+ # Trade-off, stated: two candidates' validations can now overlap in one repo,
77
+ # and their tiers derive host ports from the same per-slug bands. The bands were
78
+ # moved out of the kernel's ephemeral range (ctl-test-harness 0.1.42); an
79
+ # ephemeral daemon port is the durable answer and is not wired yet.
61
80
  concurrency:
62
- group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
63
- cancel-in-progress: true
81
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ github.event.client_payload.version }}
82
+ cancel-in-progress: ${{ github.event_name != 'repository_dispatch' }}
64
83
 
65
84
  jobs:
66
85
  integration:
@@ -161,3 +161,37 @@ export function jobsMissingTestTempStep(yaml: string): string[] {
161
161
  settle();
162
162
  return bad;
163
163
  }
164
+
165
+ /** A `ctl-candidate` validation that another candidate, or its own retry, can
166
+ * cancel.
167
+ *
168
+ * release-collect scores anything other than a literal success as red — a
169
+ * CANCELLED validation included — and answers a red with one re-dispatch. So a
170
+ * concurrency group shared across candidates makes the gate eat itself:
171
+ * candidate B's dispatch cancels A's in-flight run, A scores red, A retries,
172
+ * and that retry cancels B's run. Observed 2026-09-05: commentary dispatched
173
+ * four times in thirteen minutes, every run cancelled, and
174
+ * `rc-gate/0.1.0-2026-09-05-98e09d1/FAILED` written for a candidate on which
175
+ * nothing had actually failed.
176
+ *
177
+ * Both halves are needed. The group must SEPARATE candidates
178
+ * (`client_payload.version`), so a different candidate never shares one; and a
179
+ * dispatch run must not be cancellable, so a retry queues behind the run it is
180
+ * asking for rather than killing it. A superseded PENDING run is dropped
181
+ * before it starts, so it fires no `report-candidate` and scores nothing —
182
+ * which is why queueing is safe where cancelling is not. */
183
+ export function candidateConcurrencyProblems(yaml: string): string[] {
184
+ const block = yaml.match(/^concurrency:\n((?:[ \t]+.*\n)+)/m)?.[1] ?? "";
185
+ const group = block.match(/^\s*group:\s*(.*)$/m)?.[1] ?? "";
186
+ const cancel = (block.match(/^\s*cancel-in-progress:\s*(.*)$/m)?.[1] ?? "").trim();
187
+ const problems: string[] = [];
188
+ if (!/client_payload\.version/.test(group)) {
189
+ problems.push(
190
+ "group does not separate candidates: a newer candidate cancels an older validation, which scores red",
191
+ );
192
+ }
193
+ if (cancel === "true") {
194
+ problems.push("cancel-in-progress is unconditionally true: the re-dispatch cancels the run it is asking for");
195
+ }
196
+ return problems;
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",
@@ -25,13 +25,13 @@ way resolved defaults (segment durations, as-rendered resolutions, output count)
25
25
  are guarded too, not only literal fields. When a code comment carries one of
26
26
  these rules, cite the ID there too instead of restating the rule.
27
27
 
28
- | ID | Invariant | Guarded by |
29
- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- |
30
- | INV-PLAYLIST-001 | This customer serves legacy playlists only — every composed CMAF playlist is non-low-latency, never LL-HLS | `<slug>/<slug>-invariants.test.ts` |
31
- | INV-SEG-001 | CMAF segment duration is 4s on every rendition (a resolved default — only guardable via the composed graph) | `<slug>/<slug>-invariants.test.ts` |
32
- | INV-OUT-001 | Exactly three delivery rungs in a single named CMAF group, one playlist per rung | `<slug>/<slug>-invariants.test.ts` |
33
- | INV-RES-001 | The delivery resolutions are exactly the ladder the customer signed off (e.g. 1280x720 / 640x360 / 320x180) | `<slug>/<slug>-invariants.test.ts` |
34
- | INV-MOQ-001 | The low-latency return / monitor rides MoQ, never a WHIP/RTMP fallback | `<slug>/<slug>-invariants.test.ts` |
28
+ | ID | Invariant | Guarded by |
29
+ | ---------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- |
30
+ | INV-PLAYLIST-001 | This customer serves legacy playlists only — every composed CMAF playlist is non-low-latency, never LL-HLS | `<slug>/<slug>-invariants.test.ts` |
31
+ | INV-SEG-001 | CMAF segment duration is 4s on every rendition (a resolved default — only guardable via the composed graph) | `<slug>/<slug>-invariants.test.ts` |
32
+ | INV-OUT-001 | Exactly three delivery rungs in a single named CMAF group, one playlist per rung | `<slug>/<slug>-invariants.test.ts` |
33
+ | INV-RES-001 | The delivery resolutions are exactly the ladder the customer signed off (e.g. 1280x720 / 640x360 / 320x180) | `<slug>/<slug>-invariants.test.ts` |
34
+ | INV-MOQ-001 | The low-latency return / monitor rides MoQ, never a WHIP/RTMP fallback | `<slug>/<slug>-invariants.test.ts` |
35
35
 
36
36
  The rows above are worked examples (drawn from the datum commentary deployment);
37
37
  delete them when seeding a new customer.