@openwop/openwop-conformance 2.46.1 → 2.46.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,23 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.46.3] — 2026-10-12 — discovery rules the schema cannot carry
4
+
5
+ Four rules from openwop-app's v2 discovery review (#4676, ADR 0858, ADR 0812). Each is stated in prose or a schema description, and none can go into the schema within 2.x: a conditional `required` or a narrowed pattern is a major change (COMPATIBILITY.md §2.4). The suite carries them, following RFC 0210 §C; the schema changes are booked for 3.0 (TODO.md).
6
+
7
+ - **New `v2-multi-agent-advertisement`** (claims-check, wave 4 of `family-advert-witness.ts`):
8
+ - `multiAgent.level3-cross-host-causation`: at `executionModel.version` >= 3 the host advertises `crossHostCausation` with a non-empty `hostId`. The levels are cumulative, so the description's "`version: 3`" now reads "3 or higher".
9
+ - `multiAgent.level4-replay-determinism`: at >= 4, `replayDeterminism` with `refusalDivergenceEmission: true`.
10
+ - `multiAgent.escalation-kind-canonical`: `confidenceEscalationInterruptKind` is `clarification` or `approval`. Level 2 fires one of the two, and the v2 interrupt `kind` is closed, so an `x-host-` value promises an interrupt the payload schema rejects. The description no longer points at `spec/v1/host-extensions.md` or an RFC 0044 §C kind-mapping document.
11
+ - **New `v2-auth-subject-link-key`:** a host advertising both the `saml` and `scim` lanes advertises `auth.subjectLinkKey` (`identity.md` §3 now says MUST, as RFC 0164 §A.3 did at v1). Forbidding the key with one lane is a new rule and needs an RFC.
12
+ - **Fan-out has no v2 seat.** The `dispatch-config` and `orchestrator-decision` descriptions named `capabilities.dispatch.*` and `POST /v1/workflows`. They now say plainly that a v2 host refuses `fanOutPolicy: "parallel"` and a non-empty `nextWorkerInputs` until a seat is added. That was already the rule, because the seat could not be advertised. `dispatch-config.schema.json` is hand-owned from here. Adding one needs an RFC.
13
+ - **What a host's next bundle shows:** MyndHyve advertises `x-host-myndhyve-low-confidence` and will record `escalation-kind-canonical` `executed-fail`. Its committed bundles stay valid measurements at their suite versions (§2.3). openwop-app (level 5) and the v2 reference host pass or are `inapplicable`.
14
+
15
+ ## [2.46.2] — 2026-10-12 — the seams profile is derived from its advert
16
+
17
+ - **`openwop-conformance-seams-v2` derives from `conformance.seamsProfile`, not from any `conformance` block.** The predicate was the bare `conformance` metadata key, which also carries `certificationBundleUrl` and `mockAgent`. A host that published only a bundle URL claimed the profile, so its claim flapped with whether the URL was set (openwop-app, 7 cuts on 2.45.30 with no host change), and four openwop-app bundles (`openwop-workflow-engine-2.35.1`, `-2.43.0`, `-2.44.0`, `-2.45.28-4683d9f89`) certified it with no seam mounted. A predicate `metadata` entry may now name one member, `key.member` (capabilities.md §7); `v2ProfileIds` and `v2-profiles-derived-only` share one `metadataPresent`.
18
+ - **What a host's next bundle shows:** a host with a `conformance` block but no `seamsProfile` no longer lists the profile. `--verify` of the four bundles above now refuses their seams-v2 certification (`profile-not-derivable`, scoped to that profile); their other certifications stand. The v2 reference host and MyndHyve are unaffected.
19
+ - **`check-declaration` rule 13:** every predicate entry resolves to a declared family, a kept metadata key, or `key.member` naming a property of that key's generated schema.
20
+
3
21
  ## [2.46.1] — 2026-10-11 — the modal webhook receiver closes through a tunnel
4
22
 
5
23
  - **`startModalReceiver().close()` no longer hangs on a keep-alive socket.** Through a public tunnel the tunnel client holds a keep-alive connection open, and a bare `server.close()` waits for it: the v2 reference host's certified public cut on 2.46.0 recorded `v2-webhook-message-id-stable` `executed-fail` with "Hook timed out in 30000ms" in its `afterEach`, while the host's deliveries were correct (the loopback rehearsal passed the same leg). `close()` now closes idle and then all connections, the idiom `scoped-receiver.ts` already used. No assertion changes.
package/README.md CHANGED
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
137
137
 
138
138
  ## What's Covered
139
139
 
140
- The current suite has 653 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 213 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
140
+ The current suite has 655 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 215 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
141
141
 
142
142
  The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
143
143
 
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
166
166
  - **Host** checks drive a live host over HTTP.
167
167
  - **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
168
168
 
169
- Current source tree: 653 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
169
+ Current source tree: 655 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
170
170
 
171
171
  ## How the 2.x suite is built
172
172
 
