@openwop/openwop-conformance 1.152.0 → 1.154.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -0
  3. package/dist/cli.js +7 -2
  4. package/dist/lib/requirement-ids.js +100 -0
  5. package/dist/lib/scenario-disposition.js +22 -0
  6. package/package.json +33 -2
  7. package/requirement-aliases.json +4 -0
  8. package/requirements.json +23554 -0
  9. package/schemas/CORPUS-STAMP.json +99 -3
  10. package/src/cli.ts +7 -5
  11. package/src/global-setup.ts +13 -0
  12. package/src/lib/corpus-stamp.ts +125 -0
  13. package/src/lib/requirement-ids.ts +111 -0
  14. package/src/lib/scenario-disposition.ts +22 -0
  15. package/src/setup.ts +84 -5
  16. package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
  17. package/src/lib/fork-availability.test.ts +0 -69
  18. package/src/lib/global-setup.test.ts +0 -76
  19. package/src/lib/grpc-framing.test.ts +0 -96
  20. package/src/lib/oidc-issuer.test.ts +0 -328
  21. package/src/lib/otel-collector-grpc.test.ts +0 -191
  22. package/src/lib/otel-collector.test.ts +0 -303
  23. package/src/lib/otlp-protobuf.test.ts +0 -461
  24. package/src/lib/polling.test.ts +0 -80
  25. package/src/lib/requirement-ledger.test.ts +0 -75
  26. package/src/lib/risk-disposition.test.ts +0 -91
  27. package/src/lib/saml-idp.test.ts +0 -127
  28. package/src/lib/spec-coherence-registry.test.ts +0 -155
  29. package/src/lib/webhook-receiver.test.ts +0 -144
  30. package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
  31. package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
  32. package/src/scenarios/capability-example-root-layout.test.ts +0 -272
  33. package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
  34. package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
  35. package/src/scenarios/compensation-profile.test.ts +0 -340
  36. package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
  37. package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
  38. package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
  39. package/src/scenarios/effect-identity-composition.test.ts +0 -129
  40. package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
  41. package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
  42. package/src/scenarios/form-content-packs.test.ts +0 -415
  43. package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
  44. package/src/scenarios/normative-example-extraction.test.ts +0 -242
  45. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
  46. package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
  47. package/src/scenarios/protocol-version-grammar.test.ts +0 -119
  48. package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
  49. package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
  50. package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
  51. package/src/scenarios/semantic-digest-v2.test.ts +0 -128
  52. package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
  53. package/src/scenarios/spec-section-citations.test.ts +0 -132
  54. package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
  55. package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
  56. package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
  57. package/src/scenarios/workload-identity-profile.test.ts +0 -184
