@openwop/openwop-conformance 2.0.2 → 2.0.4

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,25 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.0.4] — 2026-09-06 — a fixture that punished a host for obeying the rule the scenario checks
4
+
5
+ `v2-provider-conflict` drove `connection-pack-github`. On a host shipping a built-in `github`, RFC 0177 §D.1 requires refusing the later registration — so the fixture never installed and the qualified-form leg recorded **`blocked`** permanently, on a host whose only fault was obeying the rule under test. A production host carried that row across a dozen cuts; any blocked row denies certification (RFC 0168 §E.1).
6
+
7
+ New fixture **`connection-pack-acme-widgets`** — same shape, a deliberately fictional provider id so no host ships it built-in and both legs are reachable everywhere.
8
+
9
+ The v1 scenario `connection-provider-resolution` keeps the `github` fixture, checked rather than assumed: v1 settles a collision by **version precedence** (`spec/v1/connection-packs.md:89`) instead of refusing the install, so it is not trapped by the same choice.
10
+
11
+ **An instrument must not require a host to violate the spec in order to be measured.**
12
+
13
+ ## [2.0.3] — 2026-09-06 — a selector that asked the wrong question, and a window that convicted a host for its own width
14
+
15
+ **`v2-effect-seam-no-refire` selected on a branch permission to witness a replay obligation.** The filter was `guarded === true && branchReFires === false`. `replay.md:78`: *"A host MAY suppress branch effects and MUST NOT report that as replay suppression."* Replay suppression is unconditional (§Suppression rule 1) and does not vary with `branchReFires`, which states only what a **branch** re-fires by design. Every `guarded: true` row is a valid target; the filter now selects on `guarded` alone. `branchReFires` is also optional, so `=== false` additionally excluded rows merely silent on it. Found by the reference host, whose ten `branchReFires: true` rows are all honest and which was about to build `fireEffectSeam` for a scenario that would have kept recording `inapplicable` afterwards.
16
+
17
+ **`v2-webhook-durable-delivery`'s at-least-once leg convicted a host for the suite's window.** The 204 arrives on attempt `FAIL_FIRST + 1`, costing the **sum** of the first `FAIL_FIRST` backoff intervals — `30 + 60 = 90 s` on exponential-from-30s, against a cap of exactly `90_000`. The interval is not advertised (`retryPolicy` is `additionalProperties: false` over `{ maxAttempts, backoff }`), so the wait is underivable and any cap is a guess. The leg now records **`blocked`** with the arithmetic, not `executed-fail`.
18
+
19
+ **The cost:** a host that retries forever and never succeeds also records `blocked` now. That is a missed detection traded for a false conviction, and it returns only by advertising the interval — an RFC and a 2.1.0.
20
+
21
+ Both defects encoded a condition the obligation does not have: one a flag the requirement never mentions, one a deadline it never sets.
22
+
3
23
  ## [2.0.2] — 2026-09-06 — 2.0.1's own fix could not run: the derived wait exceeded the harness timeout that governs it
4
24
 
5
25
  A regression in 2.0.1, measured by the host that reported the defect 2.0.1 fixed. On a 2.0.1 re-cut one row moved, the wrong way: `0173.webhook-durable-delivery.dead-letter` went `executed-pass` → `executed-fail`.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.2",
4
- "stampSha256": "ae8640bf298cdf8a4efd624f16424ed1feba41be0f3f919f859bf5a5bbe0432f"
3
+ "version": "2.0.4",
4
+ "stampSha256": "8abb52df9340e47ac5d16d15fb0abe88cc3bd6857e2cb8be269079d6d7ca1530"
5
5
  }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "core.openwop.connections.acme-widgets",
