@openwop/openwop-conformance 2.45.2 → 2.45.4

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/certification-bundle-verify.js +22 -1
  5. package/dist/lib/profiles.js +17 -2
  6. package/dist/lib/requirement-registry.js +15 -0
  7. package/dist/lib/scenario-disposition.js +60 -9
  8. package/dist/spec-artifacts.lock.json +2 -2
  9. package/fixtures/conformance-budget-tool-calls.json +81 -0
  10. package/fixtures/conformance-fs-probe.json +25 -0
  11. package/fixtures/conformance-queue-consume.json +27 -0
  12. package/fixtures/conformance-queue-publish.json +29 -0
  13. package/fixtures/conformance-safefetch-probe.json +23 -0
  14. package/fixtures/conformance-secret-resolve-then-fail.json +33 -0
  15. package/fixtures/conformance-storage-probe.json +27 -0
  16. package/fixtures/conformance-tool-scope-probe.json +27 -0
  17. package/fixtures/openwop-secrets-run-witness.json +29 -0
  18. package/fixtures.md +113 -1
  19. package/package.json +2 -2
  20. package/requirement-aliases.json +72 -71
  21. package/requirements.json +1081 -43
  22. package/scenario-majors.json +45 -3
  23. package/schemas/CORPUS-STAMP.json +22 -22
  24. package/src/cli.ts +1 -1
  25. package/src/lib/backpressure-witness.ts +163 -0
  26. package/src/lib/budget-witness.ts +165 -0
  27. package/src/lib/certification-bundle-verify.ts +23 -2
  28. package/src/lib/driver.ts +61 -2
  29. package/src/lib/major-profile.ts +93 -0
  30. package/src/lib/memoryAttribution.ts +41 -6
  31. package/src/lib/polling.ts +2 -18
  32. package/src/lib/profiles.ts +26 -2
  33. package/src/lib/requirement-registry.ts +12 -0
  34. package/src/lib/run-secrets-witness.ts +240 -0
  35. package/src/lib/scenario-disposition.ts +54 -7
  36. package/src/lib/scratch-host.ts +130 -0
  37. package/src/lib/secret-scan.ts +141 -0
  38. package/src/lib/timeout-scale.ts +25 -0
  39. package/src/lib/triggerBridge.ts +69 -1
  40. package/src/scenarios/byok-roundtrip.test.ts +20 -4
  41. package/src/scenarios/runner-ledger.test.ts +2 -0
  42. package/src/scenarios/secrets-run-witness.test.ts +154 -0
  43. package/src/scenarios/trigger-bridge-delivery.test.ts +183 -126
  44. package/src/scenarios/trigger-refused-event-keeps-subscription.test.ts +141 -0
  45. package/src/scenarios/v2-budget-enforcement.test.ts +94 -0
  46. package/src/scenarios/v2-eval-mode-unadvertised-refused.test.ts +55 -0
  47. package/src/scenarios/v2-fs-sandbox-escape-refused.test.ts +161 -0
  48. package/src/scenarios/v2-memory-cross-tenant-isolation.test.ts +113 -0
  49. package/src/scenarios/v2-production-backpressure.test.ts +74 -0
  50. package/src/scenarios/v2-queue-cross-tenant-isolation.test.ts +118 -0
  51. package/src/scenarios/v2-safefetch-ssrf-refused.test.ts +198 -0
  52. package/src/scenarios/v2-secret-canary-absent.test.ts +211 -0
  53. package/src/scenarios/v2-secrets-run-witness.test.ts +167 -0
  54. package/src/scenarios/v2-storage-cross-tenant-isolation.test.ts +213 -0
  55. package/src/scenarios/v2-tool-authorization-fail-closed.test.ts +197 -0
  56. package/src/scenarios/v2-workspace-scope-from-identity.test.ts +146 -0
@@ -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": 575,
5
- "v1": 452,
6
- "v2": 138
4
+ "files": 589,
5
+ "v1": 454,
6
+ "v2": 150
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1149,6 +1149,9 @@
1149
1149
  "secret-leakage-otel-attribute.test.ts": [
1150
1150
  1
1151
1151
  ],
1152
+ "secrets-run-witness.test.ts": [
1153
+ 1
1154
+ ],
1152
1155
  "self-hosted-runner.test.ts": [
1153
1156
  1
1154
1157
  ],
@@ -1263,6 +1266,9 @@
1263
1266
  "trigger-ingestion.test.ts": [
1264
1267
  1
1265
1268
  ],
1269
+ "trigger-refused-event-keeps-subscription.test.ts": [
1270
+ 1
1271
+ ],
1266
1272
  "trigger-stream-cdc-sources.test.ts": [
1267
1273
  1
1268
1274
  ],
@@ -1311,6 +1317,9 @@
1311
1317
  "v2-bound-id-path-projection.test.ts": [
1312
1318
  2
1313
1319
  ],
1320
+ "v2-budget-enforcement.test.ts": [
1321
+ 2
1322
+ ],
1314
1323
  "v2-bundle-v3-signed.test.ts": [
1315
1324
  2
1316
1325
  ],
@@ -1386,6 +1395,9 @@
1386
1395
  "v2-error-registry.test.ts": [
1387
1396
  2
1388
1397
  ],
1398
+ "v2-eval-mode-unadvertised-refused.test.ts": [
1399
+ 2
1400
+ ],
1389
1401
  "v2-event-type-closed.test.ts": [
1390
1402
  2
1391
1403
  ],
@@ -1401,6 +1413,9 @@
1401
1413
  "v2-form-when-reuses-edge-conditions.test.ts": [
1402
1414
  2
1403
1415
  ],
