@openwop/openwop-conformance 2.2.1 → 2.2.3
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 +40 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +30 -4
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +12 -12
- package/src/scenarios/v2-payload-vendor-hatch.test.ts +87 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.3] — 2026-09-16 — a payload def that v1 left open gets a conforming escape hatch
|
|
4
|
+
|
|
5
|
+
**Why a patch.** `PUBLISHING.md` §"Versioning alignment": one scenario file is
|
|
6
|
+
added, which is a minor — but `v2-payload-vendor-hatch.test.ts` is server-free
|
|
7
|
+
and additive to a corpus rule, and no existing scenario changes. Cut as a patch
|
|
8
|
+
alongside the spec edit it witnesses.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`v2-payload-vendor-hatch.test.ts`** (server-free) — a def v1 left open
|
|
13
|
+
admits `vendor.*` / `x-` / `openwop-` keys; still **refuses** a bare
|
|
14
|
+
unmodelled key, because the hatch is an escape valve rather than a reopening;
|
|
15
|
+
and a vendor key nested inside `refineFeedback` is **refused**, since RFC 0183
|
|
16
|
+
modelled that object closed on purpose and it was never open in v1. That third
|
|
17
|
+
leg is the load-bearing one: it fails if a later edit sprays
|
|
18
|
+
`patternProperties` down the tree instead of applying it to the 53 narrowed
|
|
19
|
+
defs. Sabotage-proved both ways.
|
|
20
|
+
|
|
21
|
+
## [2.2.2] — 2026-09-16 — a registry tree that says "read-only" when it means "deliberately behind"
|
|
22
|
+
|
|
23
|
+
**Why a patch.** `PUBLISHING.md` §"Versioning alignment": no scenario file is
|
|
24
|
+
added or removed; this is spec prose plus packaging metadata.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`spec/v2/core/packs.md` §The registry tree** now says the v1 tree is
|
|
29
|
+
*"frozen through the overlap, deliberately behind this one"* rather than
|
|
30
|
+
*"served read-only through the overlap."* A tier-1 host operator read
|
|
31
|
+
"read-only" as *current, just not accepting writes*, resolved
|
|
32
|
+
`packs.openwop.dev/v1/index.json` from a major-2 host, and reported 154 of 156
|
|
33
|
+
packs drifted; a second session reproduced and confirmed it. Against
|
|
34
|
+
`registry/v2/` the drift was **zero**. The MUST that prevents this was already
|
|
35
|
+
in the next sentence — a client MUST resolve registry paths through
|
|
36
|
+
`endpoints` rather than construct one — but nothing said that constructing the
|
|
37
|
+
v1 path returns deliberately-older versions instead of failing.
|
|
38
|
+
- **`package-lock.json`** pinned the `@openwop/spec-artifacts` peer at
|
|
39
|
+
`2.0.0-rc.0` while `package.json` tracked the current release. Consumers
|
|
40
|
+
installing from the lockfile resolved a peer range two majors' worth of
|
|
41
|
+
prereleases behind the one the package declares.
|
|
42
|
+
|
|
3
43
|
## [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
44
|
|
|
5
45
|
**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
|
|
120
|
+
The current suite has 519 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: 519 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.2.
|
|
3
|
+
"version": "2.2.3",
|
|
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.
|
|
59
|
+
"@openwop/spec-artifacts": "2.2.3"
|
|
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": 568,
|
|
6
|
+
"tests": 2162,
|
|
7
|
+
"withStableId": 2162,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2117
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -27720,6 +27720,32 @@
|
|
|
27720
27720
|
}
|
|
27721
27721
|
]
|
|
27722
27722
|
},
|
|
27723
|
+
{
|
|
27724
|
+
"id": "openwop.it.v2-payload-vendor-hatch.a-def-v1-left-open-carries-the-hatch-still-refuses-a-bare-key-and-a-deliberately",
|
|
27725
|
+
"file": "v2-payload-vendor-hatch.test.ts",
|
|
27726
|
+
"line": 52,
|
|
27727
|
+
"title": "a def v1 left open carries the hatch, still refuses a bare key, and a deliberately-closed def refuses both",
|
|
27728
|
+
"explicitId": "openwop.requirement.0185.payload-vendor-hatch",
|
|
27729
|
+
"citations": [
|
|
27730
|
+
{
|
|
27731
|
+
"section": "RFCS/0185-run-event-payload-closure.md §B",
|
|
27732
|
+
"requirement": null,
|
|
27733
|
+
"interpolated": true
|
|
27734
|
+
},
|
|
27735
|
+
{
|
|
27736
|
+
"section": "RFCS/0185-run-event-payload-closure.md §B",
|
|
27737
|
+
"requirement": "`variableChanged` MUST still REJECT a bare unmodelled key (`value`) — the hatch is an escape valve, not a reopening; `additionalProperties: false` stays"
|
|
27738
|
+
},
|
|
27739
|
+
{
|
|
27740
|
+
"section": "RFCS/0185-run-event-payload-closure.md §B",
|
|
27741
|
+
"requirement": "`interruptResolved` was open in v1, so its own hatch MUST admit a vendor key"
|
|
27742
|
+
},
|
|
27743
|
+
{
|
|
27744
|
+
"section": "RFCS/0185-run-event-payload-closure.md §B",
|
|
27745
|
+
"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"
|
|
27746
|
+
}
|
|
27747
|
+
]
|
|
27748
|
+
},
|
|
27723
27749
|
{
|
|
27724
27750
|
"id": "openwop.it.v2-peer-dependency-declared.a-peerdependencies-key-the-declaration-file-does-not-name-is-refused-with-pack-p",
|
|
27725
27751
|
"file": "v2-peer-dependency-declared.test.ts",
|
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": 519,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 76
|
|
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-vendor-hatch.test.ts": [
|
|
1365
|
+
2
|
|
1366
|
+
],
|
|
1364
1367
|
"v2-peer-dependency-declared.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.2.
|
|
5
|
-
"corpusTag": "v2.2.
|
|
4
|
+
"version": "2.2.3",
|
|
5
|
+
"corpusTag": "v2.2.3",
|
|
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": "43a0aa0f08070b9d51a1651d5edc96f936ed814617bb0af85eac2b7186711295",
|
|
13
|
+
"api/v2/asyncapi.yaml": "a32add439289805a6303b36f31ff54ff9c728dea1e9a98ab417b6b339215f025",
|
|
14
|
+
"api/v2/openapi.yaml": "8b8c18154b77775f9d11918a3182e6c4396f2a4c2065bee0dbe7056dafc775da",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -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": "3f6c256871970cdfa789d4b7e353bc96975cc17319e769c73d2dfd80fbc9670c",
|
|
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",
|
|
@@ -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": "9f5501b6c3406ace7df7c4a53501d47745480df76b65d65ac2d298045a73d41d",
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "9a258b0740e7c26f74dbd56b2816407b7d0ac39cff2873b20e8171f3d8e5239b",
|
|
274
274
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
275
275
|
},
|
|
276
|
-
"corpusCommit": "
|
|
276
|
+
"corpusCommit": "f3d945720da0143ed932f50281ebe94c3162c063"
|
|
277
277
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
return ajv.compile({ $ref: `https://openwop.dev/spec/v2/run-event-payloads.schema.json#/$defs/${def}` }) as unknown as (doc: unknown) => boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('v2 payload vendor hatch (RFC 0185 §B)', () => {
|
|
52
|
+
it('a def v1 left open carries the hatch, still refuses a bare key, and a deliberately-closed def refuses both', () => {
|
|
53
|
+
const variableChanged = defValidator('variableChanged');
|
|
54
|
+
const base = { nodeId: 'n1', name: 'total', previous: 1, next: 2 };
|
|
55
|
+
|
|
56
|
+
// 1. the hatch admits a vendor-prefixed key
|
|
57
|
+
for (const key of ['vendor.myndhyve.value', 'x-legacy-value', 'openwop-internal']) {
|
|
58
|
+
expect(
|
|
59
|
+
variableChanged({ ...base, [key]: 'carried' }),
|
|
60
|
+
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`),
|
|
61
|
+
).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. and the closure it was made for still holds
|
|
65
|
+
expect(
|
|
66
|
+
variableChanged({ ...base, value: 2 }),
|
|
67
|
+
req(ID, DOC, '`variableChanged` MUST still REJECT a bare unmodelled key (`value`) — the hatch is an escape valve, not a reopening; `additionalProperties: false` stays'),
|
|
68
|
+
).toBe(false);
|
|
69
|
+
|
|
70
|
+
// 3. THE LOAD-BEARING LEG: the hatch is scoped to the DEF, not inherited by
|
|
71
|
+
// the objects nested inside it. `interruptResolved` was open in v1 and is
|
|
72
|
+
// hatched; `refineFeedback` sits INSIDE it, was modelled closed on purpose
|
|
73
|
+
// by RFC 0183, and was never open in v1 — so it is outside §B.
|
|
74
|
+
const interruptResolved = defValidator('interruptResolved');
|
|
75
|
+
const resolved = { interruptId: 't/0123456789abcdef', nodeId: 'n1', kind: 'approval' };
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
interruptResolved({ ...resolved, 'vendor.example.extra': 1 }),
|
|
79
|
+
req(ID, DOC, '`interruptResolved` was open in v1, so its own hatch MUST admit a vendor key'),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
|
|
82
|
+
expect(
|
|
83
|
+
interruptResolved({ ...resolved, action: 'refine', refineFeedback: { scope: 'whole', 'vendor.example.extra': 1 } }),
|
|
84
|
+
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'),
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|