@@ -1,5 +1,101 @@
1
1
  {
2
- "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.152.0",
4
- "corpusCommit": "8d02d922ac933096f36fc9e649b6ba574b4f609c"
2
+ "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md §\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
+ "suiteVersion": "1.154.0",
4
+ "corpusCommit": "07f4573cca4a742445d1078773f422d28f986ee5",
5
+ "files": {
6
+ "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
7
+ "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
8
+ "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
9
+ "api/openapi.yaml": "105e7bc27d216eb29fbb526b3548b5545711ec2a18c004bce20b08f5af38b750",
10
+ "api/redocly.yaml": "9f2374405b9ca58994ae076ada9fbe4dfb8265b77e4bf65331d1493192c0bd3a",
11
+ "schemas/README.md": "d9624e98ba3534b27fe4823227e8c2ac8df3ffd4bc4f57b4a023c91f5addefd6",
12
+ "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
13
+ "schemas/a2ui-surface-delta-frame.schema.json": "970d434e4fdcb925de64aaace91eb83bfbcfd8f76757a9ce9f339eda5fb3f4e8",
14
+ "schemas/agent-deployment-transition.schema.json": "1ac987fed0f380fa5b4e9a40e8e13316e5ae42d03a0ff42db4be6273e1bfe9f4",
15
+ "schemas/agent-deployment.schema.json": "0a71f0c7ab8cdf4fcac15e8652514f926cd84dfc7db32c47c9f3f3c9bd04f512",
16
+ "schemas/agent-eval-suite.schema.json": "d1aea50a48ddfe5ba8e25e9ddcc2c6e28eb1d953ef81b103518999a50e0967f7",
17
+ "schemas/agent-inventory-response.schema.json": "3a64dde3b95ece16ad73b95051ca99a1ca285f60255b5b506bed655676d82687",
18
+ "schemas/agent-manifest.schema.json": "b82738459792e24b14c4fe43622e8feb0df2250bda02054caa960c3556438b2c",
19
+ "schemas/agent-org-chart.schema.json": "2ca433503e76b29a70be35db22cce24bb698e6aa2f8b79c6fe6025f988f94fb0",
20
+ "schemas/agent-ref.schema.json": "b411bd51439e26d862393d546215d6b26d9349e6c9bdd92b353ca0fde2a51fb2",
21
+ "schemas/agent-roster-entry.schema.json": "3e0d8dfc0c66afb887bd05974a1ffbf79eece391255c09d2d0f7c5536aa219fd",
22
+ "schemas/agent-roster-response.schema.json": "1f6057bddd95d1cd31fdcef2fc87ae4560003c6e0c6d9e3fd1a148bac537c0b8",
23
+ "schemas/ai-envelope.schema.json": "78167a5767809286071bfcfcdfeceed0ed04c8b9f8c13079019d70eff4f351c1",
24
+ "schemas/annotation-create.schema.json": "567e8327777968e0ddab8e1547b4acd7beda734cf9d1fc6cde17037d8d9540d4",
25
+ "schemas/annotation.schema.json": "b26d22f9a97e1ae7c195e8799964aca804e78452a079ca3a0d61ea013907ce91",
26
+ "schemas/artifact-type-pack-manifest.schema.json": "13932ea58e819e01b3a0cc7a65590b70683dd5fe9208fc06c64c339943d3c4ce",
27
+ "schemas/audit-verify-result.schema.json": "e8fd013fb610ba5db27ab0113ec2eb18e7da5f9310dd3de2dd3dc87a259c21fe",
28
+ "schemas/budget-policy.schema.json": "3d4fe30ae129fe136ab9ece395f2a2a42a76360786b1b9a1d5f1cbc753eb5b39",
29
+ "schemas/capabilities.schema.json": "594d92806866886f3aefa7855e1ef5f33fec6459b06ef1ce6c07220c396b5661",
30
+ "schemas/certification-bundle-v2.schema.json": "3540232676086869ae78d24f3b9c2154f9c4a8a5eeb976512d6a7409f8cf19bb",
31
+ "schemas/channel-presence-payload.schema.json": "f9d0ef4219814c2b765baf3e68cbe5eea2cb42379f516a93a0459dcd5f7db47e",
32
+ "schemas/channel-written-payload.schema.json": "54723a56bc5a35232f64544ca686ac1ac959a5fb05b32b2401facca6b3ef51a3",
33
+ "schemas/chat-card-pack-manifest.schema.json": "402f7e9081cc39857a52e772061f4e034a957dcbc17501fe6e8e0bcb3d337e42",
34
+ "schemas/compact-tool-descriptor.schema.json": "4856d52aed0438d366b4328e53e4d248126e46f57137ad25fb04c3cc22bba1be",
35
+ "schemas/compensation-policy.schema.json": "6038bf939a982be1105d92b8a0bb70fae936b49d5e8187e1215660d265e2c382",
36
+ "schemas/conformance-certification-bundle.schema.json": "30a1f7fc35ed06098b7abb6fcf3b6f46b5716f29fbae35e0de1247f1f8cbe54c",
37
+ "schemas/connection-pack-manifest.schema.json": "35aca9fc3b3c8322babebe77ef77a8033673254f948025fb2f37191f4cfd05b5",
38
+ "schemas/conversation-event.schema.json": "bfcb88ce8dfe4dd7a3a40e56519a2833c7d25487fd2ab4104f6a23195120c0d9",
39
+ "schemas/conversation-turn.schema.json": "7c025df336aef9456d07f3a1a6959fc6d1c6fa62859e884354d71efee705ca3c",
40
+ "schemas/core-conformance-mock-agent-config.schema.json": "2253e1fb0cd8823ef2bfbae9b957ac374a7b617f62c1eb15d819e3536296feb3",
41
+ "schemas/credential-provenance.schema.json": "7675889469a67c68f31b8d0b28fd0b0a3c837f21ef51ce3b3c676064d3f58597",
42
+ "schemas/credential-reference.schema.json": "14927f095c235367a0645cc4811f4cfcfa3bed152bf05b970881831c62178b56",
43
+ "schemas/debug-bundle.schema.json": "8bd626e4cfbdaa774231e7182f61f2b5a4a42c3afeb8672c8388ea41e3fb7f2e",
44
+ "schemas/dispatch-config.schema.json": "131767797315e980f182b2480a1e2ed562d3326a2db08628704273f5b38c9e26",
45
+ "schemas/envelopes/clarification.request.schema.json": "3b3520a9ca2e40b4fb54b3a8741b2d3f2d414a0dfc004ba7c71dcf3786c32603",
46
+ "schemas/envelopes/error.schema.json": "845261720e8398975042ddfe78012491e42f8a39ed195b524c389cc9bc25f5c0",
47
+ "schemas/envelopes/media.audio.schema.json": "6c23f7398558ed781a36295c980a7ca9c585cc51faac8dd0ea56973be9dc96df",
48
+ "schemas/envelopes/media.file.schema.json": "3eb9ba4d3727a86640d82d566dca4fbcc06f6f0f3f80cfd100be869e9739d0b4",
49
+ "schemas/envelopes/media.image.schema.json": "1517b2036450226a20a55c223583d3cb2b606d157698a0806e00ca9ea1ea413e",
50
+ "schemas/envelopes/schema.request.schema.json": "16f6523778bdae67c715c10b0eb61fb8c7abaed4673d888957d6cc46f604be8c",
51
+ "schemas/envelopes/schema.response.schema.json": "35323b628db30a2b51efb66192f8f1219e184afa130126395524165a98539c6b",
52
+ "schemas/envelopes/ui.a2ui-surface.schema.json": "68f977c11b9bb40435a9c5727b66d76275f8dcfa0b380086622a837c84b8c5e4",
53
+ "schemas/error-envelope.schema.json": "1f2addeabbaa3cd7957c77816a1609b0c5005e0730cff78162bbc2804ff7c4aa",
54
+ "schemas/eval-summary.schema.json": "cda7ea443069f5cfd088cd000dccb4ea8b6e8268cfcc3af740295241bf77fa10",
55
+ "schemas/export-bundle.schema.json": "1ed674786293fc33f3474fb93cd3e5a1d8f85d0cb09636a09856bd8671e62c90",
56
+ "schemas/form-content-pack-manifest.schema.json": "bd373116c8912e2cd8a0e4df1f7a56086c0fdae472320f695552dd46b1b48a45",
57
+ "schemas/frontend-plugin-manifest.schema.json": "33d4e4eea5753900669c7ed6416e314376395a0256a9d656bf03a18845c16e84",
58
+ "schemas/goal.schema.json": "66aab339cda217318dab40e40ea0b46ea949164e7f2482e865284d6b2e3383db",
59
+ "schemas/heartbeat-evaluated.schema.json": "d3caa3fae3d935cc782c2359afa2d6bf11feb1ce9d71b9345b8e5716a9348c61",
60
+ "schemas/heartbeat-state-changed.schema.json": "6b66086582436ac72bd929d2830d0539d932a41b8ddf90263e2b8ca903d60584",
61
+ "schemas/localized-content-language-settings.schema.json": "6ba0e2918e08756493648a64d6169829eed0ead91fb6c3f2a76dec5c451b2d77",
62
+ "schemas/localized-content-page-response.schema.json": "3fd47db2484c40417ab7a555ec42bf443d48deca0c38ecdbe008a3bdcd1a6809",
63
+ "schemas/localized-content-page.schema.json": "2ca2d591d197562c4bf668b338e47803256cfa0c9105abdf64bd79e8469f15a8",
64
+ "schemas/localized-content-section.schema.json": "3e183eb2e80b985dc9b3770e97865247059d32ffbab2957f4a5c0addfdfedc2e",
65
+ "schemas/memory-entry.schema.json": "f5d806c934e900a979e14ccd961224d55eede954ffd10c5369f4cb97654e142b",
66
+ "schemas/memory-list-options.schema.json": "fd3e61c0caf984c6cf14f8acf55c0e84b6b579d62a702be45e20dbedfc9fffce",
67
+ "schemas/node-pack-manifest.schema.json": "3c7812fc8aae840edd8afe6d51fcb58571a50008e0b7ade4b7c5d55e8da9fcb8",
68
+ "schemas/orchestrator-decision.schema.json": "9dbcc9e7595df1781db91b1e8764fec67feca7d232b07d151094400aa53750e7",
69
+ "schemas/org-chart-responsibility-view.schema.json": "9bf2cc7b2ac8b6d7c28d360e8263da5706f691c364c30d20f16fd46b21fdd278",
70
+ "schemas/pack-lockfile.schema.json": "e6c6fd6c042485a3a16c5aedf184a3304b3068c6dd0cf8a22cba1e2e58781556",
71
+ "schemas/prompt-kind.schema.json": "fcf4305b430dc9d2d34b6f2971f4185fd2ed7f617013720d975bfc3ab665e000",
72
+ "schemas/prompt-pack-manifest.schema.json": "d473d827797ce6c9fe4fe03e439840a9dbc78d899bfb9ff05ff3e6a747380977",
73
+ "schemas/prompt-ref.schema.json": "2fffd62cb27c36ddfa32363922b2cb5339091716112c54ded32c21a8c26398bb",
74
+ "schemas/prompt-template.schema.json": "54fc1f0d22b938d171dff214f3d446ae911ef0c3cdb2056929b93d01248e0969",
75
+ "schemas/proposal.schema.json": "472ac8c6bcbb4e34ae8fe7a0757567de4875e69aa5058dac4dc9226ddba6c731",
76
+ "schemas/registry-version-manifest.schema.json": "437890063087beab3cadbdb0dae354e9f92a3ebbecdd90a1d0487df71d34ed93",
77
+ "schemas/residency.schema.json": "b45fdc91ae88091251914dbd6787a1e2ce687580b91d31a759217b7a1f0010c0",
78
+ "schemas/run-ancestry-response.schema.json": "963a5763b03216aa72fc2854560c778e53994f8cb06ea9a4053e541273c87f5c",
79
+ "schemas/run-diff-response.schema.json": "d9d196730b5d5d8a04a591d21a8fc6b44be7fa134f11827ff7e4f1a86d47e83d",
80
+ "schemas/run-event-payloads.schema.json": "6907c2a388d27169756781e1a5771e411238b465d9f0917698cde25dde6bccae",
81
+ "schemas/run-event.schema.json": "c70c06932b9831295042836213e617c8e946f20ddd49cf4e889b74d89d2175a8",
82
+ "schemas/run-options.schema.json": "f8241ece6ddc6527de9160d811d42c231b4f77b935f2e107b3c05608545c59ab",
83
+ "schemas/run-orchestrator-decided-event.schema.json": "17a43a3c44680a24d411141357a867b3caed6107d56de88fe2e062eaf93fa2c1",
84
+ "schemas/run-snapshot.schema.json": "05c406120e1958984ac7e06deff369f2df9f4d7b72ced9b624c327c1c86999e8",
85
+ "schemas/security-advisory.schema.json": "9c550f0bf10a3193dcca4a127081fb66fe5dc1b6f729c4c17bfaea3ba3ce05fc",
86
+ "schemas/self-hosted-runner-dispatch-frame.schema.json": "3f1d7edd572c6747657299525d8e85329b93b6fadf670e3a19c74d53e725adec",
87
+ "schemas/self-hosted-runner-registration.schema.json": "6c781adf1bb915a9e47796494f4a527353cbbf72b480e5a05be4aac65a6d0c6e",
88
+ "schemas/self-hosted-runner-result-frame.schema.json": "16f2f711b6071c797d8d4065f6f5ff39102ff03890fefce7a88a669831566296",
89
+ "schemas/suspend-request.schema.json": "1aa39f2a947934bedc9f9696bf45fcb6119395c580d5ca282efb9443bd94a7bb",
90
+ "schemas/tool-descriptor.schema.json": "7f6adbae63003c1f1311f6a436f7f286542be62aa19a27b46730f2c2810857f7",
91
+ "schemas/trigger-event.schema.json": "59d63b79dca3593f45d281776cbfe43ae4d85ee25519b82ad59f162a9efb592c",
92
+ "schemas/trigger-subscription-registration.schema.json": "929d628bad08e1def696e47e9a148aba00fa801e28d49bbd0dc2360b08673435",
93
+ "schemas/trigger-subscription.schema.json": "4f7b067f20032073b140c4bb15dc9a9914c4f6421a252ff1ff261cbdd8edf270",
94
+ "schemas/ui-plugin-message.schema.json": "346638babf1f8ea7b161cde75c0038b518cd2cc5bf0631772e20a9b74c96b5dd",
95
+ "schemas/workflow-chain-pack-manifest.schema.json": "c027571913c44d8d9678750c72701a710a02fa0df55e86849da03e13e95c333a",
96
+ "schemas/workflow-definition.schema.json": "28b050ffe22121796c9d4a5a8c7e57c901bb283bd08d3fd3ff2ea25a898108c1",
97
+ "schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
98
+ "schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
99
+ "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4"
100
+ }
5
101
  }
