@openwop/openwop-conformance 2.45.20 → 2.45.22

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,15 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.45.22] — 2026-10-06 — an empty `dataResidency.regions` still binds the reject leg; RFC 0236's registers close
4
+
5
+ - **No scenario change.** RFC 0236 goes `Accepted` on the v2 reference host's certified 2.45.21 cut; its gaps G1 and G2 close, which changes the packed `spec/v1/gaps.json`.
6
+
7
+ - **`v2-data-residency-admission`: an empty `regions` list no longer skips the reject leg.** A host advertising `dataResidency` with no region honours residency nowhere, so it MUST refuse every constraint. Both legs used to skip on `[]`, so a host that accepted and ignored every constraint passed; a double now fails it. The accept leg stays `inapplicable` there.
8
+
9
+ ## [2.45.21] — 2026-10-05 — `0236.webhook.host-variant` gets its own row
10
+
11
+ - **`v2-host-event-delivery`: no-fan-out is its own `it`.** A bundle records one requirement id per `it` (the last one asserted), so the webhook `it` that asserted host-variant and then no-fan-out recorded only no-fan-out. Found by the v2 reference host's 2.45.20 loopback rehearsal: five of the six RFC 0236 ids appeared. The new `it` re-runs the same gate, so a host without the families records the same disposition.
12
+
3
13
  ## [2.45.20] — 2026-10-05 — host events (RFC 0236 Active); `channelPresence` delivery witnessed through the §13 seam at v2
4
14
 
5
15
  - **New `v2-channel-presence-delivery` (seam-gated).** `conversation.md` now restates two RFC 0110 rules: a host advertising `channelPresence` MUST emit it, and MUST NOT deliver it to a non-member. Two legs check them through the snapshot seam, asking as the member first so an unwired seam is never read as a refusal.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.45.20",
4
- "stampSha256": "3fedfa905f02f5fc87e0ecea242b9485a56be19b66f7cb2e04c37d6785888604"
3
+ "version": "2.45.22",
4
+ "stampSha256": "ef94915e768fcf83164913c8fcabb237515e2b7538d2ae93bc7dde8ae7ae79ad"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.45.20",
3
+ "version": "2.45.22",
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.45.20"
59
+ "@openwop/spec-artifacts": "2.45.22"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
5
  "files": 728,
6
- "tests": 2762,
7
- "withStableId": 2762,
6
+ "tests": 2763,
7
+ "withStableId": 2763,
8
8
  "interpolatedTitles": 0,
9
9
  "explicitIds": 2626
10
10
  },
