@openwop/openwop-conformance 2.40.0 → 2.40.1

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,13 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.40.1] — 2026-09-26 — three declared-unwitnessable rows are reached and say why
4
+
5
+ - **Three Falsifiability rows declared non-executable now have a leg that records the reason** (RFC 0174 §B.1 rule 4, second branch: "the suite must reach the leg and record a reason"). Each RFC's table declares one of these rows unwitnessable, but no suite leg referenced it, so no bundle could account for it. Each of RFCs 0197, 0205 and 0209 was held from `Accepted` by that one row. No leg can record `executed-pass` without observing the requirement.
6
+ - `openwop.requirement.0197.retired-not-emitted` (`v2-capability-maturity-bounded`, new describe) is **conditional, not constant**. It reads `spec/v1/deprecations.json`, `spec/v2/migrations.json` and `spec/v2/release.json` from the shipped corpus. While no `v2-minor` row with `retirement.persistence: "persisted"` is due, it records `inapplicable` with RFC 0197 §A.3's reason; that is today's state, with 0 rows. Once one is due, it locates the member by the schema's `x-openwop-retired-in` annotation (a property name, or a `const` / single-`enum` value). It then drives a `conformance-noop` run and fails on any event that carries the member. A due row whose member it cannot locate records `blocked`. The branches no host can reach yet are self-tested in `src/lib/v2-retired-members.test.ts` (due / not-due / advertised-class / unannotated / caught emission / clean event).
7
+ - `openwop.requirement.0205.export-alias-equivalence` (`v2-artifact-a2a-shape`, new describe) records `inapplicable` on every host, before any request, with RFC 0205 §C.11's verdict: consumer matching logic and a pack-author call, neither on the wire.
8
+ - `openwop.requirement.0209.render-needs-root` (`v2-a2ui-v09-surface`, new describe) records `inapplicable` on every host, before any request or seam gate, with RFC 0209 §C.9's verdict. It names the reference-impl witness: openwop-app `frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx` (openwop-app#4121).
9
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.1` because `2.40.0` is tagged. Not tagged, not published. 561 scenario files, unchanged, so this is a patch.
10
+
3
11
  ## [2.40.0] — 2026-09-26 — RFC 0215: one subscription's dead receiver cannot hold another's delivery, and unregistering stops the retries
4
12
 
5
13
  - **Two new scenarios, gated on `webhooks` (RFC 0215, `Active` 2026-09-25).** `v2-webhook-delivery-isolation` (`openwop.requirement.0215.no-head-of-line`) holds 8 subscriptions' attempts unanswered, then checks that a ninth subscription's attempt still starts. The 8 filter `run.started` and the ninth `run.completed` of a 2 s `conformance-delay` run, so contention exists before the healthy delivery falls due and the host's dispatch order cannot decide the row. It fails when the held attempts were still open at the due time and the healthy one waited for the host to release one. It records `partial-witness` when the host's own delivery timeout closed them first. `v2-webhook-unregister-stops-delivery` (`openwop.requirement.0215.unregister-stops-delivery`) answers `500` to two subscriptions on one run, unregisters one after its first attempt, and fails if that one is attempted more than 5 s after the `204`. It passes only when the other, the control, was retried after that point: a silence counts only when a retry demonstrably would have come. Sabotage on the v2 reference host: a sequential worker and a 5-slot pool each fail the first scenario, and an attempt that falls back to a cached subscription after the delete fails the second.
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.40.0 @openwop/spec-artifacts@2.40.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.40.1 @openwop/spec-artifacts@2.40.1
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.40.0",
4
- "stampSha256": "f65c56d3a36eb7301c4baceef9ce0899091151fb12bc72c1508eb7a7d7b4ca4c"
3
+ "version": "2.40.1",
4
+ "stampSha256": "1fb5733bb565913ac21afc3e5179ea3cb4d1b98ab39f543fb3c70f9a936453bd"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.40.0",
3
+ "version": "2.40.1",
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.40.0"
59
+ "@openwop/spec-artifacts": "2.40.1"
60
60
  }
61
61
  }
@@ -167,6 +167,8 @@
167
167
  "openwop.it.v2-security-defaults-table.the-obligation-table-has-a-witness-column-and-no-row-is-unwitnessable": "openwop.requirement.0173.security-defaults-table",
168
168
  "openwop.it.v2-spec-artifacts-digest.the-corpus-gate-script-exits-0": "openwop.requirement.0168.spec-artifacts-digest",
169
169
  "openwop.it.v2-threat-model-template.the-corpus-gate-script-exits-0": "openwop.requirement.0173.threat-model-template",
170
- "openwop.it.v2-witness-required.the-corpus-gate-script-exits-0": "openwop.requirement.0168.witness-required"
170
+ "openwop.it.v2-witness-required.the-corpus-gate-script-exits-0": "openwop.requirement.0168.witness-required",
171
+ "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-report-names-the-overstated-families-and-t": "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-gate-states-when-c-7-binds",
172
+ "openwop.it.bundle-maturity-gate.the-same-committed-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-every-ov": "openwop.it.bundle-maturity-gate.the-superseded-myndhyve-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-eve"
171
173
  }
172
174
  }
package/requirements.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
5
  "files": 635,
6
- "tests": 2427,
7
- "withStableId": 2427,
6
+ "tests": 2430,
7
+ "withStableId": 2430,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2307
9
+ "explicitIds": 2310
10
10
  },
11
11
  "records": [
12
12
  {
@@ -27218,7 +27218,7 @@
27218
27218
  {
27219
27219
  "id": "openwop.it.v2-a2ui-v09-surface.a-version-2-envelope-carrying-a-version-1-body-is-refused-the-positive-is-admitt",
27220
27220
  "file": "v2-a2ui-v09-surface.test.ts",
27221
- "line": 129,
27221
+ "line": 137,
27222
27222
  "title": "a version-2 envelope carrying a version-1 body is refused; the positive is admitted; above the floor is refused; below it follows envelopeStrictness",
27223
27223
  "explicitId": "openwop.requirement.0209.version-selects-branch",
27224
27224
  "citations": [
@@ -27262,7 +27262,7 @@
27262
27262
  {
27263
27263
  "id": "openwop.it.v2-a2ui-v09-surface.no-first-createsurface-a-message-after-deletesurface-and-a-second-createsurface",
27264
27264
  "file": "v2-a2ui-v09-surface.test.ts",
27265
- "line": 155,
27265
+ "line": 163,
27266
27266
  "title": "no first createSurface, a message after deleteSurface, and a second createSurface are each refused; the valid steps are admitted",
27267
27267
  "explicitId": "openwop.requirement.0209.fold-guarded",
27268
27268
  "citations": [
@@ -27276,7 +27276,7 @@
27276
27276
  {
27277
27277
  "id": "openwop.it.v2-a2ui-v09-surface.a-createsurface-catalogid-differing-from-the-payload-catalogid-is-refused",
27278
27278
  "file": "v2-a2ui-v09-surface.test.ts",
27279
- "line": 180,
27279
+ "line": 188,
27280
27280
  "title": "a createSurface.catalogId differing from the payload catalogId is refused",
27281
27281
  "explicitId": "openwop.requirement.0209.catalog-equality",
27282
27282
  "citations": [
@@ -27294,7 +27294,7 @@
27294
27294
  {
27295
27295
  "id": "openwop.it.v2-a2ui-v09-surface.a-message-whose-surfaceid-differs-from-the-payload-surfaceid-is-refused",
27296
27296
  "file": "v2-a2ui-v09-surface.test.ts",
27297
- "line": 194,
27297
+ "line": 202,
27298
27298
  "title": "a message whose surfaceId differs from the payload surfaceId is refused",
27299
27299
  "explicitId": "openwop.it.v2-a2ui-v09-surface.surface-id-equality",
27300
27300
  "citations": [
@@ -27312,7 +27312,7 @@
27312
27312
  {
27313
27313
  "id": "openwop.it.v2-a2ui-v09-surface.a-recorded-version-2-surface-is-returned-byte-equal-on-poll-and-carried-by-a-for",
27314
27314
  "file": "v2-a2ui-v09-surface.test.ts",
27315
- "line": 210,
27315
+ "line": 218,
27316
27316
  "title": "a recorded version-2 surface is returned byte-equal on poll and carried by a fork at a later fromSeq",
27317
27317
  "explicitId": "openwop.it.v2-a2ui-v09-surface.recorded-as-recorded",
27318
27318
  "citations": [
@@ -27349,7 +27349,7 @@
27349
27349
  {
27350
27350
  "id": "openwop.it.v2-a2ui-v09-surface.a-version-1-surface-recorded-at-floor-1-is-returned-byte-equal-on-poll-and-carri",
27351
27351
  "file": "v2-a2ui-v09-surface.test.ts",
27352
- "line": 237,
27352
+ "line": 245,
27353
27353
  "title": "a version-1 surface recorded at floor 1 is returned byte-equal on poll and carried by a fork",
27354
27354
  "explicitId": "openwop.requirement.0209.legacy-readable",
27355
27355
  "citations": [
@@ -27386,7 +27386,7 @@
27386
27386
  {
27387
27387
  "id": "openwop.it.v2-a2ui-v09-surface.one-untrusted-update-taints-a-trusted-surface-a-later-trusted-update-does-not-la",
27388
27388
  "file": "v2-a2ui-v09-surface.test.ts",
27389
- "line": 261,
27389
+ "line": 269,
27390
27390
  "title": "one untrusted update taints a trusted surface, a later trusted update does not launder it, and the approval does not advance; an all-trusted surface does",
27391
27391
  "explicitId": "openwop.requirement.0209.taint-sticky",
27392
27392
  "citations": [
@@ -27397,6 +27397,19 @@
27397
27397
  }
27398
27398
  ]
27399
27399
  },
27400
+ {
27401
+ "id": "openwop.it.v2-a2ui-v09-surface.records-why-render-needs-root-has-no-server-side-witness-and-where-its-witness-l",
27402
+ "file": "v2-a2ui-v09-surface.test.ts",
27403
+ "line": 308,
27404
+ "title": "records why render-needs-root has no server-side witness, and where its witness lives",
27405
+ "explicitId": "openwop.requirement.0209.render-needs-root",
27406
+ "citations": [
27407
+ {
27408
+ "section": "RFC 0209 §C.9",
27409
+ "requirement": "a renderer does not render a surface before its root component exists"
27410
+ }
27411
+ ]
27412
+ },
27400
27413
  {
27401
27414
  "id": "openwop.it.v2-advertised-fixtures-exist.a-sampled-advertised-fixture-is-actually-creatable-not-just-listed",
27402
27415
  "file": "v2-advertised-fixtures-exist.test.ts",
@@ -27467,7 +27480,7 @@
27467
27480
  {
27468
27481
  "id": "openwop.it.v2-artifact-a2a-shape.a-getartifact-answered-as-application-a2a-json-validates-against-artifact-schema",
27469
27482
  "file": "v2-artifact-a2a-shape.test.ts",
27470
- "line": 80,
27483
+ "line": 87,
27471
27484
  "title": "a getArtifact answered as application/a2a+json validates against artifact.schema.json",
27472
27485
  "explicitId": "openwop.requirement.0205.artifact-a2a-shape",
27473
27486
  "citations": [
@@ -27491,7 +27504,7 @@
27491
27504
  {
27492
27505
  "id": "openwop.it.v2-artifact-a2a-shape.the-artifactid-of-an-application-a2a-json-body-equals-the-artifactid-path-segmen",
27493
27506
  "file": "v2-artifact-a2a-shape.test.ts",
27494
- "line": 92,
27507
+ "line": 99,
27495
27508
  "title": "the artifactId of an application/a2a+json body equals the {artifactId} path segment",
27496
27509
  "explicitId": "openwop.requirement.0205.artifact-id-matches-path",
27497
27510
  "citations": [
@@ -27508,7 +27521,7 @@
27508
27521
  {
27509
27522
  "id": "openwop.it.v2-artifact-a2a-shape.the-url-of-a-url-part-answers-no-2xx-to-a-second-tenant-or-to-no-credential",
27510
27523
  "file": "v2-artifact-a2a-shape.test.ts",
27511
- "line": 103,
27524
+ "line": 110,
27512
27525
  "title": "the url of a url Part answers no 2xx to a second tenant or to no credential",
27513
27526
  "explicitId": "openwop.requirement.0205.artifact-url-part-scoped",
27514
27527
  "citations": [
@@ -27524,6 +27537,19 @@
27524
27537
  }
27525
27538
  ]
27526
27539
  },
27540
+ {
27541
+ "id": "openwop.it.v2-artifact-a2a-shape.records-why-export-alias-equivalence-has-no-wire-witness",
27542
+ "file": "v2-artifact-a2a-shape.test.ts",
27543
+ "line": 132,
27544
+ "title": "records why export-alias-equivalence has no wire witness",
27545
+ "explicitId": "openwop.requirement.0205.export-alias-equivalence",
27546
+ "citations": [
27547
+ {
27548
+ "section": "RFC 0205 §C.11",
27549
+ "requirement": "an export alias and its media type name one format"
27550
+ }
27551
+ ]
27552
+ },
27527
27553
  {
27528
27554
  "id": "openwop.it.v2-assurance-downgrade-audited.an-identity-below-the-lane-floor-is-refused-with-sender-constraint-missing",
27529
27555
  "file": "v2-assurance-downgrade-audited.test.ts",
@@ -27912,7 +27938,7 @@
27912
27938
  {
27913
27939
  "id": "openwop.it.v2-capability-maturity-bounded.no-family-record-advertises-stable-above-its-corpus-declaration-row",
27914
27940
  "file": "v2-capability-maturity-bounded.test.ts",
27915
- "line": 69,
27941
+ "line": 92,
27916
27942
  "title": "no family record advertises `stable` above its corpus declaration row",
27917
27943
  "explicitId": "openwop.requirement.0197.maturity-not-overstated",
27918
27944
  "citations": [
@@ -27933,6 +27959,20 @@
27933
27959
  }
27934
27960
  ]
27935
27961
  },
27962
+ {
27963
+ "id": "openwop.it.v2-capability-maturity-bounded.no-run-emits-a-persisted-member-the-release-has-retired",
27964
+ "file": "v2-capability-maturity-bounded.test.ts",
27965
+ "line": 134,
27966
+ "title": "no run emits a persisted member the release has retired",
27967
+ "explicitId": "openwop.requirement.0197.retired-not-emitted",
27968
+ "citations": [
27969
+ {
27970
+ "section": "RFC 0197 §A.3",
27971
+ "requirement": null,
27972
+ "interpolated": true
27973
+ }
27974
+ ]
27975
+ },
27936
27976
  {
27937
27977
  "id": "openwop.it.v2-capability-record-shape.every-family-record-carries-status-since-and-witness-from-the-wire-legal-sets",
27938
27978
  "file": "v2-capability-record-shape.test.ts",
@@ -36661,10 +36701,10 @@
36661
36701
  ]
36662
36702
  },
36663
36703
  {
36664
- "id": "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-report-names-the-overstated-families-and-t",
36704
+ "id": "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-gate-states-when-c-7-binds",
36665
36705
  "file": "bundle-maturity-gate.test.ts",
36666
- "line": 76,
36667
- "title": "the committed corpus is green, and the report NAMES the overstated families and the gap row",
36706
+ "line": 85,
36707
+ "title": "the committed corpus is green, and the gate states when §C.7 binds",
36668
36708
  "explicitId": "openwop.requirement.0197.bundle-maturity-bound",
36669
36709
  "citations": [
36670
36710
  {
@@ -36672,14 +36712,6 @@
36672
36712
  "requirement": null,
36673
36713
  "interpolated": true
36674
36714
  },
36675
- {
36676
- "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36677
- "requirement": "a \"reported\" disposition that printed nothing would be indistinguishable from a clean tree; the report MUST name the gap row that owes the re-cut"
36678
- },
36679
- {
36680
- "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36681
- "requirement": "the report MUST name the families by key, not just count them — a number nobody can act on is not a report"
36682
- },
36683
36715
  {
36684
36716
  "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36685
36717
  "requirement": "the gate MUST state which suite version makes §C.7 binding, so \"reported\" has a visible end date"
@@ -36687,10 +36719,10 @@
36687
36719
  ]
36688
36720
  },
36689
36721
  {
36690
- "id": "openwop.it.bundle-maturity-gate.the-same-committed-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-every-ov",
36722
+ "id": "openwop.it.bundle-maturity-gate.the-superseded-myndhyve-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-eve",
36691
36723
  "file": "bundle-maturity-gate.test.ts",
36692
- "line": 86,
36693
- "title": "the same committed bundle re-labelled onto the RFC’s suite FAILS, naming every overstated family",
36724
+ "line": 93,
36725
+ "title": "the superseded MyndHyve bundle re-labelled onto the RFC’s suite FAILS, naming every overstated family",
36694
36726
  "explicitId": "openwop.requirement.0197.bundle-maturity-bound",
36695
36727
  "citations": [
36696
36728
  {
@@ -36712,7 +36744,7 @@
36712
36744
  {
36713
36745
  "id": "openwop.it.bundle-maturity-gate.the-count-is-derived-promoting-one-family-in-the-declaration-drops-38-to-37",
36714
36746
  "file": "bundle-maturity-gate.test.ts",
36715
- "line": 95,
36747
+ "line": 102,
36716
36748
  "title": "the count is DERIVED — promoting one family in the declaration drops 38 to 37",
36717
36749
  "explicitId": "openwop.requirement.0197.bundle-maturity-bound",
36718
36750
  "citations": [
@@ -36730,7 +36762,7 @@
36730
36762
  {
36731
36763
  "id": "openwop.it.bundle-maturity-gate.a-bundle-whose-suite-is-below-the-rfc-s-is-reported-not-failed-even-with-the-sam",
36732
36764
  "file": "bundle-maturity-gate.test.ts",
36733
- "line": 103,
36765
+ "line": 110,
36734
36766
  "title": "a bundle whose suite is BELOW the RFC’s is reported, not failed, even with the same 38",
36735
36767
  "explicitId": "openwop.requirement.0197.bundle-maturity-bound",
36736
36768
  "citations": [
@@ -36746,6 +36778,14 @@
36746
36778
  {
36747
36779
  "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36748
36780
  "requirement": "the reported row must cite COMPATIBILITY.md §2.3 — an old pass stays a measurement at the suite that measured it"
36781
+ },
36782
+ {
36783
+ "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36784
+ "requirement": "a \"reported\" disposition that printed nothing would be indistinguishable from a clean tree; the report MUST name the gap row that owes the re-cut"
36785
+ },
36786
+ {
36787
+ "section": "spec/v2/core/capabilities.md §8 (RFC 0197 §C.7)",
36788
+ "requirement": "the report MUST name the families by key, not just count them — a number nobody can act on is not a report"
36749
36789
  }
36750
36790
  ]
36751
36791
  },
@@ -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.40.0",
5
- "corpusTag": "v2.40.0",
4
+ "version": "2.40.1",
5
+ "corpusTag": "v2.40.1",
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": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "acd23b627e68bee6c886f21f4b8252f0dcb49d9deb5d4abc4c19d390f56b426a",
13
- "api/v2/asyncapi.yaml": "de935d89b72a1196c0f8f9731f1c3d32cee118e81b4271365e76ffdc5c9fced6",
14
- "api/v2/openapi.yaml": "0d12490c1739defdb302ec384d9dd81306d1d865c7023fda040baa846ff35c8b",
12
+ "api/seams-v2.yaml": "23c87c259cbe4b6d087faa9f358684921d400a11330bde6b27c8cacbf0b16d78",
13
+ "api/v2/asyncapi.yaml": "3629ae4718168e424aee3e93b3c785df904a12229a2c58c0eec0ecaf9f8f5b2e",
14
+ "api/v2/openapi.yaml": "74d9beac93b861a3d191ee5e87ce047a907171df6ecdb3fcb0854d28e8df119c",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,13 +204,13 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "ca5a83168c0e6a4d73d22d32acdd4ca37bfc62297ad730df6698134caa99ebbb",
207
+ "spec/v1/core-standard-manifest.json": "77d77c3ee245f8f7855a530a3aa610a0a7c00a26df6c97962f1600c93e8b13c1",
208
208
  "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "468d5b60f5e8b276f895833ac072c3984ab104d0ad97c4d31b757290c6077172",
213
+ "spec/v1/gaps.json": "45483b0932d4b2a80b0781419abc697f7d363be46bddfd8163a48364b8f76a36",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -292,9 +292,9 @@
292
292
  "spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
293
293
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
294
294
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
295
- "spec/v2/release.json": "f17a7e0066d7851adfb246c586f12199d14fee60bf7a0eda203a124116dcff9e",
295
+ "spec/v2/release.json": "c24fde3d12b1732d9f0162ef0a0596ebd24501935cd28801eb892fbb6ec57949",
296
296
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
297
- "spec/v2/surface-baseline.json": "2ba53481583261c59008da86d1db82ed9f657675dfe2f8b019ec7d82f5bdb4b4"
297
+ "spec/v2/surface-baseline.json": "72d51a1bd81cbe3cee07f9ad0ab7b481b60d4e74e651704e9b3242c49c93ec11"
298
298
  },
299
- "corpusCommit": "44b89e1e50f56dd2a0834771225b8e04d2bd9e6f"
299
+ "corpusCommit": "470ee4d7a2cb94ddb893d82d57ade4d95644d74b"
300
300
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * RFC 0197 §A.3 — which persisted-class v2 members are retired at the release
3
+ * the suite ships, and whether an emitted event carries one.
4
+ *
5
+ * A persisted-class retirement keeps the member in the READER schema,
6
+ * annotated `x-openwop-retired-in: "2.N"`, and forbids only its EMISSION after
7
+ * `2.N`. The deprecations register says WHICH rows are persisted-class and
8
+ * when they fall due; the annotation in the v2 schema says WHAT the member is
9
+ * (a property name, or a `const` / single-member `enum` value). This module
10
+ * joins the two so `v2-capability-maturity-bounded`'s `retired-not-emitted`
11
+ * leg can scan real events for it.
12
+ *
13
+ * Pure (the caller passes the parsed register and a schema loader) so the
14
+ * self-test drives the due / not-due / unlocatable branches with fixtures —
15
+ * the register is empty today, so the host leg cannot reach them yet.
16
+ *
17
+ * @see RFCS/0197-v2-surfaces-retired-never-reshaped.md §A.3
18
+ * @see scripts/check-removal-dates.mjs (the corpus gate that requires the annotation on a due persisted row)
19
+ */
20
+
21
+ type Json = Record<string, unknown>;
22
+
23
+ export interface PersistedRetirement {
24
+ readonly id: string;
25
+ readonly removeIn: string;
26
+ readonly due: boolean;
27
+ /** `schemas/v2/…` source files the row cites, relative to the contract root. */
28
+ readonly schemaFiles: readonly string[];
29
+ }
30
+
31
+ export interface RetiredMember {
32
+ readonly rowId: string;
33
+ readonly removeIn: string;
34
+ readonly schemaFile: string;
35
+ /** `property`: an object key; `value`: a string value. */
36
+ readonly kind: 'property' | 'value';
37
+ readonly name: string;
38
+ }
39
+
40
+ export interface RetiredEmission {
41
+ readonly rowId: string;
42
+ readonly member: string;
43
+ readonly eventType: string;
44
+ readonly path: string;
45
+ }
46
+
47
+ const minorOf = (v: string): number | null => {
48
+ const m = /^2\.(\d+)(?:\.\d+)?$/.exec(v.trim());
49
+ return m ? Number(m[1]) : null;
50
+ };
51
+
52
+ const triggersOf = (e: Json): string[] => {
53
+ const t = e['removalTrigger'];
54
+ return Array.isArray(t) ? t.filter((x): x is string => typeof x === 'string') : typeof t === 'string' ? [t] : [];
55
+ };
56
+
57
+ /** Every `v2-minor` row whose `retirement.persistence` is `persisted`, and whether `release` has reached its `removeIn`. */
58
+ export function persistedRetirements(deprecations: unknown, release: string): PersistedRetirement[] {
59
+ const entries = ((deprecations as { entries?: unknown } | null)?.entries ?? []) as Json[];
60
+ const rel = minorOf(release);
61
+ const out: PersistedRetirement[] = [];
62
+ for (const e of Array.isArray(entries) ? entries : []) {
63
+ if (!triggersOf(e).includes('v2-minor')) continue;
64
+ const r = e['retirement'] as Json | undefined;
65
+ if ((r?.['persistence'] ?? 'advertised') !== 'persisted') continue;
66
+ const removeIn = String(e['removeIn'] ?? '');
67
+ const at = minorOf(removeIn);
68
+ const sources = (Array.isArray(e['sources']) ? e['sources'] : []) as Json[];
69
+ out.push({
70
+ id: String(e['id'] ?? '(no id)'),
71
+ removeIn,
72
+ due: rel !== null && at !== null && rel >= at,
73
+ schemaFiles: sources.map((s) => String(s['file'] ?? '')).filter((f) => /^schemas\/v2\//.test(f)),
74
+ });
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** The members a due row's schema annotates `x-openwop-retired-in: <removeIn>`. */
80
+ export function retiredMembers(row: PersistedRetirement, loadSchema: (file: string) => unknown): RetiredMember[] {
81
+ const out: RetiredMember[] = [];
82
+ for (const file of row.schemaFiles) {
83
+ const walk = (node: unknown, parentKey: string | null, grandKey: string | null): void => {
84
+ if (Array.isArray(node)) { for (const n of node) walk(n, null, parentKey); return; }
85
+ if (node === null || typeof node !== 'object') return;
86
+ const o = node as Json;
87
+ if (o['x-openwop-retired-in'] === row.removeIn) {
88
+ if (typeof o['const'] === 'string') out.push({ rowId: row.id, removeIn: row.removeIn, schemaFile: file, kind: 'value', name: o['const'] });
89
+ else if (Array.isArray(o['enum']) && o['enum'].length === 1 && typeof o['enum'][0] === 'string') out.push({ rowId: row.id, removeIn: row.removeIn, schemaFile: file, kind: 'value', name: o['enum'][0] });
90
+ else if (grandKey === 'properties' && parentKey !== null) out.push({ rowId: row.id, removeIn: row.removeIn, schemaFile: file, kind: 'property', name: parentKey });
91
+ }
92
+ for (const [k, v] of Object.entries(o)) walk(v, k, parentKey);
93
+ };
94
+ walk(loadSchema(file), null, null);
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Every place an event carries a retired member: a key named for a `property` member, or a string equal to a `value` member. */
100
+ export function findRetiredEmissions(events: readonly unknown[], members: readonly RetiredMember[]): RetiredEmission[] {
101
+ const hits: RetiredEmission[] = [];
102
+ for (const ev of events) {
103
+ const eventType = String((ev as Json | null)?.['type'] ?? '(untyped)');
104
+ const walk = (node: unknown, path: string): void => {
105
+ if (typeof node === 'string') {
106
+ for (const m of members) if (m.kind === 'value' && node === m.name) hits.push({ rowId: m.rowId, member: m.name, eventType, path });
107
+ return;
108
+ }
109
+ if (Array.isArray(node)) { node.forEach((n, i) => walk(n, `${path}[${i}]`)); return; }
110
+ if (node === null || typeof node !== 'object') return;
111
+ for (const [k, v] of Object.entries(node as Json)) {
112
+ for (const m of members) if (m.kind === 'property' && k === m.name) hits.push({ rowId: m.rowId, member: m.name, eventType, path: `${path}.${k}` });
113
+ walk(v, `${path}.${k}`);
114
+ }
115
+ };
116
+ walk(ev, '$');
117
+ }
118
+ return hits;
119
+ }
@@ -19,6 +19,14 @@
19
19
  * Every refusal leg carries an admitted control in the same run, so a host that
20
20
  * refuses everything fails as surely as one that refuses nothing.
21
21
  *
22
+ * The last describe accounts for §C.9's `render-needs-root`, whose
23
+ * Falsifiability verdict is `unwitnessable`: the guarantee is render-side, and
24
+ * a server-oriented suite cannot observe a renderer. It records `inapplicable`
25
+ * on EVERY host, before any request and before the seam gate, and names where
26
+ * the real witness lives: a reference-app client probe, `tier: reference-impl`,
27
+ * following the `a2ui-surface-no-code-exec` precedent. It never asserts and
28
+ * cannot become a pass.
29
+ *
22
30
  * @see RFCS/0209-v2-a2ui-surfaces-are-a2ui-v0-9.md
23
31
  * @see spec/v2/ext/a2uiSurface/README.md
24
32
  * @see spec/v2/core/events.md §"The envelope-kind catalog"
@@ -295,3 +303,10 @@ describe('RFC 0209 §C.12 — trust is sticky across the fold (seam-gated)', ()
295
303
  }
296
304
  });
297
305
  });
306
+
307
+ describe('RFC 0209 §C.9 — no render before root (declared unwitnessable here; reference-impl witness)', () => {
308
+ it('records why render-needs-root has no server-side witness, and where its witness lives', () => {
309
+ req('openwop.requirement.0209.render-needs-root', 'RFC 0209 §C.9', 'a renderer does not render a surface before its root component exists');
310
+ return softSkip('inapplicable', 'unwitnessable by this suite (RFC 0209 §C.9 Falsifiability verdict): a render-side guarantee, and a server-oriented suite cannot observe a renderer. The fold guard the server CAN observe is openwop.requirement.0209.fold-guarded, above. The render half is witnessed as a reference-app client probe (tier: reference-impl, the a2ui-surface-no-code-exec precedent): openwop/openwop-app frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx (openwop-app#4121, ADR 0749). Removing the `if (!state.renderable)` guard in frontend/react/src/chat/a2ui/v09/A2uiV09Surface.tsx turns 2 of its 4 cases red. This row records that the suite reached the requirement, and it is not a pass.');
311
+ });
312
+ });
@@ -21,6 +21,13 @@
21
21
  * The server-free legs (the schemas against A2A v1.0.1) are corpus rows in
22
22
  * `src/coherence/a2a-parts-schemas.test.ts`.
23
23
  *
24
+ * The last describe accounts for §C.11's `export-alias-equivalence`, whose
25
+ * Falsifiability verdict is `unwitnessable`. It records `inapplicable` with that
26
+ * verdict's reason on EVERY host, before any request, and never asserts. The
27
+ * row exists so a bundle says the suite reached the requirement and why it
28
+ * cannot observe it (`check-accepted-predicate` rule 4, second branch). It is
29
+ * not a witness and cannot become one.
30
+ *
24
31
  * @see RFCS/0205-run-artifacts-and-turns-speak-a2a-parts.md
25
32
  * @see spec/v2/core/runs.md §"Annotations, artifacts, eval summary"
26
33
  */
@@ -120,3 +127,10 @@ describe('RFC 0205 §A.3 — a url Part does not resolve beyond the caller\'s au
120
127
  }
121
128
  });
122
129
  });
130
+
131
+ describe('RFC 0205 §C.11 — an export alias and its media type name one format (declared unwitnessable)', () => {
132
+ it('records why export-alias-equivalence has no wire witness', () => {
133
+ req('openwop.requirement.0205.export-alias-equivalence', 'RFC 0205 §C.11', 'an export alias and its media type name one format');
134
+ return softSkip('inapplicable', 'unwitnessable on the wire (RFC 0205 §C.11 Falsifiability verdict): the equivalence binds a consumer\'s format-matching logic and the pack author\'s host.artifactTypes.export call, and neither is on the wire. The advertised spellings are readable, but a mismatch between them is legal, so no host response can falsify the row. No wire witness exists; this row records that the suite reached the requirement, and it is not a pass.');
135
+ });
136
+ });
@@ -27,21 +27,44 @@
27
27
  * document and 38 families fail. That run is the sabotage proof, recorded in
28
28
  * the RFC 0197 gates PR.
29
29
  *
30
+ * The second describe is RFC 0197 §A.3's emission leg,
31
+ * `openwop.requirement.0197.retired-not-emitted`. Its Falsifiability verdict
32
+ * is "unwitnessable until the first persisted-class retirement row exists", so
33
+ * the leg is CONDITIONAL, never vacuous. It reads the deprecations register
34
+ * the suite ships, and while that has no due `v2-minor` row with
35
+ * `retirement.persistence: "persisted"`, it records `inapplicable` with the
36
+ * RFC's own reason. It must record a reason: rule 4 of
37
+ * `check-accepted-predicate` accepts a declared non-executable row only when a
38
+ * bundle row gives one. Once such a row is due, the leg drives a run and fails
39
+ * on any event that carries a member the v2 schema annotates
40
+ * `x-openwop-retired-in`. It never records `executed-pass` without scanning
41
+ * events. The branches the host leg cannot reach today are self-tested in
42
+ * `lib/v2-retired-members.test.ts`.
43
+ *
30
44
  * @see spec/v2/core/capabilities.md §8
31
45
  * @see spec/v2/declaration.json
32
- * @see RFCS/0197-v2-surfaces-retired-never-reshaped.md §C
46
+ * @see RFCS/0197-v2-surfaces-retired-never-reshaped.md §A.3, §C
33
47
  */
34
48
 
35
49
  import { describe, it, expect } from 'vitest';
36
50
  import { existsSync, readFileSync } from 'node:fs';
37
51
  import { join } from 'node:path';
38
52
  import { v2Discovery } from '../lib/v2.js';
53
+ import { driver } from '../lib/driver.js';
54
+ import { persistedRetirements, retiredMembers, findRetiredEmissions } from '../lib/v2-retired-members.js';
39
55
  import { SCHEMAS_DIR, SPEC_V2_DIR } from '../lib/paths.js';
40
56
  import { softSkip } from '../lib/soft-skip.js';
41
57
  import { req } from '../lib/requirement-ids.js';
42
58
 
43
59
  const DOC = 'spec/v2/core/capabilities.md §8';
44
60
  const ID = 'openwop.requirement.0197.maturity-not-overstated';
61
+ const RETIRED_ID = 'openwop.requirement.0197.retired-not-emitted';
62
+ const RETIRED_DOC = 'RFC 0197 §A.3';
63
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
64
+
65
+ function readJson(path: string): unknown {
66
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
67
+ }
45
68
 
46
69
  interface DeclarationFamily { key: string; maturity?: { technical?: string } }
47
70
 
@@ -106,3 +129,41 @@ describe('v2 capability-maturity-bounded (RFC 0197 §C.7)', () => {
106
129
  expect(records.length, req(ID, DOC, `the scenario asserted over ${records.length} family record(s)${undeclared.length > 0 ? `; ${undeclared.length} carry no declaration row and are left to v2-capabilities-root-closed: ${undeclared.join(', ')}` : ''}`)).toBeGreaterThan(0);
107
130
  });
108
131
  });
132
+
133
+ describe('v2 retired-not-emitted (RFC 0197 §A.3) — conditional on a due persisted-class retirement', () => {
134
+ it('no run emits a persisted member the release has retired', async () => {
135
+ if (SPEC_V2_DIR === null) return softSkip('blocked', 'spec/v2/ is not present in this layout — the retirement register cannot be read, and a register that cannot be read is not an absence');
136
+ const deprecations = readJson(join(SPEC_V2_DIR, '..', 'v1', 'deprecations.json'));
137
+ const migrations = readJson(join(SPEC_V2_DIR, 'migrations.json')) as { rows?: unknown[] } | null;
138
+ const release = (readJson(join(SPEC_V2_DIR, 'release.json')) as { version?: unknown } | null)?.version;
139
+ if (deprecations === null || typeof release !== 'string') return softSkip('blocked', 'spec/v1/deprecations.json or spec/v2/release.json is missing from the shipped corpus — the retirement register cannot be read');
140
+ const rows = persistedRetirements(deprecations, release);
141
+ const due = rows.filter((r) => r.due);
142
+ const census = `corpus ${release}: ${rows.length} persisted-class v2-minor retirement row(s), ${(migrations?.rows ?? []).length} v2→v2 migration row(s)`;
143
+ if (due.length === 0) {
144
+ return softSkip('inapplicable', `unwitnessable until the first persisted-class retirement row is due (RFC 0197 §A.3 Falsifiability): ${census}${rows.length > 0 ? `, none due yet (${rows.map((r) => `${r.id} removeIn ${r.removeIn}`).join(', ')})` : ''} — no retired member exists for a post-2.N event to carry. Before one does, a scenario would be vacuous, and a row that cannot fail is not a witness. This leg asserts the moment such a row falls due.`);
145
+ }
146
+ const contractRoot = join(SPEC_V2_DIR, '..', '..');
147
+ const members = due.flatMap((r) => retiredMembers(r, (file) => readJson(join(contractRoot, file))));
148
+ const unlocated = due.filter((r) => !members.some((m) => m.rowId === r.id));
149
+ if (unlocated.length > 0) return softSkip('blocked', `due persisted-class retirement row(s) whose member carries no x-openwop-retired-in annotation the suite can read: ${unlocated.map((r) => `${r.id} (${r.schemaFiles.join(', ') || 'no schemas/v2 source'})`).join('; ')}. Without the member there is nothing to scan for, and scanning for nothing is not a pass (check-removal-dates owns the missing annotation)`);
150
+ const create = await driver.post('/runs', { workflowId: 'conformance-noop' });
151
+ const runId = String((create.json as { runId?: unknown } | null)?.runId ?? '');
152
+ if (create.status !== 201 || runId === '') return softSkip('blocked', `POST /runs {workflowId: conformance-noop} answered ${create.status} — no post-${due[0]!.removeIn} run to read events from`);
153
+ const deadline = Date.now() + 20_000;
154
+ for (;;) {
155
+ const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
156
+ if (res.status === 200 && TERMINAL.has(String((res.json as { status?: unknown } | null)?.status ?? ''))) break;
157
+ if (Date.now() > deadline) return softSkip('blocked', `conformance-noop run ${runId} did not reach a terminal status within 20 s`);
158
+ await new Promise((r) => setTimeout(r, 200));
159
+ }
160
+ const poll = await driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
161
+ const events = (poll.json as { events?: unknown } | null)?.events;
162
+ if (poll.status !== 200 || !Array.isArray(events) || events.length === 0) return softSkip('blocked', `the event poll for run ${runId} answered ${poll.status} with no events — there is nothing emitted to scan`);
163
+ const hits = findRetiredEmissions(events, members);
164
+ expect(
165
+ hits.map((h) => `${h.member} (${h.rowId}, retired in ${members.find((m) => m.rowId === h.rowId)?.removeIn}) in ${h.eventType} at ${h.path}`),
166
+ req(RETIRED_ID, RETIRED_DOC, `a host MUST NOT emit a persisted member after the minor that retired it; scanned ${events.length} event(s) for ${members.length} retired member(s): ${members.map((m) => `${m.kind} ${m.name}`).join(', ')}`),
167
+ ).toEqual([]);
168
+ });
169
+ });