3
+ "version": "1.0.0",
4
+ "kind": "connection",
5
+ "engines": { "openwop": ">=1.0.0" },
6
+ "provider": {
7
+ "id": "acme-widgets",
8
+ "displayName": "Acme Widgets",
9
+ "category": "dev",
10
+ "auth": {
11
+ "kind": "oauth2",
12
+ "authFlow": "pkce",
13
+ "scopeModel": "groups",
14
+ "endpoints": {
15
+ "authorize": "https://auth.acme-widgets.example.com/oauth/authorize",
16
+ "token": "https://auth.acme-widgets.example.com/oauth/token"
17
+ },
18
+ "scopes": {
19
+ "read": [
20
+ { "key": "widget.read", "label": "Read widgets", "scopes": ["widget:read"] }
21
+ ],
22
+ "write": [
23
+ { "key": "widget.write", "label": "Write widgets", "scopes": ["widget:write"] }
24
+ ]
25
+ }
26
+ },
27
+ "reach": { "mcp": { "server": { "url": "https://mcp.acme-widgets.example.com/mcp/", "transport": "http" } } },
28
+ "consumerNodes": ["core.openwop.mcp.invoke-tool", "core.openwop.mcp.read-resource"],
29
+ "docsUrl": "https://docs.acme-widgets.example.com/oauth"
30
+ }
31
+ }
package/fixtures.md CHANGED
@@ -516,7 +516,8 @@ The `fixtures/connection-packs/` sub-directory holds canonical connection-pack m
516
516
 
517
517
  | Fixture | `provider.id` | Purpose |
518
518
  | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
519
- | `connection-pack-github` | `github` | Canonical positive manifest (oauth2 + pkce, read/write scope groups, MCP reach, the exempt `provider.auth.endpoints.token` endpoint URL). Drives manifest-valid, no-credential-material, reach-exclusive, provider-resolution, and write-reconsent scenarios. |
519
+ | `connection-pack-github` | `github` | Canonical positive manifest (oauth2 + pkce, read/write scope groups, MCP reach, the exempt `provider.auth.endpoints.token` endpoint URL). Drives manifest-valid, no-credential-material, reach-exclusive, provider-resolution, and write-reconsent scenarios. **v1 only for the resolution leg:** v1 settles a collision with a built-in by version precedence (`spec/v1/connection-packs.md:89`), so a host shipping a built-in `github` still installs this pack. |
520
+ | `connection-pack-acme-widgets` | `acme-widgets` | Same shape as the `github` pack over a **deliberately fictional** provider. Drives `v2-provider-conflict` (RFC 0177 §D.1). The id is fictional so that no host ships it built-in: at v2 the later registration of a bare id MUST be refused, so a real-world id made the fixture uninstallable on any host that shipped it — and the qualified-form leg recorded `blocked` on a host whose only fault was obeying §D.1. Suite 2.0.4. |
520
521
  | `connection-pack-apihosts-valid` | `meta-ads` | Canonical positive `openapi`-reach manifest declaring a `provider.apiHosts` credential-egress allow-list (`["facebook.com"]`) — RFC 0120. Drives `connection-pack-apihosts` (the `apiHosts` schema + conditional-MUST + egress allow-list scenario). Negatives (IP/wildcard/port/single-label/uppercase entries, openapi-without-apiHosts) are inline mutations per suite convention. |
521
522
 
522
523
  Negative manifests (credential material, mixed kinds, dual reach) are inline test data in the scenario files per suite convention — a deliberately-invalid fixture file would fail the automatic `fixtures-valid.test.ts` sweep.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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.0.2"
59
+ "@openwop/spec-artifacts": "2.0.4"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -26623,7 +26623,7 @@
26623
26623
  "explicitId": "openwop.requirement.0173.effect-seam-no-refire",
