@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 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 518 scenario files under `src/scenarios/`.
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: 518 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
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
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.2.1",
4
- "stampSha256": "35a007d7fd7df22f277d9b3c6d58c2a723e3a7312eaafd851a0f771c18c844c5"
3
+ "version": "2.2.3",
4
+ "stampSha256": "69115713bb818c82018807f79b5c45d759b3fd8d434e81d386b005fc90ca4e6c"
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.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.1"
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": 567,
6
- "tests": 2161,
7
- "withStableId": 2161,
5
+ "files": 568,
6
+ "tests": 2162,
7
+ "withStableId": 2162,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2116
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",
@@ -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": 519,
5
5
  "v1": 445,
6
- "v2": 75
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.1",
5
- "corpusTag": "v2.2.1",
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": "a309a537a5b0aaa20c5f496943bb8cc6642b143eb27b7481ee0b9e1030d00bfb",
13
- "api/v2/asyncapi.yaml": "1f7023d581d02dba0427630dfd89ec890941ecbbc31bf8f09ce7ec8b965988b2",
14
- "api/v2/openapi.yaml": "f26603003e562b928c148ebaebe49b93c9bd35c01e0996611311114908858e5d",
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": "38dd47929f0245c865e4850aab453e073bb717bc81590d68a91bd7b490536d63",
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": "9d76a090508697628031972795c813a12ff83b53ed1dff2dbaabe14d853b90c2",
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": "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": "9a258b0740e7c26f74dbd56b2816407b7d0ac39cff2873b20e8171f3d8e5239b",
274
274
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
275
275
  },
276
- "corpusCommit": "a27e18972293865c72e4a776426718be6e2d77cc"
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
+ });