@@ -68,12 +68,30 @@ function readRegistry() {
68
68
  export function v2RegistryAvailable() {
69
69
  return readRegistry() !== null;
70
70
  }
71
+ /**
72
+ * Is one predicate `metadata` entry present in `doc`? An entry is a metadata
73
+ * key, or `key.member` for one member of it (capabilities.md §7).
74
+ *
75
+ * The member form exists for the seams profile. Its advert is
76
+ * `conformance.seamsProfile` (conformance.md §The seams profile), but the
77
+ * `conformance` block also carries `certificationBundleUrl` and `mockAgent`.
78
+ * Until 2.46.2 the predicate was the bare key, so a host that published only a
79
+ * bundle URL claimed the profile, and four openwop-app bundles certified it
80
+ * with no seam mounted.
81
+ */
82
+ export function metadataPresent(doc, entry) {
83
+ const dot = entry.indexOf('.');
84
+ if (dot < 0)
85
+ return doc[entry] !== undefined;
86
+ const v = doc[entry.slice(0, dot)];
87
+ return typeof v === 'object' && v !== null && !Array.isArray(v) && v[entry.slice(dot + 1)] !== undefined;
88
+ }
71
89
  /**
72
90
  * Every profile the declaration derives at major 2, or `null` when the registry
73
91
  * is unavailable (see the module docblock — `null` is not `[]`).
74
92
  *
75
93
  * The predicate is the registry's own: every listed family present as a record,
76
- * every listed metadata key present at the root. Nothing here is hand-written
94
+ * every listed metadata entry present ({@link metadataPresent}). Nothing here is hand-written
77
95
  * per profile, so adding a v2 profile is a corpus edit and not a code edit.
78
96
  */
79
97
  export function v2ProfileIds(doc) {
@@ -91,7 +109,7 @@ export function v2ProfileIds(doc) {
91
109
  continue;
92
110
  const families = Array.isArray(p.predicate?.families) ? p.predicate.families.map(String) : [];
93
111
  const metadata = Array.isArray(p.predicate?.metadata) ? p.predicate.metadata.map(String) : [];
94
- if (families.every(isRecord) && metadata.every((k) => root[k] !== undefined))
112
+ if (families.every(isRecord) && metadata.every((k) => metadataPresent(root, k)))
95
113
  out.push(p.id);
96
114
  }
97
115
  return out;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.46.1",
4
- "stampSha256": "292af71458912382d78a12adc1da1d6a913346d875096727354f909a35bab2b2"
3
+ "version": "2.46.3",
4
+ "stampSha256": "982ec02aa2b5d1d3ec6a64da49429f3361125a0a2bc02aa669c3efff64da1b01"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.46.1",
3
+ "version": "2.46.3",
4
4
  "description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
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.46.1"
59
+ "@openwop/spec-artifacts": "2.46.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": 741,
6
- "tests": 2790,
7
- "withStableId": 2790,
5
+ "files": 743,
6
+ "tests": 2794,
7
+ "withStableId": 2794,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2646
9
+ "explicitIds": 2650
10
10
  },
11
11
  "records": [
12
12
  {
@@ -28670,6 +28670,20 @@
28670
28670
  }
28671
28671
  ]
28672
28672
  },
28673
+ {
28674
+ "id": "openwop.it.v2-auth-subject-link-key.a-host-advertising-both-the-saml-and-scim-lanes-advertises-subjectlinkkey",
28675
+ "file": "v2-auth-subject-link-key.test.ts",
28676
+ "line": 34,
28677
+ "title": "a host advertising both the saml and scim lanes advertises subjectLinkKey",
28678
+ "explicitId": "openwop.requirement.auth.subject-link-key-advertised",
28679
+ "citations": [
28680
+ {
28681
+ "section": null,
28682
+ "requirement": null,
28683
+ "interpolated": true
28684
+ }
28685
+ ]
28686
+ },
28673
28687
  {
28674
28688
  "id": "openwop.it.v2-authorization-advertisement.the-authorization-record-validates-against-its-capabilities-schema-seat",
28675
28689
  "file": "v2-authorization-advertisement.test.ts",
@@ -33493,6 +33507,48 @@
33493
33507
  }
33494
33508
  ]
33495
33509
  },