26624
26624
  "citations": [
26625
26625
  {
26626
- "section": "spec/v2/core/replay.md §The effect-seam manifest",
26626
+ "section": "spec/v2/core/replay.md §Suppression",
26627
26627
  "requirement": null,
26628
26628
  "interpolated": true
26629
26629
  }
@@ -27965,7 +27965,7 @@
27965
27965
  {
27966
27966
  "id": "openwop.it.v2-provider-conflict.two-definitions-of-one-bare-provider-id-fail-closed-the-later-registration-is-re",
27967
27967
  "file": "v2-provider-conflict.test.ts",
27968
- "line": 84,
27968
+ "line": 99,
27969
27969
  "title": "two definitions of one bare provider id fail closed: the later registration is refused with connection_provider_conflict",
27970
27970
  "explicitId": "openwop.requirement.0177.provider-conflict.fail-closed",
27971
27971
  "citations": [
@@ -27987,10 +27987,10 @@
27987
27987
  ]
27988
27988
  },
27989
27989
  {
27990
- "id": "openwop.it.v2-provider-conflict.the-qualified-form-packname-github-resolves-to-the-named-pack-s-definition",
27990
+ "id": "openwop.it.v2-provider-conflict.the-qualified-form-packname-acme-widgets-resolves-to-the-named-pack-s-definition",
27991
27991
  "file": "v2-provider-conflict.test.ts",
27992
- "line": 102,
27993
- "title": "the qualified form <packName>#github resolves to the named pack's definition",
27992
+ "line": 120,
27993
+ "title": "the qualified form <packName>#acme-widgets resolves to the named pack's definition",
27994
27994
  "explicitId": "openwop.requirement.0177.provider-conflict.qualified-form",
27995
27995
  "citations": [
27996
27996
  {
@@ -29006,7 +29006,7 @@
29006
29006
  {
29007
29007
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
29008
29008
  "file": "v2-webhook-durable-delivery.test.ts",
29009
- "line": 221,
29009
+ "line": 232,
29010
29010
  "title": "a failed attempt is retried and the event is delivered at least once",
29011
29011
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
29012
29012
  "citations": [
@@ -29023,10 +29023,6 @@
29023
29023
  "requirement": null,
29024
29024
  "interpolated": true
29025
29025
  },
29026
- {
29027
- "section": "webhooks.md §Durability",
29028
- "requirement": "at-least-once: after the failing attempts the retry MUST land (the receiver answered 204 to the third attempt for the key)"
29029
- },
29030
29026
  {
29031
29027
  "section": "webhooks.md §Durability",
29032
29028
  "requirement": null,
@@ -29041,7 +29037,7 @@
29041
29037
  {
29042
29038
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
29043
29039
  "file": "v2-webhook-durable-delivery.test.ts",
29044
- "line": 275,
29040
+ "line": 313,
29045
29041
  "title": "an exhausted delivery is dead-lettered, never dropped",
29046
29042
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
29047
29043
  "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.0.2",
5
- "corpusTag": "v2.0.2",
4
+ "version": "2.0.4",
5
+ "corpusTag": "v2.0.4",
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": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "95eccbef6e1f7f51417efe2f307d8950bb520d3b86806bf9d1a316e96aa6b9b5",
13
- "api/v2/asyncapi.yaml": "70d1a50b3acdf51763239463efe0336fc80b4eb2f9b4ab50d05860ead0490cf0",
14
- "api/v2/openapi.yaml": "e55547a384bb43f8fe21ae93a27dfc7a16df6494f5ae50aca60028968ce039d4",
12
+ "api/seams-v2.yaml": "7298eee975d901d9356e8893c39d43261d74a76a07955fffa5b21fb9ffdadf1f",
13
+ "api/v2/asyncapi.yaml": "98f304626ae95d8fef4de49ace6a4422104faf6c5348bff56ce5aafe511c1381",
14
+ "api/v2/openapi.yaml": "a6ace2a7b2f9adce2374a6a76fe8b62c6cb2530be5689b2343d8876733e9a54c",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -199,7 +199,7 @@
199
199
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
200
200
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
201
201
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
202
- "spec/v1/core-standard-manifest.json": "1afaf3936e9d13d5fb71a83fe63a81f8bbb18efb7417c60e523cebf63a5060e7",
202
+ "spec/v1/core-standard-manifest.json": "04f279931a6749086cf24131671a48f9d119eca4c3a611d2c1e19cc909ac4101",
203
203
  "spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
204
204
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
205
205
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -230,8 +230,8 @@
230
230
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
231
231
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
232
232
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
233
- "spec/v2/release.json": "fea7f9b757b226e263b7626d1bfebfb680e7ea853cca86b2426c82435e8fdbd4",
233
+ "spec/v2/release.json": "2c18f3bb246203a2de3eef9c1e433e10f548ae74d4689c2cd8eb86a2ae14b39b",
234
234
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
235
235
  },
236
- "corpusCommit": "7cf764b47ef3b79ddcf5c6e5dfea9f37f9b60a66"
236
+ "corpusCommit": "87038a29abb5a19f8eb01bece101fe74af81a7e5"
237
237
  }
@@ -56,8 +56,27 @@ describe('RFC 0173 §C.2 — effect-seam-no-refire (gated on replay, seam-driven
56
56
  const manifest = await driver.get(MANIFEST_PATH);
57
57
  const seamRows: SeamRow[] = manifest.status === 200 && manifest.json && typeof manifest.json === 'object'
58
58
  ? ((manifest.json as Manifest).seams ?? []) : [];
59
- const target = seamRows.find((r) => r.guarded === true && r.branchReFires === false);
60
- if (!target) return softSkip('inapplicable', `no manifest row is both guarded and branchReFires: false — nothing to witness suppression on (${seamRows.length} row(s) at ${MANIFEST_PATH})`);
59
+ // Select ANY guarded row (suite 2.0.3). Until 2.0.2 this read
60
+ // `r.guarded === true && r.branchReFires === false`it selected on a
61
+ // BRANCH permission to witness a REPLAY obligation, which `replay.md:78`
62
+ // forbids in one sentence: "A host MAY suppress branch effects and MUST NOT
63
+ // report that as replay suppression." §Suppression rule 1 is unconditional
64
+ // ("a node that performs an external side effect … MUST NOT perform it")
65
+ // and does not vary with `branchReFires`, whose own schema description is
66
+ // "RFC 0140 G6 — a `branch` fork re-fires this seam by design".
67
+ //
68
+ // Two defects in one line. `branchReFires` is also OPTIONAL (`required:
69
+ // ["seam","kind","guarded","guardedBy"]`), so `=== false` additionally
70
+ // excluded every row that is merely silent on the permission.
71
+ //
72
+ // Found by the reference host, which had ten honest `branchReFires: true`
73
+ // rows — every seam it owns does re-fire on a branch, by design — and was
74
+ // about to build `fireEffectSeam` to satisfy a scenario that would have
75
+ // gone on recording `inapplicable` after the work landed. It asked instead
76
+ // of inventing a `false` row, which would have made this pass by lying
77
+ // about the seam.
78
+ const target = seamRows.find((r) => r.guarded === true);
79
+ if (!target) return softSkip('inapplicable', `no manifest row is guarded — nothing to witness suppression on (${seamRows.length} row(s) at ${MANIFEST_PATH})`);
61
80
  const fired = await driver.post(`${SEAMS_PREFIX}/sample/effect-seams/fire`, { seam: String(target.seam) });
62
81
  if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
63
82
  return softSkip('blocked', `the host advertises the seams profile but does not serve ${SEAMS_PREFIX}/sample/effect-seams/fire (answered ${fired.status}) — the no-re-fire leg cannot be driven`);
@@ -98,7 +117,7 @@ describe('RFC 0173 §C.2 — effect-seam-no-refire (gated on replay, seam-driven
98
117
  if (forkEffects.status !== 200) return softSkip('blocked', `GET /runs/{runId}/effects answered ${forkEffects.status} on the replay fork`);
99
118
  expect(
100
119
  countOf(forkEffects),
101
- req('openwop.requirement.0173.effect-seam-no-refire', 'spec/v2/core/replay.md §The effect-seam manifest', `seam ${String(target.seam)} states branchReFires: false, so a replay fork MUST NOT issue a further attempt through it — the fork's effect ledger (${countOf(forkEffects)}) cannot exceed the parent's (${countOf(parentEffects)})`),
120
+ req('openwop.requirement.0173.effect-seam-no-refire', 'spec/v2/core/replay.md §Suppression', `seam ${String(target.seam)} is guarded, so a replay fork MUST NOT issue a further attempt through it — suppression is unconditional for mode: replay (§Suppression rule 1) and does not depend on branchReFires, which states only what a BRANCH may re-fire (§Branch: a host "MUST NOT report that as replay suppression"). The fork's effect ledger (${countOf(forkEffects)}) cannot exceed the parent's (${countOf(parentEffects)})`),
102
121
  ).toBeLessThanOrEqual(countOf(parentEffects));
103
122
  });
104
123
  });
@@ -15,11 +15,26 @@
15
15
  * to `/conformance/seams/sample/…` under target major 2), gated on
16
16
  * `connections.packsSupported`.
17
17
  *
18
- * 1. install the `connection-pack-github` fixture (engines rewritten to admit
19
- * major 2), then a second pack claiming bare `github` → the later one is
20
- * refused with connection_provider_conflict. A host that ships a built-in
21
- * `github` witnesses the same rule on the FIRST install.
22
- * 2. `resolve { provider: "<packName>#github" }` → resolved, source: pack.
18
+ * 1. install the `connection-pack-acme-widgets` fixture (engines rewritten to
19
+ * admit major 2), then a second pack claiming bare `acme-widgets` → the
20
+ * later one is refused with connection_provider_conflict. A host that
21
+ * happens to ship a built-in of the same id witnesses the same rule on the
22
+ * FIRST install.
23
+ * 2. `resolve { provider: "<packName>#acme-widgets" }` → resolved, source: pack.
24
+ *
25
+ * **Why a fictional provider id (suite 2.0.4).** Until 2.0.3 this drove
26
+ * `connection-pack-github`, and a host that ships a built-in `github` could
27
+ * never witness leg 2: §D.1 says the later registration of a bare id MUST be
28
+ * refused, so the fixture did not install, and the qualified-form leg recorded
29
+ * `blocked` — permanently, on a host whose ONLY fault was obeying the rule the
30
+ * scenario exists to check. Measured on a production host, which carried that
31
+ * blocked row across a dozen cuts; a bundle with any blocked row does not
32
+ * certify (RFC 0168 §E.1). `acme-widgets` is fictional precisely so no host
33
+ * ships it built-in and the fixture always installs. The v1 scenario
34
+ * `connection-provider-resolution` keeps the `github` fixture: v1 resolves a
35
+ * collision by VERSION PRECEDENCE (`spec/v1/connection-packs.md:89`) rather
36
+ * than refusing the install, so it is not trapped by the same choice — checked
37
+ * rather than assumed.
23
38
  *
24
39
  * @see RFCS/0177-v2-registry-packs-and-extension-tail.md §D.1
25
40
  * @see spec/v2/core/connection-packs.md
@@ -38,7 +53,7 @@ import { targetMajor } from '../lib/seams.js';
38
53
  import { v2Discovery, familyAdvertised } from '../lib/v2.js';
39
54
 
40
55
  const SECTION = 'connection-packs.md §"Provider identity" (RFC 0177 §D.1)';
41
- const FIXTURE = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
56
+ const FIXTURE = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-acme-widgets.json');
42
57
  const INSTALL = '/v1/host/sample/connection-packs/install';
43
58
  const RESOLVE = '/v1/host/sample/connection-packs/resolve';
44
59
 
@@ -69,7 +84,7 @@ async function preflight(): Promise<{ kind: SoftSkipKind; reason: string } | nul
69
84
  if (!doc) return { kind: 'blocked', reason: 'discovery unreachable — /.well-known/openwop (OpenWOP-Version: 2.0) did not answer 200 JSON' };
70
85
  const connections = await familyAdvertised('connections');
71
86
  if (!behaviorGate('connections.packsSupported', connections?.['packsSupported'] === true)) return { kind: 'inapplicable', reason: 'v2 discovery does not advertise connections.packsSupported (RFC 0095 §C)' };
72
- if (!existsSync(FIXTURE)) return { kind: 'blocked', reason: 'fixture connection-packs/connection-pack-github.json is absent from this layout' };
87
+ if (!existsSync(FIXTURE)) return { kind: 'blocked', reason: 'fixture connection-packs/connection-pack-acme-widgets.json is absent from this layout' };
73
88
  return null;
74
89
  }
75
90
 
@@ -86,30 +101,33 @@ describe('v2-provider-conflict (RFC 0177 §D.1)', () => {
86
101
  if (skip) return softSkip(skip.kind, skip.reason);
87
102
  const first = await installFixture();
88
103
  if (!first) return softSkip('blocked', 'RFC 0095 install seam not mounted (404/403) — connections.packsSupported advertised but /conformance/seams/sample/connection-packs/install is absent');
89
- // A host that ships a built-in `github` makes the fixture the LATER
90
- // registration, so the rule fires on the first install; otherwise the
91
- // fixture installs and a second pack claiming bare `github` is the later one.
104
+ // The branch is kept for a host that somehow ships a built-in of this id:
105
+ // the fixture is then the LATER registration and the rule fires on the
106
+ // first install. `acme-widgets` is fictional so the normal path is the
107
+ // second one — the fixture installs, and a second pack claiming the same
108
+ // bare id is the later registration. Keeping the branch costs nothing and
109
+ // means the scenario does not depend on that assumption holding.
92
110
  const later = first.res?.installed === false && codes(first.res).includes('connection_provider_conflict')
93
111
  ? first.res
94
112
  : await (async () => {
95
- expect(first.res?.installed, req('openwop.requirement.0177.provider-conflict.fail-closed', SECTION, `the well-formed fixture MUST install when no other definition of github exists (got ${first.status}: ${JSON.stringify(first.res)})`)).toBe(true);
113
+ expect(first.res?.installed, req('openwop.requirement.0177.provider-conflict.fail-closed', SECTION, `the well-formed fixture MUST install when no other definition of acme-widgets exists (got ${first.status}: ${JSON.stringify(first.res)})`)).toBe(true);
96
114
  return (await driver.post(INSTALL, { manifest: conflicting(fixture()) })).json as InstallResult | undefined;
97
115
  })();
98
116
  expect(later?.installed, req('openwop.requirement.0177.provider-conflict.fail-closed', SECTION, 'the later registration of a bare provider id MUST NOT install (no version precedence)')).toBe(false);
99
117
  expect(codes(later), req('openwop.requirement.0177.provider-conflict.fail-closed', SECTION, 'the later registration MUST be refused with connection_provider_conflict')).toContain('connection_provider_conflict');
100
118
  });
101
119
 
102
- it('the qualified form <packName>#github resolves to the named pack\'s definition', async () => {
120
+ it('the qualified form <packName>#acme-widgets resolves to the named pack\'s definition', async () => {
103
121
  const skip = await preflight();
104
122
  if (skip) return softSkip(skip.kind, skip.reason);
105
123
  const first = await installFixture();
106
124
  if (!first) return softSkip('blocked', 'RFC 0095 install seam not mounted (404/403) — connections.packsSupported advertised but /conformance/seams/sample/connection-packs/install is absent');
107
- if (first.res?.installed !== true) return softSkip('blocked', `the fixture did not install (${codes(first.res).join(',') || first.status}) — a host with a built-in github cannot exercise the qualified form through the pack`);
125
+ if (first.res?.installed !== true) return softSkip('blocked', `the fixture did not install (${codes(first.res).join(',') || first.status}) — a host with a built-in acme-widgets cannot exercise the qualified form through the pack`);
108
126
  const packName = fixture().name;
109
- const hit = await driver.post(RESOLVE, { provider: `${packName}#github` });
127
+ const hit = await driver.post(RESOLVE, { provider: `${packName}#acme-widgets` });
110
128
  if (hit.status === 404) return softSkip('blocked', 'RFC 0095 resolve seam not mounted (404)');
111
129
  const resolved = hit.json as ResolveResult | undefined;
112
- expect(resolved?.resolved, req('openwop.requirement.0177.provider-conflict.qualified-form', 'connection-packs.md §"The qualified form" (RFC 0177 §D.1)', `${packName}#github MUST resolve (got ${JSON.stringify(resolved)})`)).toBe(true);
130
+ expect(resolved?.resolved, req('openwop.requirement.0177.provider-conflict.qualified-form', 'connection-packs.md §"The qualified form" (RFC 0177 §D.1)', `${packName}#acme-widgets MUST resolve (got ${JSON.stringify(resolved)})`)).toBe(true);
113
131
  expect(resolved?.source, req('openwop.requirement.0177.provider-conflict.qualified-form', 'connection-packs.md §"The qualified form" (RFC 0177 §D.1)', 'a qualified reference resolves only to the named pack\'s definition (source: pack)')).toBe('pack');
114
132
  });
115
133
  });
@@ -194,6 +194,17 @@ function retryWaitMs(doc: Record<string, unknown>): number {
194
194
  * constant the delivery loop reads. `WAIT_SLACK_MS` covers `waitTerminal`,
195
195
  * registration and the HTTP round trips around the waits.
196
196
  */
197
+ /**
198
+ * How many attempts the receiver refuses before answering 204 (suite 2.0.3).
199
+ *
200
+ * The retry leg's success witness lands on attempt `FAIL_FIRST + 1`, so this
201
+ * number decides how much backoff the scenario has to sit through. It is a
202
+ * named constant rather than a literal at the `startReceiver` call because the
203
+ * blocked-reason quotes it: the message a host reads must be computed from the
204
+ * receiver the suite actually started, not from a number typed twice.
205
+ */
206
+ const FAIL_FIRST = 2;
207
+
197
208
  const WAIT_SLACK_MS = 30_000;
198
209
  /** One `retryWaitMs` wait (the retry leg). */
199
210
  const RETRY_TEST_TIMEOUT_MS = RETRY_WAIT_CAP_MS + WAIT_SLACK_MS;
@@ -224,7 +235,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
224
235
  if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
225
236
  if (!fixtureAdvertised(doc, FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to deliver`);
226
237
 
227
- const receiver = await startReceiver(2); // 500, 500, then 204
238
+ const receiver = await startReceiver(FAIL_FIRST); // 500, 500, then 204
228
239
  active = receiver.server;
229
240
  const sub = await register(receiver.url);
230
241
  if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
@@ -246,10 +257,37 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
246
257
  attempts.length,
247
258
  req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `a 500 from the subscriber MUST be retried — ${failedThenSucceeded} failed attempt(s) were answered and the host made ${attempts.length} attempt(s) in total; one attempt is best-effort delivery, which is not a conforming mode (RFC 0173 §B)`),
248
259
  ).toBeGreaterThan(1);
249
- expect(
250
- retried,
251
- req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', 'at-least-once: after the failing attempts the retry MUST land (the receiver answered 204 to the third attempt for the key)'),
252
- ).toBe(true);
260
+ // At-least-once: the retry MUST eventually land. When it has not landed
261
+ // inside our window this records `blocked`, NOT `executed-fail` — suite
262
+ // 2.0.3, and this is the third time this file has had to learn it.
263
+ //
264
+ // The receiver answers 204 only on attempt `FAIL_FIRST + 1`, so reaching it
265
+ // costs the SUM of the first FAIL_FIRST backoff intervals, not the largest
266
+ // one. On an exponential-from-30s policy that is 30 + 60 = 90 s, which is
267
+ // exactly RETRY_WAIT_CAP_MS — a host loses by the width of one delivery.
268
+ // The obvious fix is to derive the wait from the intervals, and it cannot
269
+ // be built: `spec/v2/facets/webhooks.schema.json` `retryPolicy` is
270
+ // `additionalProperties: false` over exactly { maxAttempts, backoff }.
271
+ // THE BASE INTERVAL IS NOT ON THE WIRE, so the suite cannot compute the
272
+ // time to the Nth attempt, and any cap I pick is 2.0.1's 20-second
273
+ // deadline again with a bigger literal.
274
+ //
275
+ // So the honest disposition is `blocked`: the host took the obligation on
276
+ // (it advertises webhooks and we observed it retry) and the suite could not
277
+ // measure the outcome (RFC 0148 §A). Not `inapplicable` — that would claim
278
+ // it never took the obligation on.
279
+ //
280
+ // THE COST, stated rather than hidden: a host that retries forever and
281
+ // never succeeds now also records `blocked` instead of failing. This trades
282
+ // a false conviction for a missed detection. Detection comes back by
283
+ // putting the interval on the wire — an additive `retryPolicy` field so the
284
+ // sum is derivable — which is normative surface, an RFC and a 2.1.0, not a
285
+ // patch. Recorded here so the trade is visible at the assertion rather than
286
+ // only in a changelog.
287
+ if (!retried) {
288
+ const policyNote = advertisedRetryPolicy(doc);
289
+ return softSkip('blocked', `the retry was observed (${attempts.length} attempts) but the receiver's 204 did not land inside the ${retryWaitMs(doc)}ms window: it answers 204 only on attempt ${FAIL_FIRST + 1}, which costs the SUM of the first ${FAIL_FIRST} backoff intervals, and webhooks.retryPolicy carries only { maxAttempts, backoff${policyNote ? `: ${String(policyNote.backoff)}` : ''} } — the base interval is not advertised, so the suite cannot derive how long to wait. Unmeasured, not unmet (RFC 0148 §A).`);
290
+ }
253
291
  // Backoff: the retry MUST NOT be a tight loop — consecutive attempts for one
254
292
  // key are spaced. Only asserted when the host advertises a non-`none` backoff.
255
293
  const policy = advertisedRetryPolicy(doc);