@openwop/openwop-conformance 2.2.1 → 2.3.0

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,62 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.3.0] — 2026-09-17 — the three RFC 0186 payload seats are witnessed
4
+
5
+ **Why a minor.** `PROTOCOL-STATUS.md` §suite version: minor on scenario add/remove. One scenario file is added.
6
+
7
+ ### Added
8
+
9
+ - **`v2-payload-seats-0186.test.ts`** (server-free, unconditional) — both hosts'
10
+ `conversation.exchanged` shapes validate against the single bound def and a
11
+ payload with neither `turn` nor `outcome` still does; the union stays closed;
12
+ the former orphan is an alias; the `conversation-event` `oneOf` matches
13
+ exactly one branch for an exchanged **and** a closed payload; `reason`
14
+ validates on `interruptResolved` and `nodeSuspended`, refuses empty, and
15
+ neither def reopens; `ApprovalData.onTimeout` admits `reject|approve|escalate`,
16
+ refuses a fourth, and the def still refuses the legacy bare `message`.
17
+ Sabotage-proved: removing `turn`, removing `reason`, and widening the enum
18
+ each red it.
19
+
20
+ ## [2.2.3] — 2026-09-16 — a payload def that v1 left open gets a conforming escape hatch
21
+
22
+ **Why a patch.** `PUBLISHING.md` §"Versioning alignment": one scenario file is
23
+ added, which is a minor — but `v2-payload-vendor-hatch.test.ts` is server-free
24
+ and additive to a corpus rule, and no existing scenario changes. Cut as a patch
25
+ alongside the spec edit it witnesses.
26
+
27
+ ### Added
28
+
29
+ - **`v2-payload-vendor-hatch.test.ts`** (server-free) — a def v1 left open
30
+ admits `vendor.*` / `x-` / `openwop-` keys; still **refuses** a bare
31
+ unmodelled key, because the hatch is an escape valve rather than a reopening;
32
+ and a vendor key nested inside `refineFeedback` is **refused**, since RFC 0183
33
+ modelled that object closed on purpose and it was never open in v1. That third
34
+ leg is the load-bearing one: it fails if a later edit sprays
35
+ `patternProperties` down the tree instead of applying it to the 53 narrowed
36
+ defs. Sabotage-proved both ways.
37
+
38
+ ## [2.2.2] — 2026-09-16 — a registry tree that says "read-only" when it means "deliberately behind"
39
+
40
+ **Why a patch.** `PUBLISHING.md` §"Versioning alignment": no scenario file is
41
+ added or removed; this is spec prose plus packaging metadata.
42
+
43
+ ### Fixed
44
+
45
+ - **`spec/v2/core/packs.md` §The registry tree** now says the v1 tree is
46
+ *"frozen through the overlap, deliberately behind this one"* rather than
47
+ *"served read-only through the overlap."* A tier-1 host operator read
48
+ "read-only" as *current, just not accepting writes*, resolved
49
+ `packs.openwop.dev/v1/index.json` from a major-2 host, and reported 154 of 156
50
+ packs drifted; a second session reproduced and confirmed it. Against
51
+ `registry/v2/` the drift was **zero**. The MUST that prevents this was already
52
+ in the next sentence — a client MUST resolve registry paths through
53
+ `endpoints` rather than construct one — but nothing said that constructing the
54
+ v1 path returns deliberately-older versions instead of failing.
55
+ - **`package-lock.json`** pinned the `@openwop/spec-artifacts` peer at
56
+ `2.0.0-rc.0` while `package.json` tracked the current release. Consumers
57
+ installing from the lockfile resolved a peer range two majors' worth of
58
+ prereleases behind the one the package declares.
59
+
3
60
  ## [2.2.1] — 2026-09-16 — RFC 0183's `edit-accept` arm is witnessed, and the MUST the suite already enforced is now the spec's
4
61
 
5
62
  **Why a patch.** `PUBLISHING.md` §"Versioning alignment": new `it` blocks in an
package/README.md CHANGED
@@ -117,7 +117,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
117
117
 
118
118
  ## What's Covered
119
119
 
120
- The current suite has 518 scenario files under `src/scenarios/`.
120
+ The current suite has 520 scenario files under `src/scenarios/`.
121
121
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
122
122
  - 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
123
123
  - 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