@@ -30103,7 +30103,7 @@
30103
30103
  {
30104
30104
  "id": "openwop.it.v2-data-residency-admission.a-region-outside-dataresidency-regions-answers-422-residency-unavailable-and-cre",
30105
30105
  "file": "v2-data-residency-admission.test.ts",
30106
- "line": 66,
30106
+ "line": 73,
30107
30107
  "title": "a region outside dataResidency.regions answers 422 residency_unavailable and creates no run",
30108
30108
  "explicitId": "openwop.requirement.data-residency.reject-unadvertised-region",
30109
30109
  "citations": [
@@ -30126,7 +30126,7 @@
30126
30126
  {
30127
30127
  "id": "openwop.it.v2-data-residency-admission.a-region-inside-dataresidency-regions-is-accepted",
30128
30128
  "file": "v2-data-residency-admission.test.ts",
30129
- "line": 82,
30129
+ "line": 89,
30130
30130
  "title": "a region inside dataResidency.regions is accepted",
30131
30131
  "explicitId": "openwop.requirement.data-residency.accept-advertised-region",
30132
30132
  "citations": [
@@ -31509,7 +31509,7 @@
31509
31509
  {
31510
31510
  "id": "openwop.it.v2-host-event-delivery.an-emitted-durable-host-event-arrives-on-host-events-as-a-valid-envelope",
31511
31511
  "file": "v2-host-event-delivery.test.ts",
31512
- "line": 122,
31512
+ "line": 131,
31513
31513
  "title": "an emitted durable host event arrives on /host/events as a valid envelope",
31514
31514
  "explicitId": "openwop.requirement.0236.host-event.envelope",
31515
31515
  "citations": [
@@ -31522,7 +31522,7 @@
31522
31522
  {
31523
31523
  "id": "openwop.it.v2-host-event-delivery.an-ephemeral-host-event-has-no-id-and-is-not-redelivered-after-last-event-id",
31524
31524
  "file": "v2-host-event-delivery.test.ts",
31525
- "line": 136,
31525
+ "line": 145,
31526
31526
  "title": "an ephemeral host event has no id: and is not redelivered after Last-Event-ID",
31527
31527
  "explicitId": null,
31528
31528
  "citations": []
@@ -31530,7 +31530,7 @@
31530
31530
  {
31531
31531
  "id": "openwop.it.v2-host-event-delivery.registerwebhook-refuses-an-ephemeral-host-event-type-with-400-validation-error",
31532
31532
  "file": "v2-host-event-delivery.test.ts",
31533
- "line": 153,
31533
+ "line": 162,
31534
31534
  "title": "registerWebhook refuses an ephemeral host-event type with 400 validation_error",
31535
31535
  "explicitId": "openwop.requirement.0236.webhook.ephemeral-refused",
31536
31536
  "citations": [
@@ -31546,10 +31546,10 @@
31546
31546
  ]
31547
31547
  },
31548
31548
  {
31549
- "id": "openwop.it.v2-host-event-delivery.a-subscribed-durable-host-event-is-delivered-as-hostevent-signed-the-ephemeral-o",
31549
+ "id": "openwop.it.v2-host-event-delivery.a-subscribed-durable-host-event-is-delivered-as-hostevent-signed",
31550
31550
  "file": "v2-host-event-delivery.test.ts",
31551
- "line": 166,
31552
- "title": "a subscribed durable host event is delivered as { hostEvent }, signed; the ephemeral one is not",
31551
+ "line": 175,
31552
+ "title": "a subscribed durable host event is delivered as { hostEvent }, signed",
31553
31553
  "explicitId": "openwop.requirement.0236.webhook.host-variant",
31554
31554
  "citations": [
31555
31555
  {
@@ -31559,10 +31559,18 @@
31559
31559
  }
31560
31560
  ]
31561
31561
  },
31562
+ {
31563
+ "id": "openwop.it.v2-host-event-delivery.the-same-subscription-never-receives-the-ephemeral-host-event",
31564
+ "file": "v2-host-event-delivery.test.ts",
31565
+ "line": 211,
31566
+ "title": "the same subscription never receives the ephemeral host event",
31567
+ "explicitId": null,
31568
+ "citations": []
31569
+ },
31562
31570
  {
31563
31571
  "id": "openwop.it.v2-host-event-delivery.a-second-tenant-receives-nothing-for-the-first-tenant-s-host-event",
31564
31572
  "file": "v2-host-event-delivery.test.ts",
31565
- "line": 200,
31573
+ "line": 224,
31566
31574
  "title": "a second tenant receives nothing for the first tenant’s host event",
31567
31575
  "explicitId": null,
31568
31576
  "citations": []
@@ -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.45.20",
5
- "corpusTag": "v2.45.20",
4
+ "version": "2.45.22",
5
+ "corpusTag": "v2.45.22",
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": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "3d62475a6e5d2f0e005aa58b95c197f8846104d8cb7753c2161d7b6948cb89b4",
13
- "api/v2/asyncapi.yaml": "44ea76f380b18c54ccdb51eaa091289212cd044e656c4d97e94df7bf048972e9",
14
- "api/v2/openapi.yaml": "4ebf762aabf5d25318923ac0c8a2b865e11d7e07b2a2456ab6b535d91e56da33",
12
+ "api/seams-v2.yaml": "a3a2dd8f2691fe7fa831af534f41789abaea6e5a03ededf88dd4bc96803cdaea",
13
+ "api/v2/asyncapi.yaml": "587bab6ff008b0bb1d3144029bc7ed4c3badc52ac916a0afd49d6c85923bbe1b",
14
+ "api/v2/openapi.yaml": "33a60f1454fb0efeb22c667ecf505447775a0de84772b1003c3226a6e419685b",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "a829e3208413062388e52ab6ba714d35889c9c7645db38a290b0c4f4992cd3da",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -208,14 +208,14 @@
208
208
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
209
209
  "spec/v1/alias-detectors.json": "0b2f28808ffe0b9d98a187775038c3406bb70919747a788a6e4f866e293ae6d3",
210
210
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
211
- "spec/v1/core-standard-manifest.json": "d7c4865e30b6eed18c932ca0c4e4bbcbcb3f4cfedfdd560876a436d0ed774ba0",
211
+ "spec/v1/core-standard-manifest.json": "b1d45d8fc106392a46ea67fd227bcdf623454ec0499e115487ed4e8dc5647d7f",
212
212
  "spec/v1/deprecations.json": "a3293072ea951857395d05f0437264e88965f1e9019ff61c55bacfa9c1aaa17c",
213
213
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
214
214
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
215
215
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
216
216
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
217
217
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
218
- "spec/v1/gaps.json": "b745aa2620ae8c9f79e7df7bd7c81559b8929f0bdfae62a7ea15debc29e8f19a",
218
+ "spec/v1/gaps.json": "335186074848531561262525967bd85b379b3137a2233dd4c8b63914222dfa71",
219
219
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
220
220
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
221
221
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -311,9 +311,9 @@
311
311
  "spec/v2/path-manifest.json": "b84c7561f50c7df0cb61b202bd6595acad3ee7edca7a45f711949bea3c69bd27",
312
312
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
313
313
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
314
- "spec/v2/release.json": "84c88ee17dd416060ba7d564fb3d47fdb528c4efa9cd8e483749b77557dac816",
314
+ "spec/v2/release.json": "72fec975046a00db77ff1666dc76ada8663cbd18366104e451091e3f82c02455",
315
315
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
316
- "spec/v2/surface-baseline.json": "11600f3b5d689962b34aaa1bdfa4c3e2a47632b914a945b356b825dd494ec9db"
316
+ "spec/v2/surface-baseline.json": "12f193f56af68402c747503b465f8d7e18580ef832ac7b95c218ab06286478ea"
317
317
  },
318
- "corpusCommit": "d6b2495ddf9ac4b82580a9e0f055f29beccfe24a"
318
+ "corpusCommit": "b768269329be09d06da2de25b668ce27d5cc51df"
319
319
  }
@@ -17,9 +17,14 @@
17
17
  * `inapplicable` rather than a pass. Physical confinement is an operator SHOULD
18
18
  * and cannot be observed on the wire.
19
19
  *
20
- * Dispositions: discovery unreadable ⇒ `blocked`; `dataResidency` absent or no
21
- * advertised region ⇒ `inapplicable`; a 429 ⇒ `blocked` (the run budget, not
22
- * the wire).
20
+ * An empty `regions` list still binds the reject leg: the host honours
21
+ * residency in no region, so every constraint MUST be refused. Until 2.45.22
22
+ * both legs skipped on an empty list, so a host advertising no region that
23
+ * silently accepted and ignored every constraint passed.
24
+ *
25
+ * Dispositions: discovery unreadable ⇒ `blocked`; `dataResidency` absent or a
26
+ * malformed `regions` ⇒ `inapplicable`; an empty `regions` ⇒ `inapplicable` for
27
+ * the accept leg only; a 429 ⇒ `blocked` (the run budget, not the wire).
23
28
  *
24
29
  * @see spec/v2/core/runs.md §Refusals
25
30
  * @see spec/v2/core/runs.md §`dataResidency`
@@ -49,9 +54,11 @@ async function advertisedRegions(): Promise<string[] | { disposition: 'blocked'
49
54
  if (!doc) return { disposition: 'blocked', reason: UNREADABLE };
50
55
  const rec = await familyAdvertised('dataResidency');
51
56
  if (!rec) return { disposition: 'inapplicable', reason: 'the host does not advertise dataResidency' };
52
- const regions = Array.isArray(rec['regions']) ? rec['regions'].filter((r): r is string => typeof r === 'string' && r.length > 0) : [];
53
- if (regions.length === 0) return { disposition: 'inapplicable', reason: 'dataResidency advertises no region (regions is empty or malformed; the record leg owns that)' };
54
- return regions;
57
+ // An empty list is a claim, not an absence: the host honours residency in no
58
+ // region, so it MUST refuse every residency constraint. Only a malformed list
59
+ // is left to the record leg.
60
+ if (!Array.isArray(rec['regions'])) return { disposition: 'inapplicable', reason: 'dataResidency.regions is not an array (the record leg owns that)' };
61
+ return rec['regions'].filter((r): r is string => typeof r === 'string' && r.length > 0);
55
62
  }
56
63
 
57
64
  /** Cancel a run the host created, so a failing row leaves nothing behind. */
@@ -82,7 +89,8 @@ describe('v2 dataResidency admission (runs.md §dataResidency)', () => {
82
89
  it('a region inside dataResidency.regions is accepted', async () => {
83
90
  const regions = await advertisedRegions();
84
91
  if (!Array.isArray(regions)) return softSkip(regions.disposition, regions.reason);
85
- const region = regions[0] as string;
92
+ const region = regions[0];
93
+ if (region === undefined) return softSkip('inapplicable', 'dataResidency.regions is empty: there is no advertised region to accept (the reject leg still binds)');
86
94
 
87
95
  const res = await http(() => driver.post('/runs', { workflowId: NOOP, residency: { region } }));
88
96
  if (res === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
@@ -18,7 +18,9 @@
18
18
  * 0236.webhook.host-variant a subscribed durable event arrives as
19
19
  * `{ hostEvent }`, typed and signed;
20
20
  * 0236.ephemeral.no-fan-out the same subscription never receives the
21
- * ephemeral event;
21
+ * ephemeral event (its own `it`, reading what
22
+ * the host-variant leg observed: a bundle
23
+ * records one requirement id per `it`);
22
24
  * 0236.host-event.tenant-isolation a second tenant's stream receives nothing
23
25
  * for the first tenant's event.
24
26
  *
@@ -111,6 +113,13 @@ async function streamWhile(act: () => Promise<void>, opts: { lastEventId?: strin
111
113
  return [...(await sub).events];
112
114
  }
113
115
 
116
+ /**
117
+ * What the host-variant leg observed, for the no-fan-out leg. They are two
118
+ * `it`s because a bundle records one requirement id per `it` (the last one it
119
+ * asserts), so one `it` asserting both ids recorded only no-fan-out (2.45.20).
120
+ */
121
+ let fanOut: { bodies: string[]; ephemeralId: string } | { skipped: string } = { skipped: 'the host-variant leg did not run' };
122
+
114
123
  const registered: string[] = [];
115
124
  let receiver: ScopedReceiver | null = null;
116
125
  afterEach(async () => {
@@ -163,7 +172,8 @@ describe('v2 host events (RFC 0236 — seam-gated)', () => {
163
172
  expect(readErrorCode(res.json), req(ID_REFUSED, 'spec/v2/core/webhooks.md §Surfaces', 'the refusal code MUST be validation_error')).toBe('validation_error');
164
173
  }, 20_000);
165
174
 
166
- it('a subscribed durable host event is delivered as { hostEvent }, signed; the ephemeral one is not', async () => {
175
+ it('a subscribed durable host event is delivered as { hostEvent }, signed', async () => {
176
+ fanOut = { skipped: 'the host-variant leg skipped before observing deliveries' };
167
177
  const g = await gate();
168
178
  if ('kind' in g) return skip(g);
169
179
  if ((await familyAdvertised('webhooks')) === null) return softSkip('inapplicable', 'the host does not advertise webhooks');
@@ -193,10 +203,24 @@ describe('v2 host events (RFC 0236 — seam-gated)', () => {
193
203
  }
194
204
  const h = (name: string): string | undefined => { const v = hit.headers[name]; return Array.isArray(v) ? v[0] : v; };
195
205
  const verified = verifyWebhookDelivery(SECRET, h('openwop-signature') ?? '', h('openwop-signature-algorithm'), h('openwop-timestamp') ?? '', hit.body, createReceiverState()).accepted === true;
206
+ // Everything the subscription received up to the durable delivery, which was emitted after the ephemeral one.
207
+ fanOut = { bodies: hits.map((x) => x.body), ephemeralId };
196
208
  assertAll(ID_HOST_BODY, WDOC, judgeHostBody(hit.body, h('openwop-event-type'), verified, durableId, d.type, webhookDelivery));
197
- assertAll(ID_NO_FAN_OUT, DOC, [judgeNoFanOut(hits.map((x) => x.body), ephemeralId)]);
198
209
  }, 30_000);
199
210
 
211
+ it('the same subscription never receives the ephemeral host event', async () => {
212
+ // The same gate as the host-variant leg, so a host without the families records the same disposition here.
213
+ const g = await gate();
214
+ if ('kind' in g) return skip(g);
215
+ if ((await familyAdvertised('webhooks')) === null) return softSkip('inapplicable', 'the host does not advertise webhooks');
216
+ const d = pick(g.types, 'durable');
217
+ const e = pick(g.types, 'ephemeral');
218
+ if ('kind' in d) return skip(d);
219
+ if ('kind' in e) return skip(e);
220
+ if ('skipped' in fanOut) return softSkip('blocked', `no-fan-out reads the deliveries the host-variant leg observed, and ${fanOut.skipped}`);
221
+ assertAll(ID_NO_FAN_OUT, DOC, [judgeNoFanOut(fanOut.bodies, fanOut.ephemeralId)]);
222
+ });
223
+
200
224
  it('a second tenant receives nothing for the first tenant’s host event', async () => {
201
225
  const g = await gate();
202
226
  if ('kind' in g) return skip(g);