@openwop/openwop-conformance 2.2.3 → 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 +17 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +88 -5
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +11 -11
- package/src/scenarios/v2-payload-seats-0186.test.ts +97 -0
- package/src/scenarios/v2-payload-vendor-hatch.test.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
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
|
+
|
|
3
20
|
## [2.2.3] — 2026-09-16 — a payload def that v1 left open gets a conforming escape hatch
|
|
4
21
|
|
|
5
22
|
**Why a patch.** `PUBLISHING.md` §"Versioning alignment": one scenario file is
|
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
|
|
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:
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
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":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 569,
|
|
6
|
+
"tests": 2165,
|
|
7
|
+
"withStableId": 2165,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2120
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -27720,10 +27720,93 @@
|
|
|
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
|
+
},
|
|
27723
27806
|
{
|
|
27724
27807
|
"id": "openwop.it.v2-payload-vendor-hatch.a-def-v1-left-open-carries-the-hatch-still-refuses-a-bare-key-and-a-deliberately",
|
|
27725
27808
|
"file": "v2-payload-vendor-hatch.test.ts",
|
|
27726
|
-
"line":
|
|
27809
|
+
"line": 55,
|
|
27727
27810
|
"title": "a def v1 left open carries the hatch, still refuses a bare key, and a deliberately-closed def refuses both",
|
|
27728
27811
|
"explicitId": "openwop.requirement.0185.payload-vendor-hatch",
|
|
27729
27812
|
"citations": [
|
package/scenario-majors.json
CHANGED
|
@@ -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":
|
|
4
|
+
"files": 520,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 77
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1361,6 +1361,9 @@
|
|
|
1361
1361
|
"v2-payload-registry-closed.test.ts": [
|
|
1362
1362
|
2
|
|
1363
1363
|
],
|
|
1364
|
+
"v2-payload-seats-0186.test.ts": [
|
|
1365
|
+
2
|
|
1366
|
+
],
|
|
1364
1367
|
"v2-payload-vendor-hatch.test.ts": [
|
|
1365
1368
|
2
|
|
1366
1369
|
],
|
|
@@ -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.
|
|
5
|
-
"corpusTag": "v2.
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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",
|
|
@@ -270,8 +270,8 @@
|
|
|
270
270
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
271
271
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
272
272
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
273
|
-
"spec/v2/release.json": "
|
|
273
|
+
"spec/v2/release.json": "4dac86d9d990d76f5e0971eb5e1b55d366ad9372e07e68a67a4012dce56be377",
|
|
274
274
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
275
275
|
},
|
|
276
|
-
"corpusCommit": "
|
|
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
|
+
});
|
|
@@ -45,7 +45,10 @@ function defValidator(def: string): (doc: unknown) => boolean {
|
|
|
45
45
|
if (!f.endsWith('.schema.json') || statSync(join(dir, f)).isDirectory()) continue;
|
|
46
46
|
try { ajv.addSchema(JSON.parse(readFileSync(join(dir, f), 'utf8')) as Record<string, unknown>); } catch { /* duplicate $id */ }
|
|
47
47
|
}
|
|
48
|
-
|
|
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;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
describe('v2 payload vendor hatch (RFC 0185 §B)', () => {
|