1416
+ "v2-fs-sandbox-escape-refused.test.ts": [
1417
+ 2
1418
+ ],
1404
1419
  "v2-header-scheme.test.ts": [
1405
1420
  2
1406
1421
  ],
@@ -1452,6 +1467,9 @@
1452
1467
  "v2-mcp-tasks.test.ts": [
1453
1468
  2
1454
1469
  ],
1470
+ "v2-memory-cross-tenant-isolation.test.ts": [
1471
+ 2
1472
+ ],
1455
1473
  "v2-min-client-version.test.ts": [
1456
1474
  2
1457
1475
  ],
@@ -1506,6 +1524,9 @@
1506
1524
  "v2-preferred-version-default.test.ts": [
1507
1525
  2
1508
1526
  ],
1527
+ "v2-production-backpressure.test.ts": [
1528
+ 2
1529
+ ],
1509
1530
  "v2-profiles-derived-only.test.ts": [
1510
1531
  2
1511
1532
  ],
@@ -1515,6 +1536,9 @@
1515
1536
  "v2-provider-conflict.test.ts": [
1516
1537
  2
1517
1538
  ],
1539
+ "v2-queue-cross-tenant-isolation.test.ts": [
1540
+ 2
1541
+ ],
1518
1542
  "v2-refresh-sla.test.ts": [
1519
1543
  2
1520
1544
  ],
@@ -1566,12 +1590,24 @@
1566
1590
  "v2-run-snapshot-etag.test.ts": [
1567
1591
  2
1568
1592
  ],
1593
+ "v2-safefetch-ssrf-refused.test.ts": [
1594
+ 2
1595
+ ],
1596
+ "v2-secret-canary-absent.test.ts": [
1597
+ 2
1598
+ ],
1599
+ "v2-secrets-run-witness.test.ts": [
1600
+ 2
1601
+ ],
1569
1602
  "v2-sse-last-event-id-cursor.test.ts": [
1570
1603
  2
1571
1604
  ],
1572
1605
  "v2-sse-last-event-id.test.ts": [
1573
1606
  2
1574
1607
  ],
1608
+ "v2-storage-cross-tenant-isolation.test.ts": [
1609
+ 2
1610
+ ],
1575
1611
  "v2-stream-mode-refusal.test.ts": [
1576
1612
  2
1577
1613
  ],
@@ -1584,6 +1620,9 @@
1584
1620
  "v2-terminal-event-once.test.ts": [
1585
1621
  2
1586
1622
  ],
1623
+ "v2-tool-authorization-fail-closed.test.ts": [
1624
+ 2
1625
+ ],
1587
1626
  "v2-tool-catalog-annotations.test.ts": [
1588
1627
  2
1589
1628
  ],
@@ -1635,6 +1674,9 @@
1635
1674
  "v2-well-known-one-resource.test.ts": [
1636
1675
  2
1637
1676
  ],
1677
+ "v2-workspace-scope-from-identity.test.ts": [
1678
+ 2
1679
+ ],
1638
1680
  "vector-knn-roundtrip.test.ts": [
1639
1681
  1
1640
1682
  ],
@@ -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.45.2",
5
- "corpusTag": "v2.45.2",
4
+ "version": "2.45.4",
5
+ "corpusTag": "v2.45.4",
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
- "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
10
+ "api/openapi.yaml": "ad37a5a5c732ad46100fc03c3f4243fdd4f2976e330f02a8791b6b5b10a8a0c1",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "4eb20e64dbccf5e7ed1f3ffba7882f576bb1629f8523eaffae54d05809e1bc66",
13
- "api/v2/asyncapi.yaml": "910690735bc6bca78f51801854fa3e3ed9eb9a56f65441d594401fb19e684ba3",
14
- "api/v2/openapi.yaml": "b4fb04a206521719a1b13733914550d4d671e57c0c276122bedb88635fd8901c",
12
+ "api/seams-v2.yaml": "e4a8e19b494d12c0d105ed8d1d27337995564a6443cb8a747521a9d67977b52b",
13
+ "api/v2/asyncapi.yaml": "d53ca2c06a47282a0cc39d9e0b6b488068da0ad2c91c3166598b005dcf72aa21",
14
+ "api/v2/openapi.yaml": "24bce91f02de6527121f12ab23916c4f96bab2cbf8f2203208c1d3a444c632c5",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -31,7 +31,7 @@
31
31
  "schemas/artifact-type-pack-manifest.schema.json": "60342fcb15eb7f3ab46271f0641409a30d3a4a8f318b1020c53070886b191252",
32
32
  "schemas/audit-verify-result.schema.json": "0d3a3088ac1c32f9df66d346c33d7adbd8037940dba040e14ea847565a1a14bd",
33
33
  "schemas/budget-policy.schema.json": "3d4fe30ae129fe136ab9ece395f2a2a42a76360786b1b9a1d5f1cbc753eb5b39",
34
- "schemas/capabilities.schema.json": "56b367074bce0f02ff87e0b84ccdf91856793d3d285e28f261656edba6352cc0",
34
+ "schemas/capabilities.schema.json": "d1d491248930e9b50ac6aca93a33cb1155eb997c4fbb2af0a940e8e889c76807",
35
35
  "schemas/certification-bundle-v2.schema.json": "2f85530dd04824a15810c94e96869362dee1e78396eca21fdd18e5501416bb7d",
36
36
  "schemas/channel-presence-payload.schema.json": "f9d0ef4219814c2b765baf3e68cbe5eea2cb42379f516a93a0459dcd5f7db47e",
37
37
  "schemas/channel-written-payload.schema.json": "54723a56bc5a35232f64544ca686ac1ac959a5fb05b32b2401facca6b3ef51a3",
@@ -95,7 +95,7 @@
95
95
  "schemas/suspend-request.schema.json": "7a90b622d5c619e3ffb5da696d272d6505e2780ab92ab631ac1b900f5daea908",
96
96
  "schemas/tool-descriptor.schema.json": "7f6adbae63003c1f1311f6a436f7f286542be62aa19a27b46730f2c2810857f7",
97
97
  "schemas/trigger-event.schema.json": "59d63b79dca3593f45d281776cbfe43ae4d85ee25519b82ad59f162a9efb592c",
98
- "schemas/trigger-subscription-registration.schema.json": "929d628bad08e1def696e47e9a148aba00fa801e28d49bbd0dc2360b08673435",
98
+ "schemas/trigger-subscription-registration.schema.json": "2ca680ca6ffdb3cfc5de7bb8b8db8c3ec8eab4b88b7c9f7fe5edaecc7cf61ad0",
99
99
  "schemas/trigger-subscription.schema.json": "4f7b067f20032073b140c4bb15dc9a9914c4f6421a252ff1ff261cbdd8edf270",
100
100
  "schemas/ui-plugin-message.schema.json": "346638babf1f8ea7b161cde75c0038b518cd2cc5bf0631772e20a9b74c96b5dd",
101
101
  "schemas/v2/a2a-task-state.schema.json": "1810363e2d9bddb5997923bdac4044cc17c6f46ec5a40515d2ff21d9c82c1ad9",
@@ -116,7 +116,7 @@
116
116
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
117
117
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
118
118
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
119
- "schemas/v2/capabilities.schema.json": "d0b72f589b4e29ef69bd26e38a309aca1db2965c8441cae56c1dcc12c8d7152f",
119
+ "schemas/v2/capabilities.schema.json": "3792aa9d292229cf9b82c7306600a5dfdb63b3274523e0ed5dd4f952a04cb833",
120
120
  "schemas/v2/certification-bundle.schema.json": "82ce31a1ed6c7e21b0ba88ff62a7b8b768fc3b176b1d06fce023203830b94725",
121
121
  "schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
122
122
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -130,7 +130,7 @@
130
130
  "schemas/v2/conversation-turn.schema.json": "75bb6a4a03072a958feb5b34e2a1f40471e9e3996a81fa430b65717f0773c9a4",
131
131
  "schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
132
132
  "schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
133
- "schemas/v2/debug-bundle.schema.json": "f2b72cd658a2072e24031fcd916ba3eb8afe5618c540ea0e7e5cc79ef29d5cab",
133
+ "schemas/v2/debug-bundle.schema.json": "0ec1e0bca9c0a359cc6cf3448e5200a1c3d3d5b1e5efaa6605231477daaf3fee",
134
134
  "schemas/v2/dispatch-config.schema.json": "5718c1ed7d95672e618bcb1f1da8e38180e144b12151307afc3aba1314dfcef0",
135
135
  "schemas/v2/effect-ledger-projection.schema.json": "0d118d385a43921118a76888449c5c3c5618eab1b80914b84a0a1b48d18822c1",
136
136
  "schemas/v2/effect-seam-manifest.schema.json": "197ef9944ab027a50d1b9352f317cbbc12c8ad955fdb14650fb90477052ffef0",
@@ -186,7 +186,7 @@
186
186
  "schemas/v2/suspend-request.schema.json": "368d9932efb08f85b4520cac44b3c553876209f55bada93b5fcac93cc890b411",
187
187
  "schemas/v2/tool-descriptor.schema.json": "8c98fb5e93bc649dff859d3d1dde8075db929efd6c49c496252d0a88cccc73e1",
188
188
  "schemas/v2/trigger-event.schema.json": "710ecca8b3ecdad3a532a96768177f062192b24a589a4f798c7eb24bab64b30a",
189
- "schemas/v2/trigger-subscription-registration.schema.json": "04bc314a07c7e8b565577cb8df5821d968ce2af915900dfdccd4a24e928d20db",
189
+ "schemas/v2/trigger-subscription-registration.schema.json": "5f7959f3e13f49399783e47f73101d6af95e5abccea1af2f0014ccbe3e51006c",
190
190
  "schemas/v2/trigger-subscription.schema.json": "1f5c286f68577d531ef9445ab2493f8d69ac9ee42313d28d758a0f00dd0fab36",
191
191
  "schemas/v2/ui-plugin-message.schema.json": "879d65867c01656b3962d64283e66364dcb8f626ad26c5fcf2d2d55a79ae0ce9",
192
192
  "schemas/v2/webhook-dead-letter-page.schema.json": "543f5ccc26d8f777684a037172ab7b9fdf70735e5394eeb42e5fa6569503f528",
@@ -204,13 +204,13 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "0b2f28808ffe0b9d98a187775038c3406bb70919747a788a6e4f866e293ae6d3",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "c948680f15a882c4bd97fa41235879d4103d3d3887ed294028ba9dfbb1d5d1a9",
207
+ "spec/v1/core-standard-manifest.json": "aa1ecb9c1f1c1c3039f948b137e4672031fad29103bb9ac4166eb4ff89b7c85e",
208
208
  "spec/v1/deprecations.json": "a3293072ea951857395d05f0437264e88965f1e9019ff61c55bacfa9c1aaa17c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "ac1fb9286c963aa19824292526a6f9726d9d2df5fe06695cc6afdfb33554edd2",
213
+ "spec/v1/gaps.json": "2705bd6f7be31392ba09da318e99d55905dd08aa80e76e1226cf8f1a85132d18",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -227,7 +227,7 @@
227
227
  "spec/v2/core/execution.md": "8a0c9db1bbfb97c008ab9328b7333c1cc3318196e78bfbefb49e781bdf4f9d44",
228
228
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
229
229
  "spec/v2/core/headers.md": "21d6c8220c3ac75b10e31c4cf6179abb539e2558af5006e40228d8c383ed5c92",
230
- "spec/v2/core/host-services.md": "18389b55286875922c26e13fbcd6c787fca2568f68b7e6f65176f1938404a218",
230
+ "spec/v2/core/host-services.md": "73f26ea32891044131d63f9861d2cbddccb5d08bf76e29e47afef9bb5b646791",
231
231
  "spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
232
232
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
233
233
  "spec/v2/core/identity.md": "f61dc646f56fc1c07016c3acec9e8eb8c51051c55ecbedc986de793669fb6e6b",
@@ -240,16 +240,16 @@
240
240
  "spec/v2/core/persistence.md": "249a1de353f421cdf92b9006a13a10afdb002f4029eb6a13520cc95252edb2ec",
241
241
  "spec/v2/core/portability.md": "ab13a6ca43544b9a6a91edfb377ec680c768d14be841c84fb79125b231426891",
242
242
  "spec/v2/core/replay.md": "4a8b53325e18aa53400ece81168c76b4402b932e5e304a391754f293b126eabf",
243
- "spec/v2/core/runs.md": "18261bf0fff85101ee9547dd2cd9f06055f2095609dfd23e0f8fdbd1f1d2d0d0",
243
+ "spec/v2/core/runs.md": "991757ded9b3645bafad2344f4ccb75c7d79993bd80269f9dd3489193841d71b",
244
244
  "spec/v2/core/security-defaults.md": "887d8c4bc592094e3b931c23f1c2175c18ebbd47528b074016f9d2091bc513b2",
245
245
  "spec/v2/core/storage.md": "dc94b985847c1341c7a064f04a6010f0fdbe6c3ab3e555d4362878c81d764c18",
246
246
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
247
247
  "spec/v2/core/versioning.md": "b7acb0d8b77425cb53ea700e47ba790bf7506de600c398690e6ec5bb9c7a220e",
248
- "spec/v2/core/webhooks.md": "7b2606b2ebbeafd84ca052a5c4d2f392cade0b482fbb063acb0aeb38675f6ede",
248
+ "spec/v2/core/webhooks.md": "b0c08a3511b9319da7b0f7d83cddb15758acfc92e4c58c3604ac0f6b1ab13242",
249
249
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
250
- "spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
250
+ "spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
251
251
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
252
- "spec/v2/declaration.json": "24866c15908b3b70d96339dd852187ba908ddf65f90fad1d566ba6df5881aa48",
252
+ "spec/v2/declaration.json": "967f6db929fd7c4000d41a12d8b6d9f77bad90348de0dbc792b02f33e7026385",
253
253
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
254
254
  "spec/v2/errors.json": "83f048b0a06952b4af6f125fb66c4212acab51056b411390f1010acd6ab9cdc0",
255
255
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
@@ -260,7 +260,7 @@
260
260
  "spec/v2/ext/brand/README.md": "82989c018d7965bab029bcce7a67b1d76ffc433d08971a4cac91befcccc09438",
261
261
  "spec/v2/ext/canvas/README.md": "00ca0856e3a0146f1eeaec87209d450e2900ed2748dc7520cc60250a0d3ffbeb",
262
262
  "spec/v2/ext/chat/README.md": "f59b6706162abbfff96f8bccd4c0e1aeed721047cf62e9af58c875727a38d716",
263
- "spec/v2/ext/coordination/README.md": "c7fe0884f52405fdd02c5f9ec309ac6b585685ee06ec5f338fff7acf03c07148",
263
+ "spec/v2/ext/coordination/README.md": "7185af14aa0a2b554095ddbf10b0443b535fd28311a1836c6aa733b203030a9a",
264
264
  "spec/v2/ext/dataIntegration/README.md": "286dec8aa29e4d3225cbd8b3a361f5a438c3288e759966b9415ecbb99c576e70",
265
265
  "spec/v2/ext/entities/README.md": "956d4a9a2aedb8cb9c79ce8145580480920474cc6943f0e33ede32d7b20414dc",
266
266
  "spec/v2/ext/grpc-transport/README.md": "6ba378e3c6d1f2e3269b392cfdf0c40565ee9bfa259d1baaa9ee1d350e38e792",
@@ -303,9 +303,9 @@
303
303
  "spec/v2/path-manifest.json": "27c2307b84376b2f22bea4e2213fc7e33ef3bf8fd1d18039f7955edb00dc9c9a",
304
304
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
305
305
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
306
- "spec/v2/release.json": "f042a3f11bc9eaffcd054a6f4b94e78931d6289191d2c8dde2f169a63478e3f1",
306
+ "spec/v2/release.json": "d1b495b89fc43030e607d7dc312732500f0433e84656ccc88ad8c80c2ac444b1",
307
307
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
308
- "spec/v2/surface-baseline.json": "18c9f38e3b596465128ceeee10cbdd17395672da5fe321db03ab4eee67bd50a4"
308
+ "spec/v2/surface-baseline.json": "9e17340cea86146899703c91246ddd5a5403c68e40e749f0ad1958801f60b619"
309
309
  },
310
- "corpusCommit": "5bc14dd1ef9a0c6d8ab4bf220fbef47a74792212"
310
+ "corpusCommit": "5a4ad1ab0a3584c7ea30f4b32f4599e53ce1f75a"
311
311
  }
package/src/cli.ts CHANGED
@@ -685,7 +685,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
685
685
  if (target.major === 2) return verdictFor(profile)?.witnessedPasses ?? 0;
686
686
  const floor = PROFILE_FLOOR_SCENARIOS[profile];
687
687
  if (!floor) return 0;
688
- const onFloor = (scenario: string): boolean => floor.required.includes(scenario) || (floor.requiredAnyPrefix ?? []).some((pre) => scenario.startsWith(pre));
688
+ const onFloor = (scenario: string): boolean => floor.required.includes(scenario) || (floor.requiredAnyPrefix ?? []).some((pre) => scenario.startsWith(pre)) || (floor.requiredAnyOf ?? []).some((g) => g.includes(scenario));
689
689
  return derived.requirements.filter((r) => r.disposition === 'executed-pass' && onFloor(r.scenarioId)).length;
690
690
  };
691
691
  // `certified` IS the verdict (RFC 0148 §A; RFC 0168 §E.1 adds the bundle-wide
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The backpressure witness, shared by every major (`major-profile.ts`).
3
+ *
4
+ * A host that advertises `production.backpressure.inflightCap` names a number
5
+ * the suite can saturate: `cap` long-lived requests hold the slots and the
6
+ * next request must be refused `503 service_unavailable` with `Retry-After`.
7
+ *
8
+ * Two halves, so each can be proven alone:
9
+ * - {@link saturate} drives the host and returns what it saw (or why it
10
+ * could not see anything). It asserts nothing.
11
+ * - {@link judge} is pure: observation in, findings out. The scenario turns
12
+ * findings into `expect` calls; the self-test feeds it a conforming and a
13
+ * defective observation and checks the verdicts differ.
14
+ *
15
+ * Run with `--no-file-parallelism`: saturating the cap leaves no headroom for
16
+ * a neighbouring scenario's requests.
17
+ */
18
+
19
+ import { driver, type OpenWOPResponse } from './driver.js';
20
+ import { loadEnv } from './env.js';
21
+ import { readErrorCode } from './error-envelope.js';
22
+ import { isFixtureAdvertised } from './fixtures.js';
23
+ import type { MajorProfile } from './major-profile.js';
24
+
25
+ /** The long-running fixture that holds a slot, and the cheap one that probes. */
26
+ export const HOLD_FIXTURE = 'conformance-delay';
27
+ export const PROBE_FIXTURE = 'conformance-noop';
28
+ /** Long enough that the first slot is still held when the last is filled; the runs are cancelled afterwards. */
29
+ const HOLD_MS = 15_000;
30
+ /** The most streams the suite will hold open. A larger advertised cap is not saturated. */
31
+ export const MAX_SATURABLE_CAP = 64;
32
+ const STREAM_OPEN_MS = 5_000;
33
+ const RETRY_DETAIL_KEYS = ['retryAfter', 'retryAfterMs', 'retryAfterSeconds'] as const;
34
+
35
+ export interface Refusal {
36
+ readonly cap: number;
37
+ readonly status: number;
38
+ readonly code: string | undefined;
39
+ readonly retryAfterHeader: string | null;
40
+ readonly details: Record<string, unknown> | null;
41
+ readonly advertisedRetryAfterSeconds: number | undefined;
42
+ }
43
+ export type Saturation =
44
+ | { readonly kind: 'skip'; readonly disposition: 'inapplicable' | 'blocked'; readonly reason: string }
45
+ | { readonly kind: 'refused'; readonly refusal: Refusal };
46
+
47
+ export interface Finding {
48
+ /** Which rule the finding is about; the scenario maps it to a requirement id. */
49
+ readonly rule: 'refusal' | 'retry-after-advertised' | 'retry-timing';
50
+ readonly ok: boolean;
51
+ readonly doc: string;
52
+ readonly message: string;
53
+ }
54
+
55
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
56
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
57
+ try { return await fn(); } catch { return null; }
58
+ }
59
+
60
+ /** Hold `cap` slots with long-lived event streams, send one more request, and report its answer. */
61
+ export async function saturate(profile: MajorProfile, discovery: unknown): Promise<Saturation> {
62
+ const production = profile.family(discovery, 'production');
63
+ if (production === null) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host does not advertise production' };
64
+ const bp = production['backpressure'];
65
+ if (!isRecord(bp)) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host does not advertise production.backpressure' };
66
+ const cap = bp['inflightCap'];
67
+ if (typeof cap !== 'number' || !Number.isInteger(cap) || cap < 1) {
68
+ return { kind: 'skip', disposition: 'inapplicable', reason: 'the host does not advertise production.backpressure.inflightCap — there is no cap the suite can saturate' };
69
+ }
70
+ if (cap > MAX_SATURABLE_CAP) return { kind: 'skip', disposition: 'inapplicable', reason: `inflightCap ${cap} is above the ${MAX_SATURABLE_CAP} streams the suite will hold open — not saturated` };
71
+ for (const f of [HOLD_FIXTURE, PROBE_FIXTURE]) {
72
+ if (!isFixtureAdvertised(f)) return { kind: 'skip', disposition: 'inapplicable', reason: `${f} fixture not advertised — the suite cannot hold or probe a slot` };
73
+ }
74
+
75
+ const env = loadEnv();
76
+ const streams: AbortController[] = [];
77
+ const pending: Promise<number | null>[] = [];
78
+ const runIds: string[] = [];
79
+ try {
80
+ for (let i = 0; i < cap; i++) {
81
+ const created = await http(() => driver.post(profile.runsPath, { workflowId: HOLD_FIXTURE, inputs: { delayMs: HOLD_MS } }));
82
+ if (created === null) return { kind: 'skip', disposition: 'blocked', reason: `POST ${profile.runsPath} unreachable while filling slot ${i + 1} of ${cap}` };
83
+ const runId = (created.json as { runId?: unknown } | undefined)?.runId;
84
+ if (created.status !== 201 || typeof runId !== 'string') {
85
+ return { kind: 'skip', disposition: 'blocked', reason: `slot ${i + 1} of ${cap} could not be filled: POST ${profile.runsPath} answered ${created.status} ${readErrorCode(created.json) ?? ''} (already saturated by a parallel file? run with --no-file-parallelism)`.trim() };
86
+ }
87
+ runIds.push(runId);
88
+ const ctl = new AbortController();
89
+ streams.push(ctl);
90
+ // `Accept: text/event-stream` keeps the request in flight; without it a
91
+ // negotiating host answers a one-shot snapshot and the slot drops.
92
+ const stream = fetch(`${env.baseUrl}${profile.runsPath}/${encodeURIComponent(runId)}/events`, {
93
+ headers: { Authorization: `Bearer ${env.apiKey}`, Accept: 'text/event-stream', ...profile.versionHeaders },
94
+ signal: ctl.signal,
95
+ }).then((r) => r.status, () => null);
96
+ pending.push(stream);
97
+ // The slot is held once the stream's headers are back, not after a
98
+ // guessed delay. A probe sent before that would convict a host for a slot
99
+ // the suite had not yet taken.
100
+ const opened = await Promise.race([stream, new Promise<'slow'>((r) => setTimeout(() => r('slow'), STREAM_OPEN_MS))]);
101
+ if (opened !== 200) {
102
+ return { kind: 'skip', disposition: 'blocked', reason: `slot ${i + 1} of ${cap} was not held: the event stream ${opened === 'slow' ? `did not open within ${STREAM_OPEN_MS} ms` : opened === null ? 'failed to connect' : `answered ${opened}`}` };
103
+ }
104
+ }
105
+
106
+ const probe = await http(() => driver.post(profile.runsPath, { workflowId: PROBE_FIXTURE }));
107
+ if (probe === null) return { kind: 'skip', disposition: 'blocked', reason: `the cap+1 probe got no response (POST ${profile.runsPath})` };
108
+ const accepted = (probe.json as { runId?: unknown } | undefined)?.runId;
109
+ if (probe.status === 201 && typeof accepted === 'string') runIds.push(accepted);
110
+ const body = isRecord(probe.json) ? probe.json : {};
111
+ const advertised = bp['retryAfterSeconds'];
112
+ return {
113
+ kind: 'refused',
114
+ refusal: {
115
+ cap,
116
+ status: probe.status,
117
+ code: readErrorCode(probe.json) ?? undefined,
118
+ retryAfterHeader: probe.headers.get('retry-after'),
119
+ details: isRecord(body['details']) ? body['details'] : null,
120
+ advertisedRetryAfterSeconds: typeof advertised === 'number' ? advertised : undefined,
121
+ },
122
+ };
123
+ } finally {
124
+ // The held runs outlive their streams. Cancel them so the next file does
125
+ // not meet a saturated host.
126
+ for (const c of streams) c.abort();
127
+ if (runIds.length > 0) {
128
+ const bulk = await http(() => driver.post(`${profile.runsPath}:bulk-cancel`, { runIds }));
129
+ if (bulk === null || bulk.status >= 400) {
130
+ for (const id of runIds) await http(() => driver.post(`${profile.runsPath}/${encodeURIComponent(id)}/cancel`, {}));
131
+ }
132
+ }
133
+ await Promise.allSettled(pending);
134
+ }
135
+ }
136
+
137
+ /** What the refusal must look like at this major. Pure. */
138
+ export function judge(profile: MajorProfile, r: Refusal): Finding[] {
139
+ const home = `${profile.specRoot}/${profile.major === 1 ? 'production-profile.md §Backpressure' : 'conformance.md §Production profile'}`;
140
+ const out: Finding[] = [];
141
+ out.push({ rule: 'refusal', ok: r.status === 503, doc: home, message: `with inflightCap ${r.cap} slots held, the next request MUST be refused 503 (got ${r.status})` });
142
+ out.push({ rule: 'refusal', ok: r.code === 'service_unavailable', doc: home, message: `the refusal MUST carry the code service_unavailable (got ${r.code ?? 'none'})` });
143
+ const header = (r.retryAfterHeader ?? '').trim();
144
+ out.push({ rule: 'refusal', ok: header.length > 0, doc: home, message: 'the 503 MUST set Retry-After' });
145
+
146
+ if (r.advertisedRetryAfterSeconds !== undefined) {
147
+ out.push({
148
+ rule: 'retry-after-advertised',
149
+ ok: /^\d+$/.test(header) && Number(header) === r.advertisedRetryAfterSeconds,
150
+ doc: 'capabilities.schema.json production.backpressure.retryAfterSeconds',
151
+ message: `Retry-After MUST equal the advertised retryAfterSeconds ${r.advertisedRetryAfterSeconds} (got "${header}")`,
152
+ });
153
+ }
154
+
155
+ if (profile.retryTiming === 'header-only') {
156
+ const spelled = RETRY_DETAIL_KEYS.filter((k) => r.details !== null && k in r.details);
157
+ out.push({ rule: 'retry-timing', ok: spelled.length === 0, doc: `${profile.specRoot}/errors.md §Retry timing`, message: `retry timing lives in the Retry-After header only; details MUST NOT carry ${spelled.join(', ') || 'retryAfter*'}` });
158
+ } else {
159
+ const d = r.details?.['retryAfter'];
160
+ out.push({ rule: 'retry-timing', ok: typeof d === 'number' && /^\d+$/.test(header) && d === Number(header), doc: home, message: 'details.retryAfter MUST be numeric and equal the Retry-After header in seconds' });
161
+ }
162
+ return out;
163
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The run-budget witness, shared by every major that gives a budget a run wire
3
+ * surface (`major-profile.ts` `runBudget`).
4
+ *
5
+ * Unaided: the suite creates a run of `conformance-budget-tool-calls` (three
6
+ * scripted tool calls, no model, no seam) with a two-call budget, waits for it
7
+ * to end, and reads its event log through the poll.
8
+ *
9
+ * Two halves, as in `backpressure-witness.ts`: {@link drive} observes and
10
+ * asserts nothing; {@link judge} is pure.
11
+ *
12
+ * The fixture is the opt-in. A host that advertises `budget` without seeding it
13
+ * records `inapplicable`: the family is then unwitnessed at this major, which
14
+ * the coverage report shows, and the host is not denied certification for a
15
+ * fixture it was never asked to seed.
16
+ */
17
+
18
+ import { driver, type OpenWOPResponse } from './driver.js';
19
+ import { readErrorCode } from './error-envelope.js';
20
+ import { isFixtureAdvertised } from './fixtures.js';
21
+ import type { MajorProfile } from './major-profile.js';
22
+
23
+ export const BUDGET_FIXTURE = 'conformance-budget-tool-calls';
24
+ /** The fixture makes 3 calls: 50% is crossed on the first, and the budget cannot cover the third. */
25
+ export const BUDGET_POLICY = { maxToolCalls: 2, thresholdPercent: 50, onExhaustion: 'fail' } as const;
26
+ const DIMENSION = 'toolCalls';
27
+ const CAP_KIND = 'budget-tool-calls';
28
+ /** Keys a `budget.*` or `cap.breached` payload must never carry (`budget-no-pricing-leak`). */
29
+ export const PRICING_KEYS = ['pricing', 'priceTable', 'prices', 'rate', 'rates', 'unitPrice', 'costModel', 'tokenPrice', 'secret', 'apiKey'] as const;
30
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
31
+
32
+ export interface RunEvent { readonly type: string; readonly sequence: number; readonly payload: Record<string, unknown> }
33
+ export interface BudgetObservation {
34
+ readonly enforce: 'hard' | 'advisory' | undefined;
35
+ readonly status: string;
36
+ readonly errorCode: string | undefined;
37
+ readonly events: readonly RunEvent[];
38
+ /** This major's names for the four events the witness reads. */
39
+ readonly names: { readonly reserved: string; readonly threshold: string; readonly exhausted: string; readonly capBreached: string };
40
+ }
41
+ export type BudgetRun =
42
+ | { readonly kind: 'skip'; readonly disposition: 'inapplicable' | 'blocked'; readonly reason: string }
43
+ | { readonly kind: 'refused'; readonly status: number; readonly code: string | undefined }
44
+ | { readonly kind: 'observed'; readonly observation: BudgetObservation };
45
+
46
+ export interface Finding {
47
+ readonly rule: 'create' | 'lifecycle' | 'hard-stop' | 'advisory' | 'content-free';
48
+ readonly ok: boolean;
49
+ readonly doc: string;
50
+ readonly message: string;
51
+ }
52
+
53
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
54
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
55
+ try { return await fn(); } catch { return null; }
56
+ }
57
+
58
+ async function readEvents(profile: MajorProfile, runId: string): Promise<RunEvent[] | null> {
59
+ const out: RunEvent[] = [];
60
+ let after = 0;
61
+ for (let page = 0; page < 50; page++) {
62
+ const res = await http(() => driver.get(`${profile.runsPath}/${encodeURIComponent(runId)}/events/poll?timeout=1&afterSequence=${after}`));
63
+ if (res === null || res.status !== 200) return page === 0 ? null : out;
64
+ const batch = (res.json as { events?: unknown } | undefined)?.events;
65
+ if (!Array.isArray(batch) || batch.length === 0) return out;
66
+ for (const e of batch) {
67
+ if (!isRecord(e) || typeof e['type'] !== 'string' || typeof e['sequence'] !== 'number') continue;
68
+ out.push({ type: e['type'], sequence: e['sequence'], payload: isRecord(e['payload']) ? e['payload'] : {} });
69
+ after = Math.max(after, e['sequence']);
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /** Create the budgeted run, wait for it to end, and return its log. */
76
+ export async function drive(profile: MajorProfile, discovery: unknown, timeoutMs = 20_000): Promise<BudgetRun> {
77
+ const family = profile.family(discovery, 'budget');
78
+ if (family === null) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host does not advertise budget' };
79
+ const fragment = profile.runBudget(BUDGET_POLICY);
80
+ if (fragment === null) return { kind: 'skip', disposition: 'inapplicable', reason: `major ${profile.major} gives a run budget no createRun surface` };
81
+ const dims = family['dimensions'];
82
+ if (!Array.isArray(dims) || !dims.includes(DIMENSION)) return { kind: 'skip', disposition: 'inapplicable', reason: `budget.dimensions does not list ${DIMENSION} — the host does not enforce the dimension this witness spends` };
83
+ if (!isFixtureAdvertised(BUDGET_FIXTURE)) return { kind: 'skip', disposition: 'inapplicable', reason: `${BUDGET_FIXTURE} fixture not advertised — the host has not opted in to the unaided budget witness` };
84
+
85
+ const names = {
86
+ reserved: profile.eventType('budget.reserved'),
87
+ threshold: profile.eventType('budget.threshold.crossed'),
88
+ exhausted: profile.eventType('budget.exhausted'),
89
+ capBreached: profile.eventType('cap.breached'),
90
+ };
91
+ if (names.reserved === undefined || names.threshold === undefined || names.exhausted === undefined || names.capBreached === undefined) {
92
+ return { kind: 'skip', disposition: 'blocked', reason: `the event name map for major ${profile.major} is not on disk in this layout — the witness will not guess event names` };
93
+ }
94
+
95
+ const created = await http(() => driver.post(profile.runsPath, { workflowId: BUDGET_FIXTURE, ...fragment }));
96
+ if (created === null) return { kind: 'skip', disposition: 'blocked', reason: `POST ${profile.runsPath} unreachable (fetch failed)` };
97
+ if (created.status === 429) return { kind: 'skip', disposition: 'blocked', reason: `POST ${profile.runsPath} answered 429 — the run budget of the host, not the wire` };
98
+ const runId = (created.json as { runId?: unknown } | undefined)?.runId;
99
+ if (created.status !== 201 || typeof runId !== 'string') return { kind: 'refused', status: created.status, code: readErrorCode(created.json) ?? undefined };
100
+
101
+ const deadline = Date.now() + timeoutMs;
102
+ let snap: Record<string, unknown> | null = null;
103
+ for (;;) {
104
+ const res = await http(() => driver.get(`${profile.runsPath}/${encodeURIComponent(runId)}`));
105
+ snap = res?.status === 200 && isRecord(res.json) ? res.json : null;
106
+ if (snap !== null && TERMINAL.has(String(snap['status']))) break;
107
+ if (Date.now() > deadline) {
108
+ await http(() => driver.post(`${profile.runsPath}/${encodeURIComponent(runId)}/cancel`, {}));
109
+ return { kind: 'skip', disposition: 'blocked', reason: `the budgeted run did not reach a terminal status within ${timeoutMs} ms (last: ${snap === null ? 'unreadable' : String(snap['status'])})` };
110
+ }
111
+ await new Promise((r) => setTimeout(r, 250));
112
+ }
113
+ const events = await readEvents(profile, runId);
114
+ if (events === null) return { kind: 'skip', disposition: 'blocked', reason: `GET ${profile.runsPath}/{runId}/events/poll unreadable — the run ended but its log could not be read` };
115
+
116
+ const enforce = family['enforce'];
117
+ const error = snap['error'];
118
+ return {
119
+ kind: 'observed',
120
+ observation: {
121
+ enforce: enforce === 'hard' || enforce === 'advisory' ? enforce : undefined,
122
+ status: String(snap['status']),
123
+ errorCode: isRecord(error) && typeof error['code'] === 'string' ? error['code'] : undefined,
124
+ events,
125
+ names: names as BudgetObservation['names'],
126
+ },
127
+ };
128
+ }
129
+
130
+ /** What the log must show at this major. Pure. */
131
+ export function judge(profile: MajorProfile, o: BudgetObservation): Finding[] {
132
+ const home = `${profile.specRoot}/runs.md §budget section`;
133
+ const first = (type: string): RunEvent | undefined => o.events.find((e) => e.type === type);
134
+ const reserved = first(o.names.reserved);
135
+ const threshold = first(o.names.threshold);
136
+ const exhausted = first(o.names.exhausted);
137
+ const breach = o.events.find((e) => e.type === o.names.capBreached && typeof e.payload['kind'] === 'string' && (e.payload['kind'] as string).startsWith('budget-'));
138
+ const out: Finding[] = [];
139
+
140
+ out.push({ rule: 'lifecycle', ok: reserved !== undefined, doc: home, message: `a budgeted run MUST emit ${o.names.reserved}` });
141
+ out.push({ rule: 'lifecycle', ok: threshold !== undefined && typeof threshold.payload['percent'] === 'number', doc: home, message: `spending past thresholdPercent MUST emit ${o.names.threshold} with a numeric percent` });
142
+ out.push({ rule: 'lifecycle', ok: exhausted !== undefined, doc: home, message: `a budget that cannot cover the run MUST emit ${o.names.exhausted}` });
143
+ if (reserved !== undefined && threshold !== undefined && exhausted !== undefined) {
144
+ out.push({ rule: 'lifecycle', ok: reserved.sequence < threshold.sequence && threshold.sequence < exhausted.sequence, doc: home, message: `the log MUST order ${o.names.reserved} < ${o.names.threshold} < ${o.names.exhausted}` });
145
+ }
146
+
147
+ if (o.enforce === 'hard') {
148
+ out.push({ rule: 'hard-stop', ok: breach !== undefined && breach.payload['kind'] === CAP_KIND, doc: home, message: `hard exhaustion under onExhaustion: fail MUST emit ${o.names.capBreached} with kind ${CAP_KIND} (got ${breach === undefined ? 'none' : String(breach.payload['kind'])})` });
149
+ if (breach !== undefined && exhausted !== undefined) {
150
+ out.push({ rule: 'hard-stop', ok: exhausted.sequence <= breach.sequence, doc: home, message: `${o.names.capBreached} MUST NOT precede ${o.names.exhausted}` });
151
+ }
152
+ out.push({ rule: 'hard-stop', ok: o.status === 'failed' && o.errorCode === 'budget_exhausted', doc: home, message: `hard exhaustion MUST fail the run budget_exhausted (got ${o.status} ${o.errorCode ?? ''})`.trim() });
153
+ }
154
+ if (o.enforce === 'advisory') {
155
+ out.push({ rule: 'advisory', ok: breach === undefined && o.errorCode !== 'budget_exhausted', doc: home, message: 'an advisory host MUST NOT stop the run' });
156
+ }
157
+
158
+ const leaks: string[] = [];
159
+ for (const e of o.events) {
160
+ if (!e.type.startsWith('budget.') && e !== breach) continue;
161
+ for (const k of PRICING_KEYS) if (k in e.payload) leaks.push(`${e.type}.${k}`);
162
+ }
163
+ out.push({ rule: 'content-free', ok: leaks.length === 0, doc: home, message: `budget.* and cap.breached MUST NOT carry rate cards, unit prices or credentials (found ${leaks.join(', ') || 'none'})` });
164
+ return out;
165
+ }