33510
+ {
33511
+ "id": "openwop.it.v2-multi-agent-advertisement.executionmodel-version-3-advertises-crosshostcausation-with-a-hostid",
33512
+ "file": "v2-multi-agent-advertisement.test.ts",
33513
+ "line": 41,
33514
+ "title": "executionModel.version >= 3 advertises crossHostCausation with a hostId",
33515
+ "explicitId": "openwop.requirement.multiAgent.level3-cross-host-causation",
33516
+ "citations": [
33517
+ {
33518
+ "section": null,
33519
+ "requirement": null,
33520
+ "interpolated": true
33521
+ }
33522
+ ]
33523
+ },
33524
+ {
33525
+ "id": "openwop.it.v2-multi-agent-advertisement.executionmodel-version-4-advertises-replaydeterminism-with-refusaldivergenceemis",
33526
+ "file": "v2-multi-agent-advertisement.test.ts",
33527
+ "line": 50,
33528
+ "title": "executionModel.version >= 4 advertises replayDeterminism with refusalDivergenceEmission",
33529
+ "explicitId": "openwop.requirement.multiAgent.level4-replay-determinism",
33530
+ "citations": [
33531
+ {
33532
+ "section": null,
33533
+ "requirement": null,
33534
+ "interpolated": true
33535
+ }
33536
+ ]
33537
+ },
33538
+ {
33539
+ "id": "openwop.it.v2-multi-agent-advertisement.confidenceescalationinterruptkind-is-clarification-or-approval",
33540
+ "file": "v2-multi-agent-advertisement.test.ts",
33541
+ "line": 59,
33542
+ "title": "confidenceEscalationInterruptKind is clarification or approval",
33543
+ "explicitId": "openwop.requirement.multiAgent.escalation-kind-canonical",
33544
+ "citations": [
33545
+ {
33546
+ "section": null,
33547
+ "requirement": null,
33548
+ "interpolated": true
33549
+ }
33550
+ ]
33551
+ },
33496
33552
  {
33497
33553
  "id": "openwop.it.v2-multi-party-conversation-advertisement.the-multipartyconversation-record-validates-against-its-capabilities-schema-seat",
33498
33554
  "file": "v2-multi-party-conversation-advertisement.test.ts",
@@ -34711,7 +34767,7 @@
34711
34767
  {
34712
34768
  "id": "openwop.it.v2-profiles-derived-only.the-v2-document-carries-no-root-profiles",
34713
34769
  "file": "v2-profiles-derived-only.test.ts",
34714
- "line": 48,
34770
+ "line": 50,
34715
34771
  "title": "the v2 document carries no root `profiles[]`",
34716
34772
  "explicitId": "openwop.requirement.0169.profiles-derived-only.no-root-profiles",
34717
34773
  "citations": [
@@ -34724,7 +34780,7 @@
34724
34780
  {
34725
34781
  "id": "openwop.it.v2-profiles-derived-only.every-registry-profile-is-derivable-from-the-document-the-discovery-core-predica",
34726
34782
  "file": "v2-profiles-derived-only.test.ts",
34727
- "line": 54,
34783
+ "line": 56,
34728
34784
  "title": "every registry profile is derivable from the document; the discovery-core predicate holds on a valid document",
34729
34785
  "explicitId": "openwop.requirement.0169.profiles-derived-only.derivable",
34730
34786
  "citations": [
@@ -44764,7 +44820,7 @@
44764
44820
  {
44765
44821
  "id": "openwop.it.seams-floor-legs-gated.every-leg-of-a-seams-profile-floor-file-reaches-a-seams-gate-before-its-first-as",
44766
44822
  "file": "seams-floor-legs-gated.test.ts",
44767
- "line": 84,
44823
+ "line": 82,
44768
44824
  "title": "every leg of a seams-profile floor file reaches a seams gate before its first assertion",
44769
44825
  "explicitId": "openwop.it.seams-floor-legs-gated.every-leg-of-a-seams-profile-floor-file-reaches-a-seams-gate-before-its-first-a",
44770
44826
  "citations": [
@@ -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": 653,
4
+ "files": 655,
5
5
  "v1": 455,
6
- "v2": 213
6
+ "v2": 215
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1329,6 +1329,9 @@
1329
1329
  "v2-auth-challenge.test.ts": [
1330
1330
  2
1331
1331
  ],
1332
+ "v2-auth-subject-link-key.test.ts": [
1333
+ 2
1334
+ ],
1332
1335
  "v2-authorization-advertisement.test.ts": [
1333
1336
  2
1334
1337
  ],
@@ -1569,6 +1572,9 @@
1569
1572
  "v2-mrtr-rounds-ceiling.test.ts": [
1570
1573
  2
1571
1574
  ],
1575
+ "v2-multi-agent-advertisement.test.ts": [
1576
+ 2
1577
+ ],
1572
1578
  "v2-multi-party-conversation-advertisement.test.ts": [
1573
1579
  2
1574
1580
  ],
@@ -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.46.1",
5
- "corpusTag": "v2.46.1",
4
+ "version": "2.46.3",
5
+ "corpusTag": "v2.46.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": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "698ff413d4ab103656f15f784ecde2b72191dd9c48d47a459c3fed9c6ac5d65a",
13
- "api/v2/asyncapi.yaml": "31709ec59884ad87f2193ad5a855390ce05647a1ca507ab1aa31dd7b615e614a",
14
- "api/v2/openapi.yaml": "f00d724b067ccbad46d7d9d103efc641ced8118533af2346465559b286f04f86",
12
+ "api/seams-v2.yaml": "cb175971637256b36144514f7dbda94281892a1de32afb4eb2bb2e515e8029a2",
13
+ "api/v2/asyncapi.yaml": "cb8c4a0ab6b398fd32f35d5ad496e6fbf115f4af1546533440f1827cc660aaba",
14
+ "api/v2/openapi.yaml": "f4fef925c3013db82449498de884da1ae38a19f802968a0adf2d7c49aa927a0b",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -117,7 +117,7 @@
117
117
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
118
118
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
119
119
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
120
- "schemas/v2/capabilities.schema.json": "d859a413ff463ec39acc32a9cb76f2e8c7419e45683d6b7f886861765a9c36c7",
120
+ "schemas/v2/capabilities.schema.json": "6dc5d67585d6e9a18ab6a0b9b4c9b70b821517b401218fa855539636fb0b5974",
121
121
  "schemas/v2/certification-bundle.schema.json": "5cfa7fb5247160333e33a58b60d42bcd4e5269d8391d6a112d301e14828892ed",
122
122
  "schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
123
123
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -133,7 +133,7 @@
133
133
  "schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
134
134
  "schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
135
135
  "schemas/v2/debug-bundle.schema.json": "0ec1e0bca9c0a359cc6cf3448e5200a1c3d3d5b1e5efaa6605231477daaf3fee",
136
- "schemas/v2/dispatch-config.schema.json": "5718c1ed7d95672e618bcb1f1da8e38180e144b12151307afc3aba1314dfcef0",
136
+ "schemas/v2/dispatch-config.schema.json": "c2da8c7075e120c6204499a48dbf1a95a54e30ab897331425ac37660a53ad216",
137
137
  "schemas/v2/effect-ledger-projection.schema.json": "0d118d385a43921118a76888449c5c3c5618eab1b80914b84a0a1b48d18822c1",
138
138
  "schemas/v2/effect-seam-manifest.schema.json": "197ef9944ab027a50d1b9352f317cbbc12c8ad955fdb14650fb90477052ffef0",
139
139
  "schemas/v2/envelopes/clarification.request.schema.json": "9e39d7b09f0b3ce7902d6aa19ca0eb52867b634efc78a881469c4d44fb3333e7",
@@ -161,7 +161,7 @@
161
161
  "schemas/v2/memory-entry.schema.json": "290576ae84f4fef4cdf975932b87963283b11c50ea3d2654b32cd96aa6537c3d",
162
162
  "schemas/v2/memory-list-options.schema.json": "01ceb7370c4eacd227d2c9b7e1951ee16650abe26a32dc9d6a8e46a07a39093f",
163
163
  "schemas/v2/node-pack-manifest.schema.json": "3eb2db1d84f0b8dbde917686c72c121788a4ce6e42cf13d497a3b68564546d0e",
164
- "schemas/v2/orchestrator-decision.schema.json": "4d4b4c5fe73c22a1f42242b403650206ad58fd981f8fd45795894cebe3a514ae",
164
+ "schemas/v2/orchestrator-decision.schema.json": "1b8146b1d050dc48e3900928c5a0b89874433b3b7fa9ee59c037557b80bd0910",
165
165
  "schemas/v2/org-chart-responsibility-view.schema.json": "8a2565e4cdc6737a3946a70799f93642808b92f9c8d56dcdff3a0782053b89a1",
166
166
  "schemas/v2/pack-lockfile.schema.json": "2685417702c105146b780cd99154296a46578fbb10d81c8c713348ee26fea898",
167
167
  "schemas/v2/part.schema.json": "08b75eaec9a0b4a8cedca7bab732dd906b52b0749eaa444ec1ac015aa43519a5",
@@ -208,14 +208,14 @@
208
208
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
209
209
  "spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
210
210
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
211
- "spec/v1/core-standard-manifest.json": "cab62b59436a7b57339098abd99d90217b1ca310451dff63837ff27ab90e3a04",
211
+ "spec/v1/core-standard-manifest.json": "c65192dc000e6ad0f7b6eb641ddbf59a905fb09a702cb23d31b8cc807a85b81c",
212
212
  "spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
213
213
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
214
214
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
215
215
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
216
216
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
217
217
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
218
- "spec/v1/gaps.json": "132a59a97d6a9d1f5a5247062bdf69327256f7a3022b9590fd9481200001ab21",
218
+ "spec/v1/gaps.json": "2415b9a2152cb526fa7296b82a433d53ce19194a52389d44f43a81539ac26a1c",
219
219
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
220
220
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
221
221
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -223,19 +223,19 @@
223
223
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
224
224
  "spec/v2/README.md": "c58acd84bb67c69043d9f777cbaa16d03e4f0f0a8158d2984526b69981067a14",
225
225
  "spec/v2/core/artifact-type-packs.md": "f734740cc3963f86c9f273a7dace26291bba2855161ddd3540618131a2c8d13e",
226
- "spec/v2/core/capabilities.md": "8218c44f90782df9630dd276e004def162dad989464cba85e7fc42d987bb61f8",
226
+ "spec/v2/core/capabilities.md": "6b766bcc1ec8c8dd8054defd8a93d8f5ceb046f16d256aef24d1a98bd89a1f73",
227
227
  "spec/v2/core/conformance.md": "3ca291e63bc70a6ef829ae8759ad0ded3741ea7aa507bbbdffc57de443ab2ae2",
228
228
  "spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
229
229
  "spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
230
230
  "spec/v2/core/errors.md": "e48838701b86bbfe0ffee77b18a8ffc4e5f04724b4318507d9771da653f8da0f",
231
231
  "spec/v2/core/events.md": "370621629c2439747f2e4ddab4780eea4345354f79e5098739d66823873f3105",
232
- "spec/v2/core/execution.md": "8a0c9db1bbfb97c008ab9328b7333c1cc3318196e78bfbefb49e781bdf4f9d44",
232
+ "spec/v2/core/execution.md": "2cabb386aea55f70b6e4d10b2b637819589b6cf980325853024ee4b5a3f346d5",
233
233
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
234
234
  "spec/v2/core/headers.md": "f69fe81e4a3ad21879bb85948660c16860eb05bf152928c31aa4898f6d7d1887",
235
235
  "spec/v2/core/host-services.md": "168a40935a03867ee82461b5d83d05578be25ee3e5af4eb080587b891231e242",
236
236
  "spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
237
237
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
238
- "spec/v2/core/identity.md": "02656ffa413b5546342b8580ae928a74e621460746df74d7db50ff4df41d2310",
238
+ "spec/v2/core/identity.md": "3110138944189c378b0ca959ce1bfe5e808e6fdfc9481afeee128c899db05547",
239
239
  "spec/v2/core/interop.md": "6ec8259117bdb387b9323ca22c707b3b8571c00034e461c4ce79f4070c4d6b4a",
240
240
  "spec/v2/core/interrupt.md": "10f15dfe30baa0fc748aea0a067f9b271bea316fde6b598d78f77edc296f698a",
241
241
  "spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
@@ -254,7 +254,7 @@
254
254
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
255
255
  "spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
256
256
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
257
- "spec/v2/declaration.json": "22530d99c92b9e0d3ae0dce25507f4cc6350486d560ad8cc01acda054e9face3",
257
+ "spec/v2/declaration.json": "6fa19c3a68df5a951f9dabea13afbd4ee3b288a110002461f373b191b3eca99b",
258
258
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
259
259
  "spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
260
260
  "spec/v2/errors.json": "1f0e29455edbd22e28f67f221766209c3c55ac4987beb77993b5702dd7cae131",
@@ -312,10 +312,10 @@
312
312
  "spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
313
313
  "spec/v2/path-manifest.json": "7b5679a4dad98c80d855361d88a98c8a4e045af15e310862a05b285b815dc9fc",
314
314
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
315
- "spec/v2/profiles.json": "3d84d7712e93d1e823e362615d5f4aaba0789ea4e044b3774965876fbb8829a7",
316
- "spec/v2/release.json": "4facb248b7147ef0568bf434973a8a116d8f39f293e5c23665257f94c97cb3fe",
315
+ "spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
316
+ "spec/v2/release.json": "86eefef2a95ae211e813e113b435bdefe8319f263ad43d005362a6ded3db4aaa",
317
317
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
318
- "spec/v2/surface-baseline.json": "01a3e01c87e2b5fdbf4c6290e5730726acc195801c97b1f23c4297897b6d3979"
318
+ "spec/v2/surface-baseline.json": "046cfc5a89c711a4b06244389ba2131d89d933cc9c888c78b68f36779914228c"
319
319
  },
320
- "corpusCommit": "010da68bf57d317c7faeeda29f26bedfb3406a49"
320
+ "corpusCommit": "4b2a2a43ddd40943ebf4c332592171c3587c6e15"
321
321
  }
@@ -9,6 +9,11 @@
9
9
  * facet legs; `conversationTurnModelProvenance`, `channelPresence`,
10
10
  * `nondeterminismPolicy`, `promptLibrary` and `envelopeContracts` carry no facet
11
11
  * a host-binding v2 rule gives meaning to, so they get the record leg only.
12
+ * Wave 4 (suite 2.46.3): the `multiAgent.executionModel` sub-blocks a version
13
+ * level requires and the escalation kind it may name, and `auth.subjectLinkKey`
14
+ * under the two linking lanes. These rules live in prose and schema
15
+ * descriptions, not in the schema itself: a conditional `required` or a narrowed
16
+ * enum is a major change (COMPATIBILITY.md §2.4), so the suite carries them.
12
17
  *
13
18
  * A claims-check family is witnessed by what the host says about itself, so
14
19
  * every leg here reads the discovery record alone. Each leg checks one facet
@@ -69,7 +74,8 @@ export type AdvertFamily =
69
74
  | 'scheduling' | 'artifactTypes' | 'aiEnvelope' | 'deadLetter' | 'portability'
70
75
  | 'dataResidency' | 'conversationTurnModelProvenance' | 'multiPartyConversation' | 'channelPresence'
71
76
  | 'nodePackRuntimes' | 'uiPlugins' | 'purposePropagation' | 'nondeterminismPolicy'
72
- | 'agentRuntime' | 'promptLibrary' | 'envelopeContracts';
77
+ | 'agentRuntime' | 'promptLibrary' | 'envelopeContracts'
78
+ | 'multiAgent' | 'auth';
73
79
 
74
80
  export function recordSchemaLeg(profile: MajorProfile, discovery: unknown, family: AdvertFamily, doc: string): AdvertOutcome {
75
81
  return onFamily(profile, discovery, family, (rec) => observed([bySchema(`/properties/${family}`, rec, doc, `the ${family} record`)]));
@@ -409,3 +415,82 @@ export const agentRuntimeImpliesManifestRuntimeLeg = (p: MajorProfile, d: unknow
409
415
  const ok = agents !== null && isRecord(agents['manifestRuntime']);
410
416
  return observed([{ ok, doc: 'host-services.md §agentRuntime', message: `a host advertising agentRuntime MUST also advertise agents.manifestRuntime${ok ? '' : agents === null ? ' (agents is absent)' : ' (agents.manifestRuntime is absent)'}` }]);
411
417
  });
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Wave 4 — multiAgent.executionModel (execution.md §multiAgent) and auth (identity.md §3)
421
+ // ---------------------------------------------------------------------------
422
+
423
+ const MA_DOC = 'execution.md §multiAgent';
424
+
425
+ /** The executionModel facet and its integer level, or a skip when either is absent. */
426
+ function executionModel(rec: Record<string, unknown>, atLeast: number): { em: Record<string, unknown> } | AdvertOutcome {
427
+ const em = rec['executionModel'];
428
+ if (!isRecord(em)) return inapplicable('the host does not advertise multiAgent.executionModel');
429
+ const v = em['version'];
430
+ if (typeof v !== 'number' || !Number.isInteger(v)) return observed([{ ok: false, doc: MA_DOC, message: 'multiAgent.executionModel.version MUST be an integer' }]);
431
+ if (v < atLeast) return inapplicable(`multiAgent.executionModel.version is ${v} (< ${atLeast})`);
432
+ return { em };
433
+ }
434
+ const isOutcome = (x: unknown): x is AdvertOutcome => isRecord(x) && typeof x['kind'] === 'string';
435
+
436
+ /**
437
+ * Level 3 (cumulative): "Hosts advertising `version: 3` MUST advertise this
438
+ * sub-block with a stable `hostId`" — the `causationHostId` rule reads that id.
439
+ */
440
+ export const multiAgentCrossHostCausationLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
441
+ const got = executionModel(rec, 3);
442
+ if (isOutcome(got)) return got;
443
+ const chc = got.em['crossHostCausation'];
444
+ const hostId = isRecord(chc) ? chc['hostId'] : undefined;
445
+ return observed([
446
+ { ok: isRecord(chc), doc: MA_DOC, message: 'a host at executionModel.version >= 3 MUST advertise executionModel.crossHostCausation' },
447
+ { ok: typeof hostId === 'string' && hostId.length > 0, doc: MA_DOC, message: 'executionModel.crossHostCausation MUST carry a non-empty hostId (the causationHostId value)' },
448
+ ]);
449
+ });
450
+
451
+ /**
452
+ * Level 4 (cumulative): "Hosts advertising `version: 4` MUST advertise this
453
+ * sub-block" and "MUST set [`refusalDivergenceEmission`] to `true`".
454
+ */
455
+ export const multiAgentReplayDeterminismLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
456
+ const got = executionModel(rec, 4);
457
+ if (isOutcome(got)) return got;
458
+ const rd = got.em['replayDeterminism'];
459
+ return observed([
460
+ { ok: isRecord(rd), doc: MA_DOC, message: 'a host at executionModel.version >= 4 MUST advertise executionModel.replayDeterminism' },
461
+ { ok: isRecord(rd) && rd['refusalDivergenceEmission'] === true, doc: MA_DOC, message: 'executionModel.replayDeterminism.refusalDivergenceEmission MUST be true at version >= 4' },
462
+ ]);
463
+ });
464
+
465
+ export const ESCALATION_KINDS = ['clarification', 'approval'] as const;
466
+
467
+ /**
468
+ * `confidenceEscalationInterruptKind`: level 2 fires "a `clarification`
469
+ * interrupt (preferred) or an `approval` interrupt", and the host MUST emit the
470
+ * kind it advertises. The schema still admits `x-host-…` (removing it narrows a
471
+ * type); the v2 interrupt `kind` is closed, so such a value promises an interrupt
472
+ * the payload schema rejects. This leg enforces the prose.
473
+ */
474
+ export const multiAgentEscalationKindLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
475
+ const em = rec['executionModel'];
476
+ if (!isRecord(em)) return inapplicable('the host does not advertise multiAgent.executionModel');
477
+ const k = em['confidenceEscalationInterruptKind'];
478
+ if (k === undefined) return inapplicable('the host does not advertise executionModel.confidenceEscalationInterruptKind (optional)');
479
+ const ok = (ESCALATION_KINDS as readonly unknown[]).includes(k);
480
+ return observed([{ ok, doc: MA_DOC, message: `executionModel.confidenceEscalationInterruptKind MUST be clarification or approval at v2${ok ? '' : ` (got ${JSON.stringify(k)})`}` }]);
481
+ });
482
+
483
+ export const SUBJECT_LINK_KEY_CLASSES = ['opaque-idp', 'configured-immutable'] as const;
484
+
485
+ /**
486
+ * identity.md §3: "Advertising both `saml` and `scim` lanes implies the link
487
+ * contract. The `auth.subjectLinkKey` facet … names the key class the host forms
488
+ * links under." A host that forms links names their class.
489
+ */
490
+ export const authSubjectLinkKeyLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'auth', (rec) => {
491
+ const lanes = Array.isArray(rec['lanes']) ? (rec['lanes'] as unknown[]).map((l) => (isRecord(l) ? l['lane'] : undefined)) : [];
492
+ if (!(lanes.includes('saml') && lanes.includes('scim'))) return inapplicable('the host does not advertise both the saml and scim lanes');
493
+ const k = rec['subjectLinkKey'];
494
+ const ok = (SUBJECT_LINK_KEY_CLASSES as readonly unknown[]).includes(k);
495
+ return observed([{ ok, doc: 'identity.md §3', message: `a host advertising both the saml and scim lanes MUST advertise auth.subjectLinkKey (opaque-idp or configured-immutable)${ok ? '' : ` (got ${JSON.stringify(k)})`}` }]);
496
+ });
@@ -76,12 +76,30 @@ export function v2RegistryAvailable(): boolean {
76
76
  return readRegistry() !== null;
77
77
  }
