@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 +57 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +113 -4
- package/scenario-majors.json +8 -2
- package/schemas/CORPUS-STAMP.json +14 -14
- package/src/scenarios/v2-payload-seats-0186.test.ts +97 -0
- package/src/scenarios/v2-payload-vendor-hatch.test.ts +90 -0
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
|
|
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,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",
|
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,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.
|
|
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",
|
|
@@ -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": "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
|
+
});
|
|
@@ -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
|
+
});
|