@openwop/openwop-conformance 2.44.1 → 2.44.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.2] — 2026-09-28 — RFC 0215's no-head-of-line leg does not convict a late fan-out, and says when it sampled each count
4
+
5
+ - **The 2.44.2 cycle opens.** A patch: the requirement is unchanged, and one verdict branch that convicted without the contention it names becomes unjudged. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
6
+ - **`0215.no-head-of-line` no longer fails a host whose `run.started` fan-out is late.** The last branch failed the row as "a bounded dispatcher below the floor" whenever fewer than 8 held attempts had arrived by the time the run was terminal. MyndHyve `00821-qec` hit it twice. In the first cut no held attempt had arrived and the healthy one was delivered. In the second, 2 held attempts had arrived at terminal and 7 later, and the healthy attempt started with 7 held open and none closed. §A forbids an attempt waiting for another subscription's attempt to *finish*. None had finished, so the healthy attempt waited for nothing, and the contention §A.2 names was never established. The row is now `blocked` (unjudged) in that case. A bounded pool still fails: its healthy attempt starts only after a held attempt closes, or never while they stay open. So does a serial dispatcher, through the peak assertion.
7
+ - **The detail names each sampling time.** It read "2 held attempt(s) arrived (at most 7 open at once)", with the 2 sampled at terminal and the 7 at the verdict. It now reports what had arrived and was open at terminal, what had arrived by the verdict and the peak, when the healthy attempt arrived, and whether the host had closed any held attempt. It no longer names a cause the data does not show.
8
+ - **The verdict is a pure function with self-tests** (`lib/delivery-isolation-verdict.ts`). The tests cover pass, the contention-present fail, a 7-slot pool with a 15 s timeout (fail), a serial dispatcher (peak assertion), and both MyndHyve cuts (unjudged). Sabotage-checked: removing the new branch fails the two MyndHyve cases.
9
+
3
10
  ## [2.44.1] — 2026-09-28 — the webhook retry legs outlast an advertised maxElapsedMs, and four families get v2 normative homes
4
11
 