78
78
 
79
+ /**
80
+ * Is one predicate `metadata` entry present in `doc`? An entry is a metadata
81
+ * key, or `key.member` for one member of it (capabilities.md §7).
82
+ *
83
+ * The member form exists for the seams profile. Its advert is
84
+ * `conformance.seamsProfile` (conformance.md §The seams profile), but the
85
+ * `conformance` block also carries `certificationBundleUrl` and `mockAgent`.
86
+ * Until 2.46.2 the predicate was the bare key, so a host that published only a
87
+ * bundle URL claimed the profile, and four openwop-app bundles certified it
88
+ * with no seam mounted.
89
+ */
90
+ export function metadataPresent(doc: Record<string, unknown>, entry: string): boolean {
91
+ const dot = entry.indexOf('.');
92
+ if (dot < 0) return doc[entry] !== undefined;
93
+ const v = doc[entry.slice(0, dot)];
94
+ return typeof v === 'object' && v !== null && !Array.isArray(v) && (v as Record<string, unknown>)[entry.slice(dot + 1)] !== undefined;
95
+ }
96
+
79
97
  /**
80
98
  * Every profile the declaration derives at major 2, or `null` when the registry
81
99
  * is unavailable (see the module docblock — `null` is not `[]`).
82
100
  *
83
101
  * The predicate is the registry's own: every listed family present as a record,
84
- * every listed metadata key present at the root. Nothing here is hand-written
102
+ * every listed metadata entry present ({@link metadataPresent}). Nothing here is hand-written
85
103
  * per profile, so adding a v2 profile is a corpus edit and not a code edit.
86
104
  */