@@ -462,7 +462,7 @@ Server-required (added in 1.7.0):
462
462
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
463
463
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
464
464
 
465
- Current source tree: 518 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
465
+ Current source tree: 520 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
466
466
 
467
467
  ## Remaining Gaps
468
468
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.2.1",
4
- "stampSha256": "35a007d7fd7df22f277d9b3c6d58c2a723e3a7312eaafd851a0f771c18c844c5"
3
+ "version": "2.3.0",
4
+ "stampSha256": "d96276f51b1207530f41ee5233041c08280fe859446168454c5d5ea3a2cb0120"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
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.2.1"
59
+ "@openwop/spec-artifacts": "2.3.0"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 567,
6
- "tests": 2161,
7
- "withStableId": 2161,
5
+ "files": 569,
6
+ "tests": 2165,
7
+ "withStableId": 2165,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2116
9
+ "explicitIds": 2120
10
10
  },
11
11
  "records": [
12
12
  {
@@ -27720,6 +27720,115 @@
27720
27720
  }
27721
27721
  ]
27722
27722
  },
27723
+ {
27724
+ "id": "openwop.it.v2-payload-seats-0186.a-1-conversation-exchanged-is-one-union-that-both-host-shapes-satisfy-still-clos",
27725
+ "file": "v2-payload-seats-0186.test.ts",
27726
+ "line": 60,
27727
+ "title": "§A.1 conversation.exchanged is one union that both host shapes satisfy, still closed, and unambiguous inside the oneOf",
27728
+ "explicitId": "openwop.requirement.0186.payload-seats",
27729
+ "citations": [
27730
+ {
27731
+ "section": "RFCS/0186-payload-seats.md",
27732
+ "requirement": "the tier-1 host shape {conversationId, turnIndex, turn} MUST validate against the bound def"
27733
+ },
27734
+ {
27735
+ "section": "RFCS/0186-payload-seats.md",
27736
+ "requirement": "the tier-2 host shape {conversationId, turnIndex, outcome} MUST validate against the bound def"
27737
+ },
27738
+ {
27739
+ "section": "RFCS/0186-payload-seats.md",
27740
+ "requirement": "both turn and outcome are OPTIONAL — a payload with neither MUST validate, or the union is not a widening of the prior bound def"
27741
+ },
27742
+ {
27743
+ "section": "RFCS/0186-payload-seats.md",
27744
+ "requirement": "the union MUST stay closed — a bare unmodelled key still fails"
27745
+ },
27746
+ {
27747
+ "section": "RFCS/0186-payload-seats.md",
27748
+ "requirement": "the former orphan MUST be an alias of the union — the outcome-only shape validates through it"
27749
+ },
27750
+ {
27751
+ "section": "RFCS/0186-payload-seats.md",
27752
+ "requirement": "an exchanged payload MUST match exactly ONE branch of conversation-event oneOf"
27753
+ },
27754
+ {
27755
+ "section": "RFCS/0186-payload-seats.md",
27756
+ "requirement": "a closed payload MUST still match exactly one branch — the union MUST NOT absorb it (finalTurn is rejected by the closed union)"
27757
+ }
27758
+ ]
27759
+ },
27760
+ {
27761
+ "id": "openwop.it.v2-payload-seats-0186.a-2-reason-seats-on-interruptresolved-and-nodesuspended-and-neither-def-reopens",
27762
+ "file": "v2-payload-seats-0186.test.ts",
27763
+ "line": 76,
27764
+ "title": "§A.2 reason seats on interruptResolved and nodeSuspended, and neither def reopens",
27765
+ "explicitId": "openwop.requirement.0186.payload-seats",
27766
+ "citations": [
27767
+ {
27768
+ "section": "RFCS/0186-payload-seats.md",
27769
+ "requirement": null,
27770
+ "interpolated": true
27771
+ },
27772
+ {
27773
+ "section": "RFCS/0186-payload-seats.md",
27774
+ "requirement": null,
27775
+ "interpolated": true
27776
+ },
27777
+ {
27778
+ "section": "RFCS/0186-payload-seats.md",
27779
+ "requirement": null,
27780
+ "interpolated": true
27781
+ }
27782
+ ]
27783
+ },
27784
+ {
27785
+ "id": "openwop.it.v2-payload-seats-0186.a-3-approvaldata-ontimeout-is-a-closed-enum-and-the-def-stays-closed",
27786
+ "file": "v2-payload-seats-0186.test.ts",
27787
+ "line": 87,
27788
+ "title": "§A.3 ApprovalData.onTimeout is a closed enum and the def stays closed",
27789
+ "explicitId": "openwop.requirement.0186.payload-seats",
27790
+ "citations": [
27791
+ {
27792
+ "section": "RFCS/0186-payload-seats.md",
27793
+ "requirement": null,
27794
+ "interpolated": true
27795
+ },
27796
+ {
27797
+ "section": "RFCS/0186-payload-seats.md",
27798
+ "requirement": "onTimeout MUST refuse a value outside reject|approve|escalate — the domain came from a host type, not a sample"
27799
+ },
27800
+ {
27801
+ "section": "RFCS/0186-payload-seats.md",
27802
+ "requirement": "ApprovalData MUST still refuse the legacy bare `message` — it maps to `description`, which is the seat"
27803
+ }
27804
+ ]
27805
+ },
27806
+ {
27807
+ "id": "openwop.it.v2-payload-vendor-hatch.a-def-v1-left-open-carries-the-hatch-still-refuses-a-bare-key-and-a-deliberately",
27808
+ "file": "v2-payload-vendor-hatch.test.ts",
27809
+ "line": 55,
27810
+ "title": "a def v1 left open carries the hatch, still refuses a bare key, and a deliberately-closed def refuses both",
27811
+ "explicitId": "openwop.requirement.0185.payload-vendor-hatch",
27812
+ "citations": [
27813
+ {
27814
+ "section": "RFCS/0185-run-event-payload-closure.md §B",
27815
+ "requirement": null,
27816
+ "interpolated": true
27817
+ },
27818
+ {
27819
+ "section": "RFCS/0185-run-event-payload-closure.md §B",
27820
+ "requirement": "`variableChanged` MUST still REJECT a bare unmodelled key (`value`) — the hatch is an escape valve, not a reopening; `additionalProperties: false` stays"
27821
+ },
27822
+ {
27823
+ "section": "RFCS/0185-run-event-payload-closure.md §B",
27824
+ "requirement": "`interruptResolved` was open in v1, so its own hatch MUST admit a vendor key"
27825
+ },
27826
+ {
27827
+ "section": "RFCS/0185-run-event-payload-closure.md §B",
27828
+ "requirement": "a vendor key inside `refineFeedback` MUST be REFUSED — RFC 0183 modelled that object closed on purpose and it was never open in v1. The hatch belongs to the 53 defs that were narrowed, not to everything nested under them; this leg fails if a later edit sprays `patternProperties` down the tree"
27829
+ }
27830
+ ]
27831
+ },
27723
27832
  {
27724
27833
  "id": "openwop.it.v2-peer-dependency-declared.a-peerdependencies-key-the-declaration-file-does-not-name-is-refused-with-pack-p",
27725
27834
  "file": "v2-peer-dependency-declared.test.ts",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
3
3
  "counts": {
4
- "files": 518,
4
+ "files": 520,
5
5
  "v1": 445,
6
- "v2": 75
6
+ "v2": 77
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1361,6 +1361,12 @@
1361
1361
  "v2-payload-registry-closed.test.ts": [
1362
1362
  2
1363
1363
  ],
1364
+ "v2-payload-seats-0186.test.ts": [
1365
+ 2
1366
+ ],
1367
+ "v2-payload-vendor-hatch.test.ts": [
1368
+ 2
1369
+ ],
1364
1370
  "v2-peer-dependency-declared.test.ts": [
1365
1371
  2
1366
1372
  ],
@@ -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.2.1",
5
- "corpusTag": "v2.2.1",
4
+ "version": "2.3.0",
5
+ "corpusTag": "v2.3.0",
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": "a309a537a5b0aaa20c5f496943bb8cc6642b143eb27b7481ee0b9e1030d00bfb",
13
- "api/v2/asyncapi.yaml": "1f7023d581d02dba0427630dfd89ec890941ecbbc31bf8f09ce7ec8b965988b2",
14
- "api/v2/openapi.yaml": "f26603003e562b928c148ebaebe49b93c9bd35c01e0996611311114908858e5d",
12
+ "api/seams-v2.yaml": "4d90674341e8de143a3149d58303ae79e46dc44a1b1287a779a4690885775f9e",
13
+ "api/v2/asyncapi.yaml": "680960e1acaf412175cb57f5eaceb2bd42a443c216fb4f4be6394f7fc095f7cf",
14
+ "api/v2/openapi.yaml": "7cdd4e2aa97d8b042df5f0d53943d56b83c1ad00f0c9687fa0ab9e8d9dc9d822",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -125,7 +125,7 @@
125
125
  "schemas/v2/compensation-projection.schema.json": "f46977c7076347b5473faa8c4bbe73d8321c33b33c98fff2f26ee7cbada4ed98",
126
126
  "schemas/v2/configurable.schema.json": "33a6884047e2490f723733bf8b74d3b9164e00374cfea6dc3eac5a70dc368861",
127
127
  "schemas/v2/connection-pack-manifest.schema.json": "7410a3011b28829b286e3ff764bdb283854dca69ea4b71a8441f8c1c4f1cc948",
128
- "schemas/v2/conversation-event.schema.json": "80833d0bf72245118d23e5aebdf497aa5a4c79b0b53c262f38b6489753557f33",
128
+ "schemas/v2/conversation-event.schema.json": "e87b7fb36ab4ca0afe9f77993654065b28b67e5f24b20175907f90e238942025",
129
129
  "schemas/v2/conversation-turn.schema.json": "4c24aa85dd83f6ad00945b4a13ee8ff0a071893e884011e9289a07e97d22989d",
130
130
  "schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
131
131
  "schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
@@ -169,7 +169,7 @@
169
169
  "schemas/v2/residency.schema.json": "3fd23c9732beb140504e81560fc426eb6764d547e56da5fb92c5a5c7bf04db3a",
170
170
  "schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
171
171
  "schemas/v2/run-diff-response.schema.json": "8bdb2045c274580038bd74781f7d062f5049839797c164a89de4ed89edc53a74",
172
- "schemas/v2/run-event-payloads.schema.json": "38dd47929f0245c865e4850aab453e073bb717bc81590d68a91bd7b490536d63",
172
+ "schemas/v2/run-event-payloads.schema.json": "72f48eacb8aa3ed9b2f33c33de1f993c4cff4e086f943fd5793a1bb73c42335d",
173
173
  "schemas/v2/run-event.schema.json": "1a9946374af9323b71635d00fed22abe02c300fa2cf3ae3e9c30f3e553e408ec",
174
174
  "schemas/v2/run-list-response.schema.json": "c2dbc34768dcf64528914a213aa3cb55952e8e3e90a64b04a9423b2a19749d38",
175
175
  "schemas/v2/run-options.schema.json": "f0e9f494d80086c2ed2cf9fe87df54e5b4e66601699c4a1089f4edbc7ba58867",
@@ -181,7 +181,7 @@
181
181
  "schemas/v2/self-hosted-runner-result-frame.schema.json": "5a427362073720fb0e7b4a76143210083c017cac63e27eb3a91289bc7a443aaf",
182
182
  "schemas/v2/subject-link.schema.json": "1fc941d6c14af886534009f2ffa6611e2035eb07e30b1df918c50d6bafff026c",
183
183
  "schemas/v2/subject.schema.json": "094e981fb20052a911393e219ea5f2065d8b8417b65080a70c5ea99fd960a8fe",
184
- "schemas/v2/suspend-request.schema.json": "28451d35f4f49a075d26b2be7e0d1cd2287a196b3d41819edf146fdc8c862756",
184
+ "schemas/v2/suspend-request.schema.json": "4a0dda88a257cdb21c9b57fc9ad898d91de0d75bc4944a693eb950b709f58c91",
185
185
  "schemas/v2/tool-descriptor.schema.json": "658b18ef173e0f12d732f92d4866a9b3daeb8f88b493db8b025a577e09dd681c",
186
186
  "schemas/v2/trigger-event.schema.json": "7227fb9a130c33374fa8eab35e4448c4e8f22c962b8f271781cf4dbb5e4288b7",
187
187
  "schemas/v2/trigger-subscription-registration.schema.json": "04bc314a07c7e8b565577cb8df5821d968ce2af915900dfdccd4a24e928d20db",
@@ -200,7 +200,7 @@
200
200
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
201
201
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
202
202
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
203
- "spec/v1/core-standard-manifest.json": "9d76a090508697628031972795c813a12ff83b53ed1dff2dbaabe14d853b90c2",
203
+ "spec/v1/core-standard-manifest.json": "353689969f787aafdc836925f762fb988640e6cef14924bc0b6af7d7c02ed7a8",
204
204
  "spec/v1/deprecations.json": "a2022c2d6eb501bf406b5d75fe2d431a3fe0158e8509125d2453985eb1bf511a",
205
205
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
206
206
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -217,7 +217,7 @@
217
217
  "spec/v2/core/conformance.md": "622084e3c74c4549087f4d861b1bb98cec201a0c5aec23788d27f53a355fde3b",
218
218
  "spec/v2/core/connection-packs.md": "1d759681e2962f5103228c52c45f24607ed4bb9ae73e30605f690c1d5a59e9ee",
219
219
  "spec/v2/core/errors.md": "74fdb5862f432a831cf0c2eadbe0c8cca6b2660ec578180ae33db54773a1df28",
220
- "spec/v2/core/events.md": "3b7d1ad34f9e438c9825d60c0114b9a21c82a468f15ccbd7567022641a4537ad",
220
+ "spec/v2/core/events.md": "8162bb96730ef8a98b19eb180a0251a107fd629d993fe57345dacf81e53561ca",
221
221
  "spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
222
222
  "spec/v2/core/headers.md": "2dd67d71673539e16612f09f3f2513a384f6566052d7cd8a00885885689854c2",
223
223
  "spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
@@ -225,7 +225,7 @@
225
225
  "spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
226
226
  "spec/v2/core/interrupt.md": "f525479a99104655219fa2e51c4cad2a03d8e3be93ff4dac173a684675e57348",
227
227
  "spec/v2/core/overview.md": "69377c90bbb87b6bfe4b8be4f1b65810094b0759c8d4f93ca6df92cc530ea023",
228
- "spec/v2/core/packs.md": "e72a7586a61630cdfd342eaf84e989f864b3e199006f5511ef59ea7c1a1c56f7",
228
+ "spec/v2/core/packs.md": "8b3e1587270bb1aec814befbd287181a05b7f76aeb0802787ee7ebdc85901205",
229
229
  "spec/v2/core/persistence.md": "2373c2815b25b7b4fe29dfdf3783d3f2f3e637d3abce5085e69f019546302fb6",
230
230
  "spec/v2/core/replay.md": "bd191a2565aa7ad0df9b35a0716f3de53be34b8722f1807c5e5b02859404a9b9",
231
231
  "spec/v2/core/runs.md": "f7a95fc82fda5b7c0c613de6e5edd376bbd6d8ba4c665a500c94eacbe42b86f2",
@@ -268,10 +268,10 @@
268
268
  "spec/v2/facets/webhooks.schema.json": "0b766033ef4b4c6c2ac6f9979a19843ba024d3a1d1576972dcb55aaba52f1b9a",
269
269
  "spec/v2/id-field-bindings.json": "eecadfd11581ba2e075ccb320d85fc033b816edfdd51edb5c5727a181e55dcb4",
270
270
  "spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
271
- "spec/v2/peer-dependency-aliases.json": "10fcab53c65305e9b6bae4a40d30176091dca68739ac8a78291510c77b50ca6b",
271
+ "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
272
272
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
273
- "spec/v2/release.json": "ee596fce1d27e7ec3f7a061b5705ca8022158adee5ba1aa7f321ee9abd97e9fb",
273
+ "spec/v2/release.json": "4dac86d9d990d76f5e0971eb5e1b55d366ad9372e07e68a67a4012dce56be377",
274
274
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
275
275
  },
276
- "corpusCommit": "a27e18972293865c72e4a776426718be6e2d77cc"
276
+ "corpusCommit": "c4d3f8528dc380210f87a94d595fff246775c00a"
277
277
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * RFC 0186 — three payload seats the hosts measured and the corpus lacked
3
+ * (suite 2.3.0, target major 2; server-free).
4
+ *
5
+ * §A.1 `conversation.exchanged` had TWO closed defs for one event that could not
6
+ * both be satisfied: `run-event-payloads.conversationExchanged` seated `outcome`
7
+ * and not `turn`; `conversation-event.ConversationExchangedPayload` seated
8
+ * `turn` (required) and not `outcome`. The codemap bound the first; a tier-1
9
+ * host emitted the second. The other host emits `outcome` (the `ctx.suspend`
10
+ * resume value) and never `turn`. The union carries both, optional — a strict
11
+ * widening of both prior shapes — and the orphan is an alias.
12
+ *
13
+ * §A.2 `reason` — a CAUSE axis on `interruptResolved` and `nodeSuspended`,
14
+ * disjoint from `kind`, RFC 0183 `decision` and `action`. One host measured it
15
+ * single-valued `timeout`; the other dropped it on 350 rows.
16
+ *
17
+ * §A.3 `ApprovalData.onTimeout` — `reject | approve | escalate`, the disposition
18
+ * when `timeoutMs` elapses. Both hosts recorded it as a bare key on
19
+ * `approval.requested` because the seat did not exist.
20
+ *
21
+ * The load-bearing legs are the NEGATIVE ones: the union still refuses a bare
22
+ * key, the alias sitting inside `conversation-event`'s `oneOf` cannot
23
+ * double-match `opened`/`closed`, and `onTimeout` refuses a value outside the
24
+ * enum. Each is sabotage-checked.
25
+ *
26
+ * @see RFCS/0186-payload-seats.md
27
+ */
28
+ import { describe, it, expect } from 'vitest';
29
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+ import { Ajv2020 } from 'ajv/dist/2020.js';
32
+ import addFormats from 'ajv-formats';
33
+ import { SCHEMAS_DIR } from '../lib/paths.js';
34
+ import { req } from '../lib/requirement-ids.js';
35
+
36
+ const ID = 'openwop.requirement.0186.payload-seats';
37
+ const DOC = 'RFCS/0186-payload-seats.md';
38
+
39
+ function ajv(): Ajv2020 {
40
+ const a = new Ajv2020({ strict: false, allErrors: true });
41
+ addFormats(a);
42
+ const dir = join(SCHEMAS_DIR, 'v2');
43
+ for (const f of readdirSync(dir)) {
44
+ if (!f.endsWith('.schema.json') || statSync(join(dir, f)).isDirectory()) continue;
45
+ try { a.addSchema(JSON.parse(readFileSync(join(dir, f), 'utf8')) as Record<string, unknown>); } catch { /* duplicate $id */ }
46
+ }
47
+ return a;
48
+ }
49
+ // No cast: a compiled validator is already callable. `compile()` returns
50
+ // `ValidateFunction`, whose call result is `boolean` for a synchronous schema —
51
+ // `=== true` narrows away the async branch without asserting anything.
52
+ const v = (a: Ajv2020, ref: string): ((doc: unknown) => boolean) => { const fn = a.compile({ $ref: ref }); return (doc) => fn(doc) === true; };
53
+ const PAYLOADS = 'https://openwop.dev/spec/v2/run-event-payloads.schema.json#/$defs/';
54
+ const CONV = 'https://openwop.dev/spec/v2/conversation-event.schema.json';
55
+ const SUSPEND = 'https://openwop.dev/spec/v2/suspend-request.schema.json#/$defs/';
56
+
57
+ const turn = { messageId: 'm1', from: 'user', content: 'hello', ts: 1758110400000, role: 'user', turnIndex: 0 };
58
+
59
+ describe('RFC 0186 payload seats', () => {
60
+ it('§A.1 conversation.exchanged is one union that both host shapes satisfy, still closed, and unambiguous inside the oneOf', () => {
61
+ const a = ajv();
62
+ const exchanged = v(a, `${PAYLOADS}conversationExchanged`);
63
+ expect(exchanged({ conversationId: 'c1', turnIndex: 0, turn }), req(ID, DOC, 'the tier-1 host shape {conversationId, turnIndex, turn} MUST validate against the bound def')).toBe(true);
64
+ expect(exchanged({ conversationId: 'c1', turnIndex: 0, outcome: { ok: true } }), req(ID, DOC, 'the tier-2 host shape {conversationId, turnIndex, outcome} MUST validate against the bound def')).toBe(true);
65
+ expect(exchanged({ conversationId: 'c1', turnIndex: 0 }), req(ID, DOC, 'both turn and outcome are OPTIONAL — a payload with neither MUST validate, or the union is not a widening of the prior bound def')).toBe(true);
66
+ expect(exchanged({ conversationId: 'c1', turnIndex: 0, notASeat: 1 }), req(ID, DOC, 'the union MUST stay closed — a bare unmodelled key still fails')).toBe(false);
67
+
68
+ const alias = v(a, `${CONV}#/$defs/ConversationExchangedPayload`);
69
+ expect(alias({ conversationId: 'c1', turnIndex: 0, outcome: 1 }), req(ID, DOC, 'the former orphan MUST be an alias of the union — the outcome-only shape validates through it')).toBe(true);
70
+
71
+ const oneOf = v(a, CONV);
72
+ expect(oneOf({ conversationId: 'c1', turnIndex: 0, turn }), req(ID, DOC, 'an exchanged payload MUST match exactly ONE branch of conversation-event oneOf')).toBe(true);
73
+ expect(oneOf({ conversationId: 'c1', turnIndex: 3, finalTurn: turn }), req(ID, DOC, 'a closed payload MUST still match exactly one branch — the union MUST NOT absorb it (finalTurn is rejected by the closed union)')).toBe(true);
74
+ });
75
+
76
+ it('§A.2 reason seats on interruptResolved and nodeSuspended, and neither def reopens', () => {
77
+ const a = ajv();
78
+ const base = { nodeId: 'n1', interruptId: 't/0123456789abcdef', kind: 'approval' };
79
+ for (const def of ['interruptResolved', 'nodeSuspended']) {
80
+ const f = v(a, `${PAYLOADS}${def}`);
81
+ expect(f({ ...base, reason: 'timeout' }), req(ID, DOC, `\`${def}\` MUST admit reason — the cause axis both hosts record`)).toBe(true);
82
+ expect(f({ ...base, reason: '' }), req(ID, DOC, `\`${def}.reason\` MUST be non-empty`)).toBe(false);
83
+ expect(f({ ...base, cause: 'timeout' }), req(ID, DOC, `\`${def}\` MUST still refuse a bare unmodelled key`)).toBe(false);
84
+ }
85
+ });
86
+
87
+ it('§A.3 ApprovalData.onTimeout is a closed enum and the def stays closed', () => {
88
+ const a = ajv();
89
+ const f = v(a, `${SUSPEND}ApprovalData`);
90
+ const base = { artifactId: 'a1', artifactType: 'doc', title: 'Review', actions: ['accept', 'reject'] };
91
+ for (const d of ['reject', 'approve', 'escalate']) {
92
+ expect(f({ ...base, onTimeout: d }), req(ID, DOC, `onTimeout MUST admit \`${d}\``)).toBe(true);
93
+ }
94
+ expect(f({ ...base, onTimeout: 'ignore' }), req(ID, DOC, 'onTimeout MUST refuse a value outside reject|approve|escalate — the domain came from a host type, not a sample')).toBe(false);
95
+ expect(f({ ...base, message: 'x' }), req(ID, DOC, 'ApprovalData MUST still refuse the legacy bare `message` — it maps to `description`, which is the seat')).toBe(false);
96
+ });
97
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `spec/v2/core/events.md` §Era-2 + RFC 0185 §B — a run-event payload def that
3
+ * v1 left open carries a vendor hatch, so a host has a CONFORMING place for a
4
+ * fact the corpus does not model (suite 2.2.3, target major 2; server-free).
5
+ *
6
+ * v1's `run-event-payloads.schema.json` declares `additionalProperties: true`.
7
+ * v2's declares `false` on the same defs — 53 of them — with no hatch and no
8
+ * migration row. Every extra key a host legitimately recorded under v1, because
9
+ * v1 explicitly invited them, became invalid at the cut with nowhere to go.
10
+ *
11
+ * Two production hosts lost data to that independently: one drops 32 distinct
12
+ * keys at the major-2 read (the conversation content among them), the other
13
+ * projects era-3 rows ON WRITE and lost a variable's value AT REST. Both had a
14
+ * validator that got happier the more they deleted.
15
+ *
16
+ * The load-bearing leg is the THIRD one. A hatch that accepts a vendor key is
17
+ * satisfied by spraying `patternProperties` across every def in the file; what
18
+ * makes it a scoped decision is that a def closed DELIBERATELY in v2 still
19
+ * refuses one. `refineFeedback` is that def — RFC 0183 modelled it closed on
20
+ * purpose — and this leg fails if a later edit hatches it by reflex.
21
+ *
22
+ * @see RFCS/0185-run-event-payload-closure.md §B
23
+ * @see spec/v2/core/events.md §Era-2 logs
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { Ajv2020 } from 'ajv/dist/2020.js';
30
+ import addFormats from 'ajv-formats';
31
+ import { SCHEMAS_DIR } from '../lib/paths.js';
32
+ import { req } from '../lib/requirement-ids.js';
33
+
34
+ const ID = 'openwop.requirement.0185.payload-vendor-hatch';
35
+ const DOC = 'RFCS/0185-run-event-payload-closure.md §B';
36
+
37
+ /** Compile a validator for ONE `$def` of the payload schema. */
38
+ function defValidator(def: string): (doc: unknown) => boolean {
39
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
40
+ addFormats(ajv);
41
+ // Register the whole v2 tree: the payload defs $ref `ids.schema.json` for
42
+ // their id kinds, so compiling the file alone cannot resolve.
43
+ const dir = join(SCHEMAS_DIR, 'v2');
44
+ for (const f of readdirSync(dir)) {
45
+ if (!f.endsWith('.schema.json') || statSync(join(dir, f)).isDirectory()) continue;
46
+ try { ajv.addSchema(JSON.parse(readFileSync(join(dir, f), 'utf8')) as Record<string, unknown>); } catch { /* duplicate $id */ }
47
+ }
48
+ // No cast (code-review banned-pattern): the compiled validator is callable
49
+ // and returns `boolean` for a synchronous schema; `=== true` narrows it.
50
+ const fn = ajv.compile({ $ref: `https://openwop.dev/spec/v2/run-event-payloads.schema.json#/$defs/${def}` });
51
+ return (doc: unknown): boolean => fn(doc) === true;
52
+ }
53
+
54
+ describe('v2 payload vendor hatch (RFC 0185 §B)', () => {
55
+ it('a def v1 left open carries the hatch, still refuses a bare key, and a deliberately-closed def refuses both', () => {
56
+ const variableChanged = defValidator('variableChanged');
57
+ const base = { nodeId: 'n1', name: 'total', previous: 1, next: 2 };
58
+
59
+ // 1. the hatch admits a vendor-prefixed key
60
+ for (const key of ['vendor.myndhyve.value', 'x-legacy-value', 'openwop-internal']) {
61
+ expect(
62
+ variableChanged({ ...base, [key]: 'carried' }),
63
+ req(ID, DOC, `\`variableChanged\` MUST admit the hatched property \`${key}\` — v1 was additionalProperties:true, so a host may hold keys v2 does not name and needs a conforming home for them`),
64
+ ).toBe(true);
65
+ }
66
+
67
+ // 2. and the closure it was made for still holds
68
+ expect(
69
+ variableChanged({ ...base, value: 2 }),
70
+ req(ID, DOC, '`variableChanged` MUST still REJECT a bare unmodelled key (`value`) — the hatch is an escape valve, not a reopening; `additionalProperties: false` stays'),
71
+ ).toBe(false);
72
+
73
+ // 3. THE LOAD-BEARING LEG: the hatch is scoped to the DEF, not inherited by
74
+ // the objects nested inside it. `interruptResolved` was open in v1 and is
75
+ // hatched; `refineFeedback` sits INSIDE it, was modelled closed on purpose
76
+ // by RFC 0183, and was never open in v1 — so it is outside §B.
77
+ const interruptResolved = defValidator('interruptResolved');
78
+ const resolved = { interruptId: 't/0123456789abcdef', nodeId: 'n1', kind: 'approval' };
79
+
80
+ expect(
81
+ interruptResolved({ ...resolved, 'vendor.example.extra': 1 }),
82
+ req(ID, DOC, '`interruptResolved` was open in v1, so its own hatch MUST admit a vendor key'),
83
+ ).toBe(true);
84
+
85
+ expect(
86
+ interruptResolved({ ...resolved, action: 'refine', refineFeedback: { scope: 'whole', 'vendor.example.extra': 1 } }),
87
+ req(ID, DOC, 'a vendor key inside `refineFeedback` MUST be REFUSED — RFC 0183 modelled that object closed on purpose and it was never open in v1. The hatch belongs to the 53 defs that were narrowed, not to everything nested under them; this leg fails if a later edit sprays `patternProperties` down the tree'),
88
+ ).toBe(false);
89
+ });
90
+ });