5
12
  - **The 2.44.1 cycle opens.** `workspace`, `content`, `triggerBridge` and `uiPlugins` get v2 normative homes (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree (`spec/v2/core/host-services.md`, `i18n.md`, `webhooks.md`, `packs.md`, `spec/v2/declaration.json`) after `v2.44.0` was tagged. No scenario changes: the rules are restated from the v1 text, none added or strengthened.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.1 @openwop/spec-artifacts@2.44.1
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.2 @openwop/spec-artifacts@2.44.2
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.44.1",
4
- "stampSha256": "c9a9550d05910b204589704d1e2c83fac85656062d8374a94e5afedf00b011e3"
3
+ "version": "2.44.2",
4
+ "stampSha256": "66d1059ab9f4e5799bbb84c8b66e64033bc08f4daf6016c22c71164cca948c4c"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.44.1",
3
+ "version": "2.44.2",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.44.1"
59
+ "@openwop/spec-artifacts": "2.44.2"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -34246,7 +34246,7 @@
34246
34246
  {
34247
34247
  "id": "openwop.requirement.0215.no-head-of-line",
34248
34248
  "file": "v2-webhook-delivery-isolation.test.ts",
34249
- "line": 113,
34249
+ "line": 119,
34250
34250
  "title": null,
34251
34251
  "explicitId": "openwop.requirement.0215.no-head-of-line",
34252
34252
  "citations": [
@@ -34273,11 +34273,6 @@
34273
34273
  "requirement": null,
34274
34274
  "interpolated": true
34275
34275
  },
34276
- {
34277
- "section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
34278
- "requirement": null,
34279
- "interpolated": true
34280
- },
34281
34276
  {
34282
34277
  "section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
34283
34278
  "requirement": null,
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.44.1",
5
- "corpusTag": "v2.44.1",
4
+ "version": "2.44.2",
5
+ "corpusTag": "v2.44.2",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "6f9780f7751804b6dd9f57cb5474728326de805fcca667979f6b4f39c9c97336",
13
- "api/v2/asyncapi.yaml": "ec6520792f7c4661403053003de7669430178597b5e0250ba5264b65dc82dbef",
14
- "api/v2/openapi.yaml": "46314e02c9aeb4cb7a4ca0cfd5997dc24bd11c3971341e505cd0b7bef3150cd8",
12
+ "api/seams-v2.yaml": "f24127f3cd544f1c5a942859ffc492b438d37b1b7d7a660c631fb87f70cd24f6",
13
+ "api/v2/asyncapi.yaml": "3283ff1bbe962dc75e6d66d2b755d4eb7d53770ce8511f2d671b923d6ca0e51a",
14
+ "api/v2/openapi.yaml": "4a455ba22c81718e2842adce60e5d3c5970e6f8a55b301d9154ccc8060f6cf91",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,7 +204,7 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "b6efb5d51e1432088bfeff049f310ae0095f3852a59e877e9d628a30782f5580",
207
+ "spec/v1/core-standard-manifest.json": "3d7fd99cd02153094723c2bf383d757c282e653526aa3b199d9bdb4b9cfd7c80",
208
208
  "spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -299,9 +299,9 @@
299
299
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
300
300
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
301
301
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
302
- "spec/v2/release.json": "af448fd044ab5b990a21945905f109dbbb9b0df9ab7f1105e79aa7d1e0f35a33",
302
+ "spec/v2/release.json": "28b407019929da55701a718110d440f08899646af0b74d62f6adc5542c9c1c91",
303
303
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
304
- "spec/v2/surface-baseline.json": "2f79e3283946dd8a401db039109d403b9c386e6b91a4780cbcb1440e8d3a6ffe"
304
+ "spec/v2/surface-baseline.json": "d0540276e74001f5058dd81909100ea6ee3c1925c6ac02918418f5ec941d0661"
305
305
  },
306
- "corpusCommit": "bf414c875dba212893f0230d44f2b4a2e226311d"
306
+ "corpusCommit": "c486eafcfa16908a7b57c98b33cfd66fdf8e599d"
307
307
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The verdict of RFC 0215's `no-head-of-line` leg, as a pure function of what
3
+ * the leg observed (suite 2.44.2), so the rule can be tested without a host.
4
+ *
5
+ * 2.44.2 fixes two defects that MyndHyve's 2.44.x cuts surfaced:
6
+ *
7
+ * 1. **A conviction without the contention it names.** The last branch failed
8
+ * the row as "a bounded dispatcher below the floor" whenever fewer than
9
+ * `floor` held attempts had arrived by the time the run was terminal. On a
10
+ * host whose `run.started` fan-out arrived late, that convicted a healthy
11
+ * attempt that started while every held attempt that HAD arrived was still
12
+ * open and none had finished (MyndHyve `00821-qec`: 0 held attempts, then 2
13
+ * at terminal and 7 later, the healthy attempt delivered with 7 open). §A
14
+ * forbids an attempt waiting for another subscription's attempt to FINISH; no
15
+ * attempt had finished, so there was nothing it could have waited for. The
16
+ * contention was never established, so the row is unjudged, not failed. A
17
+ * bounded pool still fails: its healthy attempt starts only after a held
18
+ * attempt closes, or never while they stay open.
19
+ *
20
+ * 2. **A detail string that mixed two sampling times.** It read "2 held
21
+ * attempt(s) arrived (at most 7 open at once)": the 2 was sampled at
22
+ * terminal, the 7 at the verdict. The detail now names each time.
23
+ */
24
+
25
+ export interface IsolationObservation {
26
+ /** §A.2's floor. */
27
+ readonly floor: number;
28
+ /** When the leg saw the run terminal (the healthy delivery is due by then). */
29
+ readonly terminalAt: number;
30
+ /** When the healthy attempt arrived, or null if it did not within the window. */
31
+ readonly healthyAt: number | null;
32
+ /** Held attempts open when the healthy attempt arrived (-1 if it did not). */
33
+ readonly openAtHealthy: number;
34
+ /** Held attempts that had arrived by `terminalAt`. */
35
+ readonly arrivedAtTerminal: number;
36
+ /** Held attempts still open at `terminalAt`. */
37
+ readonly openAtTerminal: number;
38
+ /** Held attempts that had arrived by the verdict. */
39
+ readonly arrivedByVerdict: number;
40
+ /** The most held attempts open at once, up to the verdict. */
41
+ readonly peakOpen: number;
42
+ /** When the first held attempt closed (the host abandoning it), or Infinity. */
43
+ readonly earliestClose: number;
44
+ readonly windowMs: number;
45
+ readonly delayMs: number;
46
+ }
47
+
48
+ export type IsolationVerdict =
49
+ /** The healthy attempt started while `floor` held attempts were open. */
50
+ | { readonly kind: 'pass'; readonly detail: string }
51
+ | { readonly kind: 'fail'; readonly message: string; readonly detail: string }
52
+ /** Unjudged, after asserting `peakOpen >= floor` (which fails a pool below the floor). */
53
+ | { readonly kind: 'blocked-after-peak'; readonly message: string; readonly detail: string }
54
+ | { readonly kind: 'blocked'; readonly message: string; readonly detail: string };
55
+
56
+ /** What the leg saw, each count against the time it was sampled. */
57
+ export function isolationDetail(o: IsolationObservation): string {
58
+ const at = (t: number): string => `${t - o.terminalAt}ms after the run was seen terminal`;
59
+ const healthy = o.healthyAt === null
60
+ ? `the healthy attempt did not arrive within ${o.windowMs}ms of that`
61
+ : `the healthy attempt arrived ${at(o.healthyAt)}, with ${o.openAtHealthy} held open`;
62
+ const closed = Number.isFinite(o.earliestClose) ? `the first held attempt was closed by the host ${at(o.earliestClose)}` : 'no held attempt was closed by the host';
63
+ return `at terminal, ${o.arrivedAtTerminal} held attempt(s) had arrived and ${o.openAtTerminal} were open; by the verdict, ${o.arrivedByVerdict} had arrived, at most ${o.peakOpen} open at once; ${healthy}; ${closed}`;
64
+ }
65
+
66
+ export function isolationVerdict(o: IsolationObservation): IsolationVerdict {
67
+ const detail = isolationDetail(o);
68
+ const F = o.floor;
69
+ if (o.healthyAt !== null && o.openAtHealthy >= F) return { kind: 'pass', detail };
70
+ if (o.openAtTerminal >= F) {
71
+ // The healthy attempt waited (or never came), and the contention §A.2
72
+ // names was present when it fell due.
73
+ return { kind: 'fail', message: `with ${F} subscriptions' attempts unanswered, a further subscription's attempt MUST still start; it waited for the host to release one`, detail };
74
+ }
75
+ if (o.arrivedAtTerminal >= F || o.earliestClose < o.terminalAt) {
76
+ // Contention was not sustained to the due time. Either the host's own
77
+ // delivery timeout released the held attempts early (its choice), or it
78
+ // never opened F of them at once. peakOpen separates the two: below F the
79
+ // host never had F attempts outstanding at once, which is the defect.
80
+ const released = Number.isFinite(o.earliestClose) ? `${o.terminalAt - o.earliestClose}ms ` : '';
81
+ return { kind: 'blocked-after-peak', message: `unjudged: ${F} held attempts were open at once, but the host closed held attempts ${released}before the run was terminal (its delivery timeout is shorter than this leg's ${o.delayMs}ms delay), so ${F} attempts were not outstanding when the healthy delivery fell due`, detail };
82
+ }
83
+ if (o.healthyAt !== null && !(o.earliestClose < o.healthyAt)) {
84
+ // Fewer than F held attempts had arrived when the healthy delivery fell
85
+ // due, and it started before any held attempt finished: it cannot have
86
+ // waited for one. The held fan-out was late, so the contention the floor
87
+ // names never existed when it mattered.
88
+ return { kind: 'blocked', message: `unjudged: only ${o.arrivedAtTerminal} of the ${F} held attempts had arrived when the healthy delivery fell due, and the healthy attempt started before any held attempt finished, so it did not wait for one; the held run.started deliveries arrived late, and the contention §A.2 names was not established`, detail };
89
+ }
90
+ return {
91
+ kind: 'fail',
92
+ message: o.healthyAt === null
93
+ ? `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt did not start within ${o.windowMs}ms while the held attempts stayed open`
94
+ : `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt started only after a held attempt finished`,
95
+ detail,
96
+ };
97
+ }
@@ -26,6 +26,11 @@
26
26
  * host released one to make room), or never within the window, AND the held
27
27
  * attempts had stayed open until the run was terminal: the contention the
28
28
  * floor names existed when the healthy delivery fell due, and it waited.
29
+ * - blocked (unjudged, suite 2.44.2) — fewer than 8 held attempts had arrived
30
+ * when the healthy delivery fell due, and the healthy attempt started before
31
+ * any held attempt finished: it waited for nothing, and the contention was
32
+ * never established (a late `run.started` fan-out). The rule and its tests
33
+ * are in `lib/delivery-isolation-verdict.ts`.
29
34
  * - blocked (unjudged) — the host had all 8 held attempts open at once, but
30
35
  * its own delivery timeout closed them before the run was terminal.
31
36
  * Contention was not sustained when the healthy delivery fell due, so this
@@ -56,6 +61,7 @@ import { readErrorCode } from '../lib/error-envelope.js';
56
61
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
57
62
  import { req } from '../lib/requirement-ids.js';
58
63
  import { RETRY_WAIT_FLOOR_MS } from '../lib/webhook-retry-window.js';
64
+ import { isolationVerdict } from '../lib/delivery-isolation-verdict.js';
59
65
 
60
66
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs webhook deliveries to nine suite-owned subscriptions on the scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL, eight of which the suite holds open';
61
67
 
@@ -179,44 +185,30 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
179
185
  const terminalAt = await waitTerminal(runId, DELAY_MS + 30_000);
180
186
  if (terminalAt === null) return blockedDespiteAssertions(`${FIXTURE} run ${runId} did not reach a terminal status — the healthy delivery never fell due`);
181
187
  const openAtTerminal = openCount();
182
- const everOpened = held.size;
188
+ const arrivedAtTerminal = held.size;
183
189
  await waitFor(() => healthyAt !== null, WINDOW_MS);
184
190
  const earliestClose = Math.min(...[...held.values()].map((h) => h.closedAt ?? Infinity));
185
- const detail = `${everOpened} held attempt(s) arrived (at most ${peakOpen} open at once); ${openAtTerminal} were still open when the run was seen terminal; `
186
- + (healthyAt === null ? `the healthy attempt did not arrive within ${WINDOW_MS}ms of that` : `the healthy attempt arrived ${healthyAt - terminalAt}ms after it, with ${openAtHealthy} held open`);
187
191
 
188
- if (healthyAt === null && everOpened === 0) {
192
+ if (healthyAt === null && held.size === 0) {
189
193
  if (absenceIsUnmeasured(rx)) return blockedDespiteAssertions(noDeliveryCause(rx, 'webhook attempt'));
190
194
  return blockedDespiteAssertions(`no attempt for any of the ${FLOOR + 1} subscriptions arrived — ${noDeliveryCause(rx, 'webhook attempt')}`);
191
195
  }
192
- if (healthyAt !== null && openAtHealthy >= FLOOR) {
193
- expect(openAtHealthy, req(ID, DOC, `an attempt to one subscription MUST NOT wait for attempts to other subscriptions — ${detail}`)).toBeGreaterThanOrEqual(FLOOR);
194
- } else if (openAtTerminal >= FLOOR) {
195
- // The healthy attempt waited (or never came), and the contention §A.2
196
- // names was present when it fell due.
197
- expect.fail(req(ID, DOC, `with ${FLOOR} subscriptions' attempts unanswered, a further subscription's attempt MUST still start; it waited for the host to release one — ${detail}`));
198
- } else if (everOpened >= FLOOR || earliestClose < terminalAt) {
199
- // Contention was not sustained to the due time. Either the host's own
200
- // delivery timeout released the held attempts early (its choice), or it
201
- // never opened FLOOR of them at once. The second is the defect, the first
202
- // is not, and they differ in WHEN the first held attempt closed.
203
- //
204
- // Unfailable-leg audit (2026-09-26): this branch asserted everOpened > 0
205
- // and then `softSkip('skipped')`, which records `executed-pass` with a
206
- // `partial-witness:` detail. A serial dispatcher (pool of 1) with a
207
- // sub-2s delivery timeout landed here and passed — every held attempt
208
- // arrived eventually (everOpened reached FLOOR), one at a time, while the
209
- // healthy attempt queued behind them: head-of-line blocking. peakOpen
210
- // separates the two cases the comment above names: below FLOOR the host
211
- // never had FLOOR attempts outstanding at once, which is the defect; at
212
- // FLOOR the host's timeout released them early, which is unjudged.
196
+ const v = isolationVerdict({
197
+ floor: FLOOR, terminalAt, healthyAt, openAtHealthy, arrivedAtTerminal, openAtTerminal,
198
+ arrivedByVerdict: held.size, peakOpen, earliestClose, windowMs: WINDOW_MS, delayMs: DELAY_MS,
199
+ });
200
+ if (v.kind === 'blocked') return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
201
+ if (v.kind === 'blocked-after-peak') {
202
+ // Unfailable-leg audit (2026-09-26): a serial dispatcher (pool of 1)
203
+ // with a sub-2s delivery timeout lands here; below FLOOR at once it
204
+ // fails, at FLOOR the host's timeout released them early (unjudged).
213
205
  expect(
214
206
  peakOpen,
215
- req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${detail}`),
207
+ req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${v.detail}`),
216
208
  ).toBeGreaterThanOrEqual(FLOOR);
217
- return blockedDespiteAssertions(`unjudged: ${FLOOR} held attempts were open at once, but the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
218
- } else {
219
- expect.fail(req(ID, DOC, `the host never had ${FLOOR} subscriptions' attempts outstanding at once, and the healthy attempt waited — a bounded dispatcher below the floor — ${detail}`));
209
+ return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
220
210
  }
211
+ if (v.kind === 'fail') expect.fail(req(ID, DOC, `${v.message} — ${v.detail}`));
212
+ expect(openAtHealthy, req(ID, DOC, `an attempt to one subscription MUST NOT wait for attempts to other subscriptions — ${v.detail}`)).toBeGreaterThanOrEqual(FLOOR);
221
213
  }, DELAY_MS + 30_000 + RETRY_WAIT_FLOOR_MS + 30_000);
222
214
  });