87
105
  export function v2ProfileIds(doc: DiscoveryPayload): readonly string[] | null {
@@ -97,7 +115,7 @@ export function v2ProfileIds(doc: DiscoveryPayload): readonly string[] | null {
97
115
  if (typeof p.id !== 'string') continue;
98
116
  const families = Array.isArray(p.predicate?.families) ? (p.predicate.families as unknown[]).map(String) : [];
99
117
  const metadata = Array.isArray(p.predicate?.metadata) ? (p.predicate.metadata as unknown[]).map(String) : [];
100
- if (families.every(isRecord) && metadata.every((k) => root[k] !== undefined)) out.push(p.id);
118
+ if (families.every(isRecord) && metadata.every((k) => metadataPresent(root, k))) out.push(p.id);
101
119
  }
102
120
  return out;
103
121
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * v2 — `auth.subjectLinkKey` under the two linking lanes (`spec/v2/core/identity.md`
3
+ * §3; RFC 0163 §A, RFC 0164 §A.3). The leg lives in
4
+ * `lib/family-advert-witness.ts` (wave 4).
5
+ *
6
+ * Advertising both the `saml` and `scim` lanes implies the link contract, and
7
+ * the host MUST name the key class it forms links under. The schema cannot
8
+ * say so: a conditional `required` is a major change (COMPATIBILITY.md §2.4),
9
+ * and would also re-judge committed bundles. The schema change waits for 3.0.
10
+ * The behavioural link legs are `v2-subject-link-record`.
11
+ *
12
+ * Dispositions: discovery unreadable ⇒ `blocked`; `auth` absent, or the two
13
+ * lanes not both advertised ⇒ `inapplicable`.
14
+ *
15
+ * @see spec/v2/core/identity.md §3
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
20
+ import { softSkip } from '../lib/soft-skip.js';
21
+ import { req } from '../lib/requirement-ids.js';
22
+ import { majorProfile } from '../lib/major-profile.js';
23
+ import { authSubjectLinkKeyLeg } from '../lib/family-advert-witness.js';
24
+
25
+ const PROFILE = majorProfile(2);
26
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
27
+ const ID_KEY = 'openwop.requirement.auth.subject-link-key-advertised';
28
+
29
+ async function discovery(): Promise<Record<string, unknown> | null> {
30
+ try { return await v2Discovery(); } catch { return null; }
31
+ }
32
+
33
+ describe('v2 auth.subjectLinkKey (identity.md §3)', () => {
34
+ it('a host advertising both the saml and scim lanes advertises subjectLinkKey', async () => {
35
+ const doc = await discovery();
36
+ if (!doc) return softSkip('blocked', UNREADABLE);
37
+ if (!(await familyAdvertised('auth'))) return softSkip('inapplicable', 'the host does not advertise auth');
38
+ const out = authSubjectLinkKeyLeg(PROFILE, doc);
39
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
40
+ for (const x of out.findings) expect(x.ok, req(ID_KEY, x.doc, x.message)).toBe(true);
41
+ });
42
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * v2 — the `multiAgent.executionModel` advertisement (`spec/v2/core/execution.md`
3
+ * §multiAgent; RFCs 0039, 0040, 0041, 0044). The legs live in
4
+ * `lib/family-advert-witness.ts` (wave 4).
5
+ *
6
+ * crossHostCausation at `version` >= 3 the host advertises
7
+ * `crossHostCausation` with a non-empty `hostId`;
8
+ * replayDeterminism at `version` >= 4 the host advertises
9
+ * `replayDeterminism` with `refusalDivergenceEmission: true`;
10
+ * escalationKind `confidenceEscalationInterruptKind`, when present, is
11
+ * `clarification` or `approval`.
12
+ *
13
+ * The schema admits all three violations: a conditional `required` and a
14
+ * narrowed `x-host-…` pattern are major changes (COMPATIBILITY.md §2.4), so the
15
+ * rules are carried here and the schema change waits for 3.0 (TODO.md).
16
+ *
17
+ * Dispositions: discovery unreadable ⇒ `blocked`; `multiAgent`, its
18
+ * `executionModel`, or the level a leg needs absent ⇒ `inapplicable`.
19
+ *
20
+ * @see spec/v2/core/execution.md §multiAgent
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
25
+ import { softSkip } from '../lib/soft-skip.js';
26
+ import { req } from '../lib/requirement-ids.js';
27
+ import { majorProfile } from '../lib/major-profile.js';
28
+ import { multiAgentCrossHostCausationLeg, multiAgentEscalationKindLeg, multiAgentReplayDeterminismLeg } from '../lib/family-advert-witness.js';
29
+
30
+ const PROFILE = majorProfile(2);
31
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
32
+ const ID_CROSS_HOST = 'openwop.requirement.multiAgent.level3-cross-host-causation';
33
+ const ID_REPLAY = 'openwop.requirement.multiAgent.level4-replay-determinism';
34
+ const ID_KIND = 'openwop.requirement.multiAgent.escalation-kind-canonical';
35
+
36
+ async function discovery(): Promise<Record<string, unknown> | null> {
37
+ try { return await v2Discovery(); } catch { return null; }
38
+ }
39
+
40
+ describe('v2 multiAgent advertisement (execution.md §multiAgent)', () => {
41
+ it('executionModel.version >= 3 advertises crossHostCausation with a hostId', async () => {
42
+ const doc = await discovery();
43
+ if (!doc) return softSkip('blocked', UNREADABLE);
44
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
45
+ const out = multiAgentCrossHostCausationLeg(PROFILE, doc);
46
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
47
+ for (const x of out.findings) expect(x.ok, req(ID_CROSS_HOST, x.doc, x.message)).toBe(true);
48
+ });
49
+
50
+ it('executionModel.version >= 4 advertises replayDeterminism with refusalDivergenceEmission', async () => {
51
+ const doc = await discovery();
52
+ if (!doc) return softSkip('blocked', UNREADABLE);
53
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
54
+ const out = multiAgentReplayDeterminismLeg(PROFILE, doc);
55
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
56
+ for (const x of out.findings) expect(x.ok, req(ID_REPLAY, x.doc, x.message)).toBe(true);
57
+ });
58
+
59
+ it('confidenceEscalationInterruptKind is clarification or approval', async () => {
60
+ const doc = await discovery();
61
+ if (!doc) return softSkip('blocked', UNREADABLE);
62
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
63
+ const out = multiAgentEscalationKindLeg(PROFILE, doc);
64
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
65
+ for (const x of out.findings) expect(x.ok, req(ID_KIND, x.doc, x.message)).toBe(true);
66
+ });
67
+ });
@@ -6,7 +6,8 @@
6
6
  * declaration file, published in `spec/v2/profiles.json`; it is never a wire
7
7
  * field. The host's v2 document MUST NOT carry a root `profiles[]`, and every
8
8
  * profile in the registry MUST be derivable from the document alone: every
9
- * listed family present as a record and every listed metadata key present.
9
+ * listed family present as a record and every listed metadata entry present
10
+ * (a key, or `key.member`).
10
11
  * `profiles.json` is read from the spec tree beside `SCHEMAS_DIR`; under a
11
12
  * layout that does not ship it the derivation leg records `blocked`.
12
13
  */
@@ -17,6 +18,7 @@ import { join } from 'node:path';
17
18
  import { v2Discovery } from '../lib/v2.js';
18
19
  import { SCHEMAS_DIR } from '../lib/paths.js';
19
20
  import { softSkip } from '../lib/soft-skip.js';
21
+ import { metadataPresent } from '../lib/v2-profiles.js';
20
22
  import { req } from '../lib/requirement-ids.js';
21
23
 
22
24
  const DOC = 'spec/v2/core/capabilities.md §7';
@@ -41,7 +43,7 @@ function isRecord(v: unknown): v is Record<string, unknown> {
41
43
  function derive(doc: Record<string, unknown>, p: ProfileRow): boolean {
42
44
  const families = p.predicate.families ?? [];
43
45
  const metadata = p.predicate.metadata ?? [];
44
- return families.every((f) => isRecord(doc[f])) && metadata.every((m) => doc[m] !== undefined);
46
+ return families.every((f) => isRecord(doc[f])) && metadata.every((m) => metadataPresent(doc, m));
45
47
  }
46
48
 
47
49
  describe('v2 profiles-derived-only (RFC 0169 §C.1, §C.3)', () => {