package/src/cli.ts CHANGED
@@ -195,7 +195,7 @@ Required (unless --offline):
195
195
  --api-key <key> Bearer-style API key (or set OPENWOP_API_KEY env var)
196
196
 
197
197
  Filtering:
198
- --offline Run only the server-free subset (fixtures + spec corpus)
198
+ --offline Run only the declared server-free subset (fixtures-valid; see README §"--offline")
199
199
  --filter <pattern> Pass through to vitest --testNamePattern
200
200
 
201
201
  Implementation labels (cosmetic — surface in failure messages):
@@ -656,10 +656,12 @@ async function main(): Promise<never> {
656
656
  // the conformance package is used as a workspace member.
657
657
  const vitestArgs: string[] = ['run', '--config', resolvePath(conformanceRoot, 'vitest.config.ts')];
658
658
  if (args.offline) {
659
- vitestArgs.push(
660
- 'src/scenarios/fixtures-valid.test.ts',
661
- 'src/scenarios/spec-corpus-validity.test.ts',
662
- );
659
+ // Suite 1.154.0: the offline set is a DECLARED property of the package —
660
+ // exactly the server-free scenarios that ship in the tarball and run in
661
+ // the published layout. `spec-corpus-validity.test.ts` left the set: it is
662
+ // a corpus-coherence scenario (reads spec/v1, asserts nothing about a host)
663
+ // and is no longer packed. See conformance/README.md §"--offline".
664
+ vitestArgs.push('src/scenarios/fixtures-valid.test.ts');
663
665
  }
664
666
  if (args.filter) {
665
667
  vitestArgs.push('--testNamePattern', args.filter);
@@ -56,6 +56,8 @@ const MAX_ROWS = 40;
56
56
  // at runtime, so this costs nothing and makes the compiler the oracle: rename
57
57
  // a ledger field and this file stops building instead of silently degrading.
58
58
  import type { LedgerEntry } from './lib/requirement-ledger.js';
59
+ import { LAYOUT, PKG_ROOT_PATH } from './lib/paths.js';
60
+ import { describeVerdict, verifyCorpusStamp } from './lib/corpus-stamp.js';
59
61
 
60
62
  /** What a JSONL line parses to before validation — every field may be absent. */
61
63
  type LedgerLine = Partial<Record<keyof LedgerEntry, unknown>>;
@@ -64,6 +66,17 @@ let ownedDir: string | null = null;
64
66
  let ledgerPath: string | null = null;
65
67
 
66
68
  export function setup(): void {
69
+ // Suite 1.154.0 — the vendored contract must be the one this suite shipped.
70
+ // In the published layout every api/ + schemas/ file is checked against the
71
+ // SHA-256 map pack-vendor.sh wrote into schemas/CORPUS-STAMP.json; a mismatch
72
+ // aborts the run (a suite validating a host against a schema it did not ship
73
+ // is evidence about nothing). Repo layout: nothing vendored, nothing to check,
74
+ // and the log line says so rather than implying a pass.
75
+ const stamp = verifyCorpusStamp(PKG_ROOT_PATH, LAYOUT);
76
+ process.stderr.write(`${describeVerdict(stamp)}\n`);
77
+ if (stamp.kind === 'mismatch') {
78
+ throw new Error('openwop-conformance: refusing to run — schemas/CORPUS-STAMP.json digests do not match the vendored api/ + schemas/ files. Reinstall the package; do not hand-patch vendored contract files.');
79
+ }
67
80
  if (process.env['OPENWOP_DISPOSITION_SUMMARY'] === 'false') return;
68
81
  // `--certify` already routes the ledger to its own report dir and reads it
69
82
  // there. Never take it over — this exists only for the path that had no
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Corpus-stamp digest verification (v2 charter Phase 1, suite 1.154.0).
3
+ *
4
+ * The published suite vendors `api/` and `schemas/` at `prepack` and writes
5
+ * `schemas/CORPUS-STAMP.json` (RFC 0145 G2) — until 1.154.0 that stamp carried
6
+ * only `suiteVersion` + `corpusCommit`, so a tarball whose vendored files had
7
+ * been altered, partially copied, or hand-patched after install still read as
8
+ * "the 1.x contract". That is the generated-surface-drift class the charter's
9
+ * §A names (`@openwop/openwop-conformance@1.138.1` on npm was not the 1.138.1 in
10
+ * the tree), one layer down: the package identified its corpus by a commit
11
+ * hash it could not check.
12
+ *
13
+ * From 1.154.0 the stamp also carries `files`: a map of every vendored path
14
+ * (relative to the package root) to its SHA-256. In the PUBLISHED layout the
15
+ * suite verifies the map at global setup and refuses to run on a mismatch —
16
+ * a suite that validated a host against a schema it did not ship would be
17
+ * evidence about nothing. In the REPO layout (a spec checkout) there is no
18
+ * vendored copy and nothing to verify; the check is skipped and says so.
19
+ *
20
+ * Three outcomes, never folded (OK / FAIL / UNKNOWN): `verified` when every
21
+ * digest matches; `mismatch` (throws) when any file is missing or altered;
22
+ * `not-applicable` when the layout is not published or the stamp predates
23
+ * 1.154.0 (older tarballs cannot be verified and the run says so rather than
24
+ * pretending).
25
+ */
26
+
27
+ import { createHash } from 'node:crypto';
28
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
29
+ import { join, relative, sep } from 'node:path';
30
+
31
+ export interface CorpusStamp {
32
+ readonly suiteVersion?: string;
33
+ readonly corpusCommit?: string;
34
+ /** `<relative path>` → lowercase hex SHA-256. Present from suite 1.154.0. */
35
+ readonly files?: Readonly<Record<string, string>>;
36
+ }
37
+
38
+ export type StampVerdict =
39
+ | { readonly kind: 'verified'; readonly files: number }
40
+ | { readonly kind: 'not-applicable'; readonly reason: string }
41
+ | { readonly kind: 'mismatch'; readonly missing: readonly string[]; readonly altered: readonly string[]; readonly extra: readonly string[] };
42
+
43
+ export const STAMP_RELATIVE_PATH = join('schemas', 'CORPUS-STAMP.json');
44
+
45
+ /** SHA-256 of a file's bytes, lowercase hex. */
46
+ export function sha256File(path: string): string {
47
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
48
+ }
49
+
50
+ /** Every regular file under `dir`, as package-relative POSIX paths, sorted. */
51
+ export function listVendoredFiles(root: string, dirs: readonly string[] = ['api', 'schemas']): string[] {
52
+ const out: string[] = [];
53
+ const walk = (d: string): void => {
54
+ if (!existsSync(d)) return;
55
+ for (const name of readdirSync(d).sort()) {
56
+ const p = join(d, name);
57
+ if (statSync(p).isDirectory()) walk(p);
58
+ else out.push(relative(root, p).split(sep).join('/'));
59
+ }
60
+ };
61
+ for (const d of dirs) walk(join(root, d));
62
+ return out.filter((p) => p !== STAMP_RELATIVE_PATH.split(sep).join('/')).sort();
63
+ }
64
+
65
+ /** Build the `files` map for a package root (used by pack-vendor at prepack). */
66
+ export function digestVendoredFiles(root: string): Record<string, string> {
67
+ const map: Record<string, string> = {};
68
+ for (const rel of listVendoredFiles(root)) map[rel] = sha256File(join(root, ...rel.split('/')));
69
+ return map;
70
+ }
71
+
72
+ /**
73
+ * Compare the stamp's `files` map against the bytes on disk. Pure: reads,
74
+ * never writes, never throws on a mismatch — the caller decides how loud.
75
+ */
76
+ export function verifyCorpusStamp(root: string, layout: string): StampVerdict {
77
+ if (layout !== 'published') {
78
+ return { kind: 'not-applicable', reason: `layout is '${layout}' — the vendored copy exists only in the published package` };
79
+ }
80
+ const stampPath = join(root, STAMP_RELATIVE_PATH);
81
+ if (!existsSync(stampPath)) {
82
+ return { kind: 'not-applicable', reason: 'schemas/CORPUS-STAMP.json is absent — not a packed suite' };
83
+ }
84
+ let stamp: CorpusStamp;
85
+ try {
86
+ stamp = JSON.parse(readFileSync(stampPath, 'utf8')) as CorpusStamp;
87
+ } catch (e) {
88
+ return { kind: 'mismatch', missing: [STAMP_RELATIVE_PATH], altered: [], extra: [] };
89
+ }
90
+ if (stamp.files === undefined) {
91
+ return { kind: 'not-applicable', reason: `stamp ${stamp.suiteVersion ?? '?'} predates per-file digests (suite < 1.154.0); the vendored contract cannot be verified` };
92
+ }
93
+ const expected = stamp.files;
94
+ const onDisk = new Set(listVendoredFiles(root));
95
+ const missing: string[] = [];
96
+ const altered: string[] = [];
97
+ for (const [rel, digest] of Object.entries(expected).sort()) {
98
+ const p = join(root, ...rel.split('/'));
99
+ if (!existsSync(p)) {
100
+ missing.push(rel);
101
+ continue;
102
+ }
103
+ if (sha256File(p) !== digest) altered.push(rel);
104
+ onDisk.delete(rel);
105
+ }
106
+ const extra = [...onDisk].sort();
107
+ if (missing.length > 0 || altered.length > 0 || extra.length > 0) return { kind: 'mismatch', missing, altered, extra };
108
+ return { kind: 'verified', files: Object.keys(expected).length };
109
+ }
110
+
111
+ /** One-line, greppable rendering for the run log. */
112
+ export function describeVerdict(v: StampVerdict): string {
113
+ switch (v.kind) {
114
+ case 'verified':
115
+ return `[openwop-conformance] corpus stamp VERIFIED — ${v.files} vendored api/ + schemas/ files match their SHA-256 digests`;
116
+ case 'not-applicable':
117
+ return `[openwop-conformance] corpus stamp not checked — ${v.reason}`;
118
+ case 'mismatch':
119
+ return (
120
+ `[openwop-conformance] corpus stamp MISMATCH — the vendored contract is not the one this suite shipped ` +
121
+ `(missing ${v.missing.length}, altered ${v.altered.length}, extra ${v.extra.length}): ` +
122
+ [...v.missing.map((f) => `missing ${f}`), ...v.altered.map((f) => `altered ${f}`), ...v.extra.map((f) => `extra ${f}`)].slice(0, 12).join('; ')
123
+ );
124
+ }
125
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Per-`it` requirement ids (RFC 0148 §A, gap G3; v2 charter Phase 1).
3
+ *
4
+ * The ledger has recorded ONE row per scenario FILE (`openwop.floor.<file>` /
5
+ * `openwop.scenario.<file>`). That granularity is what produces certification
6
+ * gap G8: a file that asserted a positive control and then soft-skipped the
7
+ * requirement still resolves to `executed-pass` for the whole file. The durable
8
+ * fix named in `scenario-disposition.ts` is per-`it` recording — this module
9
+ * supplies the ids for it.
10
+ *
11
+ * Why the id derives from the TEST TITLE and not from the `driver.describe`
12
+ * text: 190 of 1,756 citations interpolate the requirement text at run time
13
+ * (`docs/REQUIREMENT-REGISTRY-FEASIBILITY.md`), so no text-derived id can be
14
+ * stable for them. Test titles are literals in every scenario file, stable
15
+ * across hosts, and already the unit vitest reports on. The registry
16
+ * (`conformance/requirements.json`, generated) maps each id back to the
17
+ * citations found inside that test's body, so a bundle reader can still ask
18
+ * "which spec section did this row witness".
19
+ *
20
+ * Grammar: `openwop.it.<file-stem>.<title-slug>` where the stem is the scenario
21
+ * file name without `.test.ts` and the slug is the title lower-cased, every run
22
+ * of non-alphanumerics folded to one `-`, trimmed, and capped at 80 characters.
23
+ * A second test in the same file whose title slugs identically gets `~2`,
24
+ * `~3`, … (vitest allows duplicate titles; the ledger does not allow duplicate
25
+ * ids). A scenario MAY override the derived id for the current test with
26
+ * `req()` when it wants a hand-authored, registry-listed id.
27
+ */
28
+
29
+ export const IT_ID_PREFIX = 'openwop.it.';
30
+ export const IT_ID_GRAMMAR = /^openwop\.it\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*(?:~[2-9][0-9]*)?$/;
31
+
32
+ const MAX_SLUG = 80;
33
+
34
+ /** Title → slug: lower-case, non-alphanumeric runs → `-`, trimmed, capped. */
35
+ export function slugTitle(title: string): string {
36
+ const s = title
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/^-+|-+$/g, '');
40
+ const capped = s.length > MAX_SLUG ? s.slice(0, MAX_SLUG).replace(/-+$/g, '') : s;
41
+ return capped.length > 0 ? capped : 'untitled';
42
+ }
43
+
44
+ /** `auth-subject-link.test.ts` → `auth-subject-link`. */
45
+ export function fileStem(scenarioFile: string): string {
46
+ return scenarioFile.replace(/\.test\.ts$/, '');
47
+ }
48
+
49
+ /** Derived id for one test, before collision suffixing. */
50
+ export function itRequirementId(scenarioFile: string, title: string): string {
51
+ return `${IT_ID_PREFIX}${fileStem(scenarioFile)}.${slugTitle(title)}`;
52
+ }
53
+
54
+ /** The scenario file a per-`it` id belongs to, or null when the id is not one. */
55
+ export function scenarioFileOfItId(requirementId: string): string | null {
56
+ if (!requirementId.startsWith(IT_ID_PREFIX)) return null;
57
+ const rest = requirementId.slice(IT_ID_PREFIX.length);
58
+ const dot = rest.indexOf('.');
59
+ if (dot <= 0) return null;
60
+ return `${rest.slice(0, dot)}.test.ts`;
61
+ }
62
+
63
+ /**
64
+ * Collision suffixing: the first occurrence keeps the bare id, later ones get
65
+ * `~2`, `~3`, … Keyed per file so a worker running many files never crosses
66
+ * streams. Reset per file by `setup.ts` when the file finishes.
67
+ */
68
+ export class ItIdAllocator {
69
+ private readonly seen = new Map<string, number>();
70
+
71
+ allocate(scenarioFile: string, title: string): string {
72
+ const base = itRequirementId(scenarioFile, title);
73
+ const n = (this.seen.get(base) ?? 0) + 1;
74
+ this.seen.set(base, n);
75
+ return n === 1 ? base : `${base}~${n}`;
76
+ }
77
+
78
+ reset(): void {
79
+ this.seen.clear();
80
+ }
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Explicit override for the current test.
85
+ // ---------------------------------------------------------------------------
86
+
87
+ let explicitId: string | null = null;
88
+
89
+ /**
90
+ * Attach a hand-authored requirement id to the CURRENT test. The per-`it`
91
+ * ledger row for this test is recorded under `id` instead of the derived
92
+ * title id. Returns the usual `driver.describe`-style message so the call
93
+ * doubles as the assertion message:
94
+ *
95
+ * expect(x, req('openwop.auth.subject-link.leaver-deny', 'auth-profiles.md §Subject linking', 'deactivation MUST deny')).toBe(true)
96
+ *
97
+ * The id MUST be listed in `conformance/requirements.json` (the generator
98
+ * collects `req(` first-argument literals); an unlisted id fails the registry
99
+ * check, so a hand id cannot drift from the registry silently.
100
+ */
101
+ export function req(id: string, specSection: string, requirement: string): string {
102
+ explicitId = id;
103
+ return `${specSection}: ${requirement}`;
104
+ }
105
+
106
+ /** setup.ts reads and clears the override after each test. */
107
+ export function takeExplicitRequirementId(): string | null {
108
+ const id = explicitId;
109
+ explicitId = null;
110
+ return id;
111
+ }
@@ -26,6 +26,7 @@
26
26
  * host or a vitest subprocess.
27
27
  */
28
28
 
29
+ import { scenarioFileOfItId } from './requirement-ids.js';
29
30
  import { PROFILE_FLOOR_SCENARIOS } from './profiles.js';
30
31
  import { requirementIdForScenario, requirementIdForPrefix, requirementsFor } from './requirement-registry.js';
31
32
  import { UNCLASSIFIED_RETURN_DETAIL } from './soft-skip.js';
@@ -285,6 +286,27 @@ export function deriveRequirementDispositions(
285
286
  rows.push(row);
286
287
  }
287
288
 
289
+ // Per-`it` rows (suite 1.153.0): every ledger entry keyed `openwop.it.<file>.<slug>`
290
+ // becomes its own bundle row, attributed to its scenario file. Additive — the
291
+ // file-level and prefix rows above are unchanged, and the floors still key on
292
+ // them. This is the granularity RFC 0148 §A describes and the G8 fix.
293
+ const emitted = new Set(rows.map((r) => r.requirementId));
294
+ for (const e of [...ledger].sort((a, b) => a.requirementId.localeCompare(b.requirementId))) {
295
+ const file = scenarioFileOfItId(e.requirementId);
296
+ // Attribute only to files this run reported on: a worker's ledger can carry
297
+ // rows from files outside the certified set (the suite's own lib tests, or
298
+ // a filtered run), and those are not evidence about the host.
299
+ if (file === null || emitted.has(e.requirementId) || !reportStates.has(file)) continue;
300
+ emitted.add(e.requirementId);
301
+ rows.push({
302
+ requirementId: e.requirementId,
303
+ scenarioId: file,
304
+ disposition: e.disposition,
305
+ ...(e.detail === undefined ? {} : { detail: e.detail }),
306
+ ...(e.assertionCount === undefined ? {} : { assertionCount: e.assertionCount }),
307
+ });
308
+ }
309
+
288
310
  const totals = { executedPass: 0, executedFail: 0, skipped: 0, inapplicable: 0, blocked: 0 };
289
311
  for (const r of rows) {
290
312
  if (r.disposition === 'executed-pass') totals.executedPass++;
package/src/setup.ts CHANGED
@@ -30,11 +30,13 @@ import { setMultiAgentCapabilities } from './lib/multi-agent-capabilities.js';
30
30
  import { OtelCollector, setCollector } from './lib/otel-collector.js';
31
31
  import { McpFakeServer, setMcpFakeServer } from './lib/mcp-fake-server.js';
32
32
  import { A2AFakePeer, setA2AFakePeer } from './lib/a2a-fake-peer.js';
33
- import { afterAll, afterEach, beforeAll, expect } from 'vitest';
33
+ import { afterAll, afterEach, beforeAll, beforeEach, expect } from 'vitest';
34
34
  import { basename } from 'node:path';
35
35
  import { recordRequirement, hasRequirement, journalLength, journalSince } from './lib/requirement-ledger.js';
36
36
  import { requirementIdForFile, resolveFileRecord, type FileTestState } from './lib/scenario-disposition.js';
37
37
  import { softSkipDisposition } from './lib/soft-skip.js';
38
+ import { ItIdAllocator, takeExplicitRequirementId } from './lib/requirement-ids.js';
39
+ import { SPEC_COHERENCE_SCENARIOS, SPEC_COHERENCE_DETAIL } from './lib/spec-coherence.js';
38
40
  import type { DiscoveryPayload } from './lib/profiles.js';
39
41
 
40
42
  const SUITE_INIT_TIMEOUT_MS = 5_000;
@@ -225,6 +227,22 @@ await maybeStartA2AFakePeer();
225
227
  const _fileStates = new Map<string, FileTestState[]>();
226
228
  const _fileAssertions = new Map<string, number>();
227
229
  const _ledgerMarks = new Map<string, number>();
230
+ // Per-`it` recording (v2 charter Phase 1, suite 1.153.0 — the durable G8 fix
231
+ // named in scenario-disposition.ts). Each test gets its own ledger row under
232
+ // `openwop.it.<file>.<title-slug>` so a file that asserted a positive control
233
+ // and soft-skipped the requirement no longer certifies the requirement. The
234
+ // file-level row is RETAINED (floors key on it); the per-`it` rows are
235
+ // additive bundle rows (RFC 0148 §C `requirements[]` accepts any id).
236
+ const _itAllocators = new Map<string, ItIdAllocator>();
237
+ const _itMarks = new Map<string, number>();
238
+ const _itAssertionsBefore = new Map<string, number>();
239
+ function _assertionCalls(): number {
240
+ try {
241
+ return (expect.getState() as { assertionCalls?: number }).assertionCalls ?? 0;
242
+ } catch {
243
+ return 0;
244
+ }
245
+ }
228
246
  function _fileOf(task: { file?: { filepath?: string; name?: string } } | undefined): string | null {
229
247
  const f = task?.file?.filepath ?? task?.file?.name;
230
248
  return typeof f === 'string' && f.length > 0 ? basename(f) : null;
@@ -236,6 +254,14 @@ beforeAll(({}, suite) => {
236
254
  const file = _fileOf({ file: s.file ?? s });
237
255
  if (file !== null && !_ledgerMarks.has(file)) _ledgerMarks.set(file, journalLength());
238
256
  });
257
+ beforeEach(({ task }) => {
258
+ const file = _fileOf(task as { file?: { filepath?: string; name?: string } });
259
+ if (file === null) return;
260
+ // Window for this test's own gate decisions and its own assertion count.
261
+ _itMarks.set(file, journalLength());
262
+ _itAssertionsBefore.set(file, _assertionCalls());
263
+ takeExplicitRequirementId(); // clear any override left by a test that threw before afterEach
264
+ });
239
265
  afterEach(({ task }) => {
240
266
  const file = _fileOf(task as { file?: { filepath?: string; name?: string } });
241
267
  if (file === null) return;
@@ -248,13 +274,63 @@ afterEach(({ task }) => {
248
274
  // A leg that early-returns from a gate makes zero, and a file of such legs is
249
275
  // an `executed-pass` with assertionCount 0 — visible, and unclassified for a
250
276
  // claimed floor.
251
- let calls = 0;
277
+ //
278
+ // `expect.getState().assertionCalls` is per-test in vitest (reset at each
279
+ // test start), so it is this test's count; the file total is the sum.
280
+ const calls = _assertionCalls();
281
+ _fileAssertions.set(file, (_fileAssertions.get(file) ?? 0) + calls);
282
+
283
+ // Per-`it` row. Disposition follows RFC 0148 §A at test granularity:
284
+ // fail → executed-fail (detail = the first error message)
285
+ // skip (ctx.skip / it.skip) → the gate's recorded reason since this test began, else `skipped`
286
+ // pass with ≥1 assertion → executed-pass
287
+ // pass with 0 assertions → the gate's recorded reason (softSkip / seamAbsent / behaviorGate)
288
+ // since this test began, else `blocked` (unclassified return)
289
+ if (!file.endsWith('.test.ts')) return;
290
+ // Only SCENARIO files get per-`it` rows. The setup hooks run for every file in
291
+ // the worker, including `src/lib/*.test.ts` — the suite's own self-tests,
292
+ // which prove fixtures and helpers, not a host, and must never become bundle
293
+ // evidence (that is why RFC 0163 G5 / RFC 0050 G1 moved them out of scenarios).
294
+ const filepath = (task as { file?: { filepath?: string } }).file?.filepath ?? '';
295
+ if (!/[\\/]src[\\/]scenarios[\\/]/.test(filepath)) {
296
+ takeExplicitRequirementId();
297
+ return;
298
+ }
299
+ const explicit = takeExplicitRequirementId();
300
+ const alloc = _itAllocators.get(file) ?? new ItIdAllocator();
301
+ _itAllocators.set(file, alloc);
302
+ const itId = explicit ?? alloc.allocate(file, task.name);
303
+ const since = journalSince(_itMarks.get(file) ?? 0);
304
+ const gate = since.find((e) => e.disposition === 'inapplicable') ?? since.find((e) => e.disposition === 'skipped');
305
+ let disposition: 'executed-pass' | 'executed-fail' | 'skipped' | 'inapplicable' | 'blocked';
306
+ let detail: string | undefined;
307
+ if (SPEC_COHERENCE_SCENARIOS.has(file) || SPEC_COHERENCE_SCENARIOS.has(file.replace(/\.test\.ts$/, ''))) {
308
+ // Corpus-coherence scenario: it reads spec/v1 and asserts nothing about a
309
+ // host, in any layout. Its rows are `inapplicable` to every host — the same
310
+ // rule `resolveFileRecord` applies to the file in the published layout.
311
+ disposition = 'inapplicable';
312
+ detail = SPEC_COHERENCE_DETAIL;
313
+ } else if (state === 'fail') {
314
+ disposition = 'executed-fail';
315
+ const err = (task.result?.errors ?? [])[0] as { message?: string } | undefined;
316
+ detail = `the test executed and failed: ${(err?.message ?? 'no message').slice(0, 300)}`;
317
+ } else if (state === 'pass' && calls > 0) {
318
+ disposition = 'executed-pass';
319
+ } else if (gate !== undefined) {
320
+ disposition = gate.disposition as 'skipped' | 'inapplicable';
321
+ detail = gate.detail ?? `${gate.disposition} (gate recorded no reason)`;
322
+ } else if (state === 'pass') {
323
+ disposition = 'blocked';
324
+ detail = 'unclassified return: the test passed with zero assertions and recorded no reason — RFC 0148 §A resolves it to blocked, never to a pass';
325
+ } else {
326
+ disposition = 'skipped';
327
+ detail = 'vitest skipped the test (ctx.skip / it.skip) without a recorded gate reason';
328
+ }
252
329
  try {
253
- calls = (expect.getState() as { assertionCalls?: number }).assertionCalls ?? 0;
330
+ recordRequirement(itId, disposition, detail, { assertionCount: calls });
254
331
  } catch {
255
- /* no state count 0 */
332
+ /* never fail a test for bookkeeping */
256
333
  }
257
- _fileAssertions.set(file, (_fileAssertions.get(file) ?? 0) + calls);
258
334
  });
259
335
  afterAll(({}, suite) => {
260
336
  // vitest 4: the suite/file task is the SECOND argument. For a file-level
@@ -306,5 +382,8 @@ afterAll(({}, suite) => {
306
382
  _fileStates.delete(file);
307
383
  _fileAssertions.delete(file);
308
384
  _ledgerMarks.delete(file);
385
+ _itAllocators.delete(file);
386
+ _itMarks.delete(file);
387
+ _itAssertionsBefore.delete(file);
309
388
  });
310
389