@openwop/openwop-conformance 1.163.0 → 2.0.0-rc.1

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 (611) hide show
  1. package/README.md +11 -2
  2. package/dist/cli.js +262 -14
  3. package/dist/lib/certification-bundle-v3.js +142 -0
  4. package/dist/lib/paths.js +25 -3
  5. package/dist/lib/requirement-ids.js +57 -4
  6. package/dist/lib/requirement-ledger.js +1 -0
  7. package/dist/lib/scenario-disposition.js +6 -1
  8. package/dist/lib/spec-coherence.js +13 -32
  9. package/dist/spec-artifacts.lock.json +5 -0
  10. package/package.json +11 -33
  11. package/requirements.json +22892 -11701
  12. package/scenario-majors.json +1493 -0
  13. package/schemas/CORPUS-STAMP.json +137 -5
  14. package/src/cli.ts +236 -19
  15. package/src/global-setup.ts +3 -2
  16. package/src/lib/certification-bundle-v3.ts +175 -0
  17. package/src/lib/corpus-stamp.ts +29 -1
  18. package/src/lib/driver.ts +9 -1
  19. package/src/lib/era2-seed.ts +197 -0
  20. package/src/lib/paths.ts +25 -3
  21. package/src/lib/requirement-ids.ts +54 -4
  22. package/src/lib/requirement-ledger.ts +7 -1
  23. package/src/lib/scenario-disposition.ts +6 -1
  24. package/src/lib/seams.ts +45 -0
  25. package/src/lib/spec-coherence.ts +15 -32
  26. package/src/lib/v2.ts +62 -0
  27. package/src/scenarios/a2a-1-0-agent-card.test.ts +17 -16
  28. package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +11 -10
  29. package/src/scenarios/a2a-card-runtime-consistency.test.ts +18 -17
  30. package/src/scenarios/a2a-peer-authority.test.ts +7 -6
  31. package/src/scenarios/a2a-task-roundtrip.test.ts +30 -29
  32. package/src/scenarios/a2a-version-negotiation.test.ts +16 -14
  33. package/src/scenarios/a2ui-surface-degrades.test.ts +5 -3
  34. package/src/scenarios/a2ui-surface-delta-transport.test.ts +35 -33
  35. package/src/scenarios/a2ui-surface-replay.test.ts +6 -4
  36. package/src/scenarios/a2ui-surface-shape.test.ts +11 -10
  37. package/src/scenarios/a2ui-surface-version-refusal.test.ts +6 -4
  38. package/src/scenarios/a2ui-untrusted-blocks-approval.test.ts +5 -3
  39. package/src/scenarios/agent-capability-degraded-projection.test.ts +12 -11
  40. package/src/scenarios/agent-channel-dispatch.test.ts +18 -16
  41. package/src/scenarios/agent-deployment-lifecycle.test.ts +20 -19
  42. package/src/scenarios/agent-deployment-shape.test.ts +22 -24
  43. package/src/scenarios/agent-eval-run.test.ts +19 -18
  44. package/src/scenarios/agent-eval-suite-shape.test.ts +20 -22
  45. package/src/scenarios/agent-live-allowlist-enforced.test.ts +6 -5
  46. package/src/scenarios/agent-live-invocation-bracket.test.ts +13 -12
  47. package/src/scenarios/agent-live-runtime-shape.test.ts +10 -12
  48. package/src/scenarios/agent-live-structured-output.test.ts +7 -6
  49. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -3
  50. package/src/scenarios/agent-loop-stateful-resume.test.ts +2 -2
  51. package/src/scenarios/agent-loop-version5-shape.test.ts +4 -4
  52. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +3 -3
  53. package/src/scenarios/agent-manifest-role-profile.test.ts +9 -9
  54. package/src/scenarios/agent-manifest-runtime.test.ts +13 -12
  55. package/src/scenarios/agent-org-chart-scoping.test.ts +14 -13
  56. package/src/scenarios/agent-org-chart-shape.test.ts +11 -13
  57. package/src/scenarios/agent-platform-aggregate-evidence.test.ts +5 -4
  58. package/src/scenarios/agent-platform-profile.test.ts +18 -19
  59. package/src/scenarios/agent-requires-capabilities-shape.test.ts +4 -4
  60. package/src/scenarios/agent-roster-attribution.test.ts +18 -17
  61. package/src/scenarios/agent-roster-shape.test.ts +17 -19
  62. package/src/scenarios/agent-verifier-shape.test.ts +11 -11
  63. package/src/scenarios/agentConfidenceEscalation.test.ts +2 -1
  64. package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +6 -5
  65. package/src/scenarios/agentMemoryRedactionContract.test.ts +2 -1
  66. package/src/scenarios/agentMemoryRoundTrip.test.ts +2 -1
  67. package/src/scenarios/agentMemoryTtlExpiry.test.ts +4 -3
  68. package/src/scenarios/agentMessageReducer.test.ts +2 -1
  69. package/src/scenarios/agentMetadata.test.ts +2 -1
  70. package/src/scenarios/agentPackCatalog.test.ts +23 -21
  71. package/src/scenarios/agentPackExport.test.ts +4 -2
  72. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +5 -4
  73. package/src/scenarios/agentPackInstall.test.ts +4 -2
  74. package/src/scenarios/agentPackProvenance.test.ts +2 -1
  75. package/src/scenarios/agentReasoningEvents.test.ts +3 -2
  76. package/src/scenarios/agentReasoningStreaming.test.ts +11 -10
  77. package/src/scenarios/agents-run-tool-allowlist.test.ts +14 -12
  78. package/src/scenarios/ai-envelope-shape.test.ts +31 -29
  79. package/src/scenarios/aiEnvelope.capBreached.test.ts +26 -24
  80. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +36 -34
  81. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +17 -15
  82. package/src/scenarios/aiEnvelope.redaction.test.ts +22 -20
  83. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +18 -16
  84. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +38 -36
  85. package/src/scenarios/aiEnvelope.universalKinds.test.ts +31 -29
  86. package/src/scenarios/aiproviders-input-shape.test.ts +5 -5
  87. package/src/scenarios/aiproviders-realtimevoice-shape.test.ts +14 -16
  88. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +6 -4
  89. package/src/scenarios/aiproviders-selfhosted-shape.test.ts +15 -17
  90. package/src/scenarios/aiproviders-speechsynth-shape.test.ts +6 -8
  91. package/src/scenarios/aiproviders-subscription-scope.test.ts +7 -8
  92. package/src/scenarios/anonymous-actor-audit-opaque.test.ts +9 -8
  93. package/src/scenarios/anonymous-actor-default-deny.test.ts +10 -9
  94. package/src/scenarios/anonymous-actor-egress-guarded.test.ts +6 -5
  95. package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +7 -6
  96. package/src/scenarios/anonymous-actor-shape.test.ts +17 -19
  97. package/src/scenarios/anonymous-actor-write-gated.test.ts +9 -8
  98. package/src/scenarios/append-ordering.test.ts +7 -5
  99. package/src/scenarios/approval-gate-events.test.ts +4 -3
  100. package/src/scenarios/approval-gate-flow.test.ts +4 -3
  101. package/src/scenarios/approval-payload.test.ts +5 -4
  102. package/src/scenarios/artifact-auth.test.ts +7 -6
  103. package/src/scenarios/artifact-type-pack-install.test.ts +6 -6
  104. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +9 -8
  105. package/src/scenarios/artifact-type-registration-source.test.ts +20 -20
  106. package/src/scenarios/artifact-type-store-emission.test.ts +15 -18
  107. package/src/scenarios/artifact-type-store-without-render.test.ts +4 -4
  108. package/src/scenarios/audit-log-integrity.test.ts +8 -6
  109. package/src/scenarios/auth-api-key-rotation.test.ts +11 -9
  110. package/src/scenarios/auth-mtls.test.ts +22 -20
  111. package/src/scenarios/auth-oauth2-client-credentials.test.ts +16 -14
  112. package/src/scenarios/auth-oidc-user-bearer.test.ts +26 -24
  113. package/src/scenarios/auth-saml-profile.test.ts +14 -12
  114. package/src/scenarios/auth-scim-profile.test.ts +3 -2
  115. package/src/scenarios/auth-subject-link-key-class.test.ts +8 -7
  116. package/src/scenarios/auth-subject-link.test.ts +11 -10
  117. package/src/scenarios/auth.test.ts +5 -4
  118. package/src/scenarios/authorization-fail-closed.test.ts +3 -2
  119. package/src/scenarios/authorization-roles-shape.test.ts +9 -7
  120. package/src/scenarios/blob-cross-tenant-isolation.test.ts +8 -6
  121. package/src/scenarios/blob-presign-expiry.test.ts +12 -10
  122. package/src/scenarios/blob-roundtrip.test.ts +0 -0
  123. package/src/scenarios/budget-enforcement.test.ts +15 -14
  124. package/src/scenarios/budget-policy-shape.test.ts +18 -19
  125. package/src/scenarios/bulk-cancel.test.ts +9 -8
  126. package/src/scenarios/byok-auth-modes.test.ts +13 -14
  127. package/src/scenarios/byok-roundtrip.test.ts +12 -10
  128. package/src/scenarios/cache-cross-tenant-isolation.test.ts +8 -6
  129. package/src/scenarios/cache-ttl-expiry.test.ts +7 -5
  130. package/src/scenarios/callai-multimodal.test.ts +5 -3
  131. package/src/scenarios/cancellation.test.ts +6 -5
  132. package/src/scenarios/cap-breach.test.ts +15 -14
  133. package/src/scenarios/capability-arm-witness.test.ts +5 -3
  134. package/src/scenarios/certification-bundle-non-vacuous.test.ts +20 -19
  135. package/src/scenarios/certification-bundle-redaction.test.ts +11 -10
  136. package/src/scenarios/certification-bundle-v2.test.ts +11 -10
  137. package/src/scenarios/chain-compensation-expansion.test.ts +14 -13
  138. package/src/scenarios/chain-produced-var-roundtrip.test.ts +13 -14
  139. package/src/scenarios/chain-subchain-cycle-rejected.test.ts +9 -10
  140. package/src/scenarios/chain-subchain-fanout.test.ts +8 -9
  141. package/src/scenarios/chain-subchain-sibling.test.ts +14 -16
  142. package/src/scenarios/channel-presence-behavioral.test.ts +9 -7
  143. package/src/scenarios/channel-presence-shape.test.ts +11 -12
  144. package/src/scenarios/channel-ttl.test.ts +6 -5
  145. package/src/scenarios/chat-card-pack-execution.test.ts +4 -4
  146. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +9 -8
  147. package/src/scenarios/commitment-fired.test.ts +6 -5
  148. package/src/scenarios/compensation-behavior.test.ts +24 -23
  149. package/src/scenarios/compensation-recovery.test.ts +28 -27
  150. package/src/scenarios/configurable-schema.test.ts +8 -6
  151. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -5
  152. package/src/scenarios/conformance-execution-witness.test.ts +9 -8
  153. package/src/scenarios/connection-pack-apihosts.test.ts +19 -17
  154. package/src/scenarios/connection-pack-manifest-valid.test.ts +12 -11
  155. package/src/scenarios/connection-pack-no-credential-material.test.ts +8 -6
  156. package/src/scenarios/connection-pack-reach-exclusive.test.ts +5 -4
  157. package/src/scenarios/connection-pack-write-reconsent.test.ts +6 -4
  158. package/src/scenarios/connection-provider-resolution.test.ts +18 -16
  159. package/src/scenarios/connector-manifest-validity.test.ts +8 -7
  160. package/src/scenarios/context-budget-transcript-bound.test.ts +17 -15
  161. package/src/scenarios/context-summarization-replay.test.ts +14 -12
  162. package/src/scenarios/contract-provenance.test.ts +18 -18
  163. package/src/scenarios/conversation-turn-model-provenance-shape.test.ts +14 -15
  164. package/src/scenarios/conversationCapabilityNegotiation.test.ts +2 -1
  165. package/src/scenarios/conversationLifecycle.test.ts +2 -1
  166. package/src/scenarios/conversationReplayDeterminism.test.ts +4 -2
  167. package/src/scenarios/conversationVsLegacySuspend.test.ts +2 -1
  168. package/src/scenarios/core-standard-profile.test.ts +8 -9
  169. package/src/scenarios/cost-attribution.test.ts +23 -21
  170. package/src/scenarios/credential-payload-redaction.test.ts +4 -3
  171. package/src/scenarios/credentials-capability-shape.test.ts +5 -4
  172. package/src/scenarios/cross-engine-append-behavior.test.ts +11 -10
  173. package/src/scenarios/cross-engine-append-ordering.test.ts +4 -3
  174. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +11 -10
  175. package/src/scenarios/cross-host-causation-shape.test.ts +7 -6
  176. package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
  177. package/src/scenarios/data-residency-admission.test.ts +15 -14
  178. package/src/scenarios/deadletter-capability-shape.test.ts +6 -4
  179. package/src/scenarios/deadletter-retry-exhaustion.test.ts +4 -3
  180. package/src/scenarios/debug-bundle-truncation.test.ts +9 -7
  181. package/src/scenarios/debugBundle.test.ts +25 -23
  182. package/src/scenarios/discovery-families-view.test.ts +5 -4
  183. package/src/scenarios/discovery.test.ts +30 -29
  184. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +10 -8
  185. package/src/scenarios/dispatch-fanout-parallel.test.ts +24 -22
  186. package/src/scenarios/dispatch-input-mapping.test.ts +12 -10
  187. package/src/scenarios/dispatch-output-mapping.test.ts +9 -7
  188. package/src/scenarios/dispatch-per-item-input.test.ts +22 -20
  189. package/src/scenarios/dispatchLoop.test.ts +3 -2
  190. package/src/scenarios/distillation-index-roundtrip.test.ts +3 -3
  191. package/src/scenarios/distillation-secret-carryforward.test.ts +2 -2
  192. package/src/scenarios/distillation-shape.test.ts +4 -4
  193. package/src/scenarios/distillation-stable-archive.test.ts +6 -6
  194. package/src/scenarios/distillation-token-budget.test.ts +5 -5
  195. package/src/scenarios/durability-poison-exhaustion.test.ts +7 -6
  196. package/src/scenarios/egress-audience-binding.test.ts +11 -10
  197. package/src/scenarios/egress-decision-content-free.test.ts +6 -5
  198. package/src/scenarios/egress-provenance-shape.test.ts +17 -18
  199. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +27 -25
  200. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +19 -17
  201. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +28 -26
  202. package/src/scenarios/envelope-reasoning-shape.test.ts +19 -17
  203. package/src/scenarios/envelope-recovery-applied.test.ts +24 -22
  204. package/src/scenarios/envelope-refusal-shape.test.ts +31 -29
  205. package/src/scenarios/envelope-rendering-hint.test.ts +5 -4
  206. package/src/scenarios/envelope-retry-attempted.test.ts +33 -31
  207. package/src/scenarios/envelope-retry-exhausted.test.ts +22 -20
  208. package/src/scenarios/envelope-tier-one-subset-static.test.ts +9 -7
  209. package/src/scenarios/envelope-truncated.test.ts +19 -17
  210. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +19 -17
  211. package/src/scenarios/envelope-variant-discriminator-static.test.ts +4 -3
  212. package/src/scenarios/errors.test.ts +20 -19
  213. package/src/scenarios/eventOrdering.test.ts +16 -14
  214. package/src/scenarios/exec-not-protocol-tier.test.ts +5 -7
  215. package/src/scenarios/experimental-tier-shape.test.ts +17 -15
  216. package/src/scenarios/export-bundle-portability.test.ts +17 -17
  217. package/src/scenarios/failure-path.test.ts +7 -6
  218. package/src/scenarios/feedback-capability-shape.test.ts +4 -3
  219. package/src/scenarios/feedback-correction-redaction.test.ts +2 -1
  220. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +2 -1
  221. package/src/scenarios/feedback-fork-not-copied.test.ts +3 -2
  222. package/src/scenarios/feedback-on-terminal-run.test.ts +3 -2
  223. package/src/scenarios/feedback-record-and-list.test.ts +2 -1
  224. package/src/scenarios/feedback-unsupported-501.test.ts +2 -1
  225. package/src/scenarios/fixtures-gating.test.ts +29 -28
  226. package/src/scenarios/fixtures-valid.test.ts +16 -15
  227. package/src/scenarios/form-content-instantiation.test.ts +15 -14
  228. package/src/scenarios/frontend-plugin-packs.test.ts +29 -27
  229. package/src/scenarios/fs-path-traversal.test.ts +14 -12
  230. package/src/scenarios/goal-standing-continuation.test.ts +21 -21
  231. package/src/scenarios/grpc-transport.test.ts +9 -7
  232. package/src/scenarios/heartbeat-capability-shape.test.ts +3 -3
  233. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +2 -2
  234. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +3 -3
  235. package/src/scenarios/heartbeat-runtime-bound.test.ts +2 -2
  236. package/src/scenarios/highConcurrency.test.ts +14 -13
  237. package/src/scenarios/host-callback-declaration.test.ts +8 -7
  238. package/src/scenarios/http-client-ssrf.test.ts +7 -5
  239. package/src/scenarios/i18n-negotiation.test.ts +14 -12
  240. package/src/scenarios/idempotency-concurrent-claim.test.ts +14 -13
  241. package/src/scenarios/idempotency-key-determinism.test.ts +20 -18
  242. package/src/scenarios/idempotency.test.ts +8 -7
  243. package/src/scenarios/idempotencyRetry.test.ts +13 -11
  244. package/src/scenarios/identity-owner-shape.test.ts +6 -5
  245. package/src/scenarios/identity-passthrough.test.ts +4 -3
  246. package/src/scenarios/interrupt-approval.test.ts +6 -5
  247. package/src/scenarios/interrupt-approver-routing.test.ts +15 -15
  248. package/src/scenarios/interrupt-auth-required-resume.test.ts +5 -3
  249. package/src/scenarios/interrupt-clarification.test.ts +4 -3
  250. package/src/scenarios/interrupt-external-event-correlation.test.ts +6 -5
  251. package/src/scenarios/interrupt-parent-child-cascade.test.ts +4 -3
  252. package/src/scenarios/interrupt-quorum-resolution.test.ts +5 -4
  253. package/src/scenarios/interrupt-token-matrix.test.ts +8 -6
  254. package/src/scenarios/interruptRace.test.ts +12 -10
  255. package/src/scenarios/kv-atomic-increment.test.ts +11 -9
  256. package/src/scenarios/kv-cas.test.ts +14 -12
  257. package/src/scenarios/kv-cross-tenant-isolation.test.ts +12 -10
  258. package/src/scenarios/kv-ttl-expiry.test.ts +7 -5
  259. package/src/scenarios/localized-content-delivery.test.ts +24 -26
  260. package/src/scenarios/maliciousManifest.test.ts +10 -8
  261. package/src/scenarios/mcp-2026-07-28-discover.test.ts +15 -14
  262. package/src/scenarios/mcp-cache-tenant-scope.test.ts +6 -5
  263. package/src/scenarios/mcp-current-auth-boundary.test.ts +7 -6
  264. package/src/scenarios/mcp-discoverability.test.ts +9 -7
  265. package/src/scenarios/mcp-extension-opacity.test.ts +7 -6
  266. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +18 -17
  267. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +13 -12
  268. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +11 -10
  269. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +11 -10
  270. package/src/scenarios/mcp-server-sampling-bridge.test.ts +12 -11
  271. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +14 -13
  272. package/src/scenarios/mcp-server-untrusted-args.test.ts +14 -13
  273. package/src/scenarios/mcp-stateless-request.test.ts +11 -10
  274. package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
  275. package/src/scenarios/mcp-toolcall-redaction.test.ts +4 -3
  276. package/src/scenarios/mcp-version-negotiation.test.ts +15 -14
  277. package/src/scenarios/media-url-inline-cap.test.ts +21 -19
  278. package/src/scenarios/memory-attribution-emits-on-write.test.ts +5 -5
  279. package/src/scenarios/memory-attribution-no-content.test.ts +4 -4
  280. package/src/scenarios/memory-attribution-replay-stable.test.ts +4 -3
  281. package/src/scenarios/memory-attribution-shape.test.ts +4 -3
  282. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +3 -3
  283. package/src/scenarios/memory-capability-model-shape.test.ts +14 -15
  284. package/src/scenarios/memory-compaction-event-emitted.test.ts +7 -6
  285. package/src/scenarios/memory-compaction-provenance-tag.test.ts +3 -2
  286. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +7 -6
  287. package/src/scenarios/memory-consolidation-idempotent.test.ts +5 -4
  288. package/src/scenarios/memory-consolidation-shape.test.ts +9 -11
  289. package/src/scenarios/memory-degraded-projection.test.ts +10 -9
  290. package/src/scenarios/memory-injection-budget.test.ts +15 -14
  291. package/src/scenarios/metric-emission.test.ts +4 -3
  292. package/src/scenarios/model-capability-insufficient.test.ts +19 -17
  293. package/src/scenarios/model-capability-substituted.test.ts +20 -18
  294. package/src/scenarios/multi-agent-confidence-escalation.test.ts +12 -11
  295. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +10 -9
  296. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +11 -10
  297. package/src/scenarios/multi-node-ordering.test.ts +4 -3
  298. package/src/scenarios/multi-party-conversation-behavioral.test.ts +6 -6
  299. package/src/scenarios/multi-party-conversation-shape.test.ts +21 -23
  300. package/src/scenarios/multi-region-idempotency-behavior.test.ts +16 -15
  301. package/src/scenarios/multi-region-idempotency.test.ts +6 -5
  302. package/src/scenarios/node-failed-payload-shape.test.ts +5 -4
  303. package/src/scenarios/node-module-required-capabilities-shape.test.ts +10 -8
  304. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +5 -4
  305. package/src/scenarios/oauth-capability-shape.test.ts +5 -4
  306. package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
  307. package/src/scenarios/openapi-resolved-paths.test.ts +6 -5
  308. package/src/scenarios/orchestratorConservativePath.test.ts +2 -1
  309. package/src/scenarios/orchestratorDispatch.test.ts +2 -1
  310. package/src/scenarios/orchestratorTermination.test.ts +2 -1
  311. package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -2
  312. package/src/scenarios/otel-collector-canary-inspection.test.ts +8 -7
  313. package/src/scenarios/otel-emission-grpc.test.ts +3 -2
  314. package/src/scenarios/otel-emission.test.ts +6 -5
  315. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +7 -6
  316. package/src/scenarios/otel-trace-propagation.test.ts +2 -1
  317. package/src/scenarios/owner-subject-echo.test.ts +13 -12
  318. package/src/scenarios/owner-subject-shape.test.ts +11 -10
  319. package/src/scenarios/pack-manifest-extension-opacity.test.ts +8 -8
  320. package/src/scenarios/pack-registry-isolation.test.ts +4 -3
  321. package/src/scenarios/pack-registry-publish.test.ts +65 -63
  322. package/src/scenarios/pack-registry.test.ts +38 -36
  323. package/src/scenarios/pause-resume.test.ts +18 -16
  324. package/src/scenarios/policies.test.ts +20 -18
  325. package/src/scenarios/production-backpressure.test.ts +18 -16
  326. package/src/scenarios/production-retention-expiry.test.ts +8 -6
  327. package/src/scenarios/profile-discovery-core-alias.test.ts +13 -11
  328. package/src/scenarios/profileDerivation.test.ts +34 -33
  329. package/src/scenarios/prompt-all-four-kinds-events.test.ts +11 -9
  330. package/src/scenarios/prompt-composed-secret-redaction.test.ts +11 -9
  331. package/src/scenarios/prompt-composed-trust-marker.test.ts +10 -8
  332. package/src/scenarios/prompt-end-to-end-events.test.ts +17 -15
  333. package/src/scenarios/prompt-list-and-fetch.test.ts +16 -14
  334. package/src/scenarios/prompt-mutable-lifecycle.test.ts +13 -11
  335. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +6 -4
  336. package/src/scenarios/prompt-pack-install.test.ts +14 -12
  337. package/src/scenarios/prompt-prefix-cache.test.ts +19 -17
  338. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +11 -9
  339. package/src/scenarios/prompt-render-deterministic.test.ts +19 -17
  340. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +7 -5
  341. package/src/scenarios/prompt-resolution-chain-event.test.ts +12 -10
  342. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +10 -8
  343. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +8 -6
  344. package/src/scenarios/prompt-template-shape.test.ts +30 -28
  345. package/src/scenarios/proposal-reviewable-learning.test.ts +19 -19
  346. package/src/scenarios/protocol-versions-array.test.ts +10 -9
  347. package/src/scenarios/provider-usage.test.ts +17 -15
  348. package/src/scenarios/providerPolicyEnforcement.test.ts +16 -14
  349. package/src/scenarios/purpose-propagation.test.ts +23 -21
  350. package/src/scenarios/queue-ack-nack-dlq.test.ts +12 -10
  351. package/src/scenarios/queue-cross-tenant-isolation.test.ts +8 -6
  352. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +10 -8
  353. package/src/scenarios/rate-limit-envelope.test.ts +6 -5
  354. package/src/scenarios/redaction.test.ts +13 -11
  355. package/src/scenarios/redactionAdversarial.test.ts +11 -9
  356. package/src/scenarios/registry-public.test.ts +12 -11
  357. package/src/scenarios/replay-divergence-at-refusal.test.ts +16 -15
  358. package/src/scenarios/replay-fanout-suppression.test.ts +16 -15
  359. package/src/scenarios/replay-fork-arbitrary.test.ts +24 -23
  360. package/src/scenarios/replay-fork.test.ts +20 -19
  361. package/src/scenarios/replay-llm-cache-key-portable.test.ts +11 -9
  362. package/src/scenarios/replay-llm-cache-key.test.ts +22 -21
  363. package/src/scenarios/replay-observable-sequence-determinism.test.ts +4 -3
  364. package/src/scenarios/replay-retention-expiry.test.ts +11 -9
  365. package/src/scenarios/replay-side-effect-suppression.test.ts +21 -19
  366. package/src/scenarios/replayDeterminism.test.ts +13 -12
  367. package/src/scenarios/requirement-ledger.test.ts +15 -14
  368. package/src/scenarios/restart-during-run.test.ts +3 -2
  369. package/src/scenarios/route-coverage.test.ts +20 -19
  370. package/src/scenarios/run-diff.test.ts +22 -20
  371. package/src/scenarios/run-execution-bounds-shape.test.ts +14 -12
  372. package/src/scenarios/run-transport-economy.test.ts +13 -11
  373. package/src/scenarios/runner-ledger.test.ts +17 -16
  374. package/src/scenarios/runs-lifecycle.test.ts +7 -6
  375. package/src/scenarios/runtime-capabilities.test.ts +12 -10
  376. package/src/scenarios/runtime-requires-install-gate.test.ts +8 -8
  377. package/src/scenarios/runtime-requires-shape.test.ts +7 -6
  378. package/src/scenarios/safefetch-behavior.test.ts +22 -21
  379. package/src/scenarios/safefetch-live-audit.test.ts +14 -13
  380. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  381. package/src/scenarios/sandbox-mvp-behavior.test.ts +24 -23
  382. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +3 -2
  383. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  384. package/src/scenarios/sandbox-wasm-isolation.test.ts +14 -15
  385. package/src/scenarios/sandbox-wasm-timeout.test.ts +5 -6
  386. package/src/scenarios/scheduling-capability-shape.test.ts +8 -6
  387. package/src/scenarios/scheduling-cron-fires-once.test.ts +8 -6
  388. package/src/scenarios/search-bm25-roundtrip.test.ts +8 -6
  389. package/src/scenarios/secret-leakage-otel-attribute.test.ts +22 -20
  390. package/src/scenarios/self-hosted-runner.test.ts +26 -24
  391. package/src/scenarios/semantic-digest-vectors.test.ts +8 -7
  392. package/src/scenarios/speech-synthesis-roundtrip.test.ts +9 -7
  393. package/src/scenarios/speech-synthesis-unadvertised.test.ts +5 -3
  394. package/src/scenarios/sql-injection-rejection.test.ts +8 -6
  395. package/src/scenarios/sql-transaction-atomicity.test.ts +10 -8
  396. package/src/scenarios/staleClaim.test.ts +5 -4
  397. package/src/scenarios/stream-modes-buffer.test.ts +8 -7
  398. package/src/scenarios/stream-modes-mixed.test.ts +10 -9
  399. package/src/scenarios/stream-modes.test.ts +12 -11
  400. package/src/scenarios/stream-subscribe-from-beginning.test.ts +11 -9
  401. package/src/scenarios/stream-text-fixture.test.ts +14 -13
  402. package/src/scenarios/streamReconnect.test.ts +13 -11
  403. package/src/scenarios/strict-behavior-gate.test.ts +9 -8
  404. package/src/scenarios/subrun-approval-fail-closed.test.ts +2 -2
  405. package/src/scenarios/subrun-approval-gate.test.ts +3 -3
  406. package/src/scenarios/subrun-attestation-shape.test.ts +2 -2
  407. package/src/scenarios/subrun-checksum-stable.test.ts +4 -4
  408. package/src/scenarios/subworkflow-input-mapping.test.ts +16 -14
  409. package/src/scenarios/subworkflow.test.ts +10 -9
  410. package/src/scenarios/table-cross-tenant-isolation.test.ts +8 -6
  411. package/src/scenarios/table-cursor-pagination.test.ts +8 -6
  412. package/src/scenarios/table-schema-enforcement.test.ts +7 -5
  413. package/src/scenarios/test-seam-unauthenticated.test.ts +2 -2
  414. package/src/scenarios/tool-catalog-compact-projection.test.ts +10 -8
  415. package/src/scenarios/tool-catalog-projection.test.ts +11 -9
  416. package/src/scenarios/tool-descriptor-shape.test.ts +14 -15
  417. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -3
  418. package/src/scenarios/tool-hooks-content-free.test.ts +4 -4
  419. package/src/scenarios/tool-hooks-failure-honesty.test.ts +4 -4
  420. package/src/scenarios/tool-hooks-rate-limit.test.ts +2 -2
  421. package/src/scenarios/tool-hooks-secret-redaction.test.ts +2 -2
  422. package/src/scenarios/tool-hooks-shape.test.ts +3 -3
  423. package/src/scenarios/tool-session-lifecycle.test.ts +14 -13
  424. package/src/scenarios/trigger-bridge-delivery.test.ts +14 -14
  425. package/src/scenarios/trigger-bridge-shape.test.ts +19 -20
  426. package/src/scenarios/trigger-ingestion.test.ts +19 -17
  427. package/src/scenarios/trigger-stream-cdc-sources.test.ts +16 -14
  428. package/src/scenarios/v2-approver-enforced.test.ts +97 -0
  429. package/src/scenarios/v2-assurance-downgrade-audited.test.ts +75 -0
  430. package/src/scenarios/v2-bundle-v3-signed.test.ts +96 -0
  431. package/src/scenarios/v2-capabilities-root-closed.test.ts +63 -0
  432. package/src/scenarios/v2-capability-record-shape.test.ts +104 -0
  433. package/src/scenarios/v2-chain-pin-exact.test.ts +118 -0
  434. package/src/scenarios/v2-coherence-not-in-bundle.test.ts +98 -0
  435. package/src/scenarios/v2-compensation-read-projection.test.ts +85 -0
  436. package/src/scenarios/v2-configurable-closed.test.ts +67 -0
  437. package/src/scenarios/v2-dual-stack-negotiation.test.ts +118 -0
  438. package/src/scenarios/v2-effect-identity-business-key.test.ts +132 -0
  439. package/src/scenarios/v2-effect-seam-manifest.test.ts +163 -0
  440. package/src/scenarios/v2-enum-growth-rule.test.ts +69 -0
  441. package/src/scenarios/v2-era-key.test.ts +86 -0
  442. package/src/scenarios/v2-error-registry.test.ts +89 -0
  443. package/src/scenarios/v2-event-type-closed.test.ts +97 -0
  444. package/src/scenarios/v2-fork-a-v1-run.test.ts +109 -0
  445. package/src/scenarios/v2-form-when-reuses-edge-conditions.test.ts +70 -0
  446. package/src/scenarios/v2-header-scheme.test.ts +67 -0
  447. package/src/scenarios/v2-id-grammar.test.ts +77 -0
  448. package/src/scenarios/v2-idempotency-key-grammar.test.ts +65 -0
  449. package/src/scenarios/v2-interrupt-token-scheme.test.ts +55 -0
  450. package/src/scenarios/v2-lane-issuer-advertised.test.ts +62 -0
  451. package/src/scenarios/v2-legacy-profiles-absent.test.ts +78 -0
  452. package/src/scenarios/v2-manifest-ceiling-refused.test.ts +130 -0
  453. package/src/scenarios/v2-manifest-hatch-carried.test.ts +120 -0
  454. package/src/scenarios/v2-min-client-version.test.ts +48 -0
  455. package/src/scenarios/v2-minimum-version-refused.test.ts +141 -0
  456. package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +95 -0
  457. package/src/scenarios/v2-negotiation-authenticated.test.ts +125 -0
  458. package/src/scenarios/v2-negotiation-decided-emitted.test.ts +149 -0
  459. package/src/scenarios/v2-no-transport-advertisement.test.ts +57 -0
  460. package/src/scenarios/v2-owner-subject-required.test.ts +72 -0
  461. package/src/scenarios/v2-pack-isolation.test.ts +147 -0
  462. package/src/scenarios/v2-payload-registry-closed.test.ts +110 -0
  463. package/src/scenarios/v2-peer-dependency-declared.test.ts +132 -0
  464. package/src/scenarios/v2-pinned-run-disposition.test.ts +124 -0
  465. package/src/scenarios/v2-poll-cursor-v2.test.ts +114 -0
  466. package/src/scenarios/v2-preferred-version-default.test.ts +83 -0
  467. package/src/scenarios/v2-profiles-derived-only.test.ts +70 -0
  468. package/src/scenarios/v2-provider-conflict.test.ts +115 -0
  469. package/src/scenarios/v2-refresh-sla.test.ts +78 -0
  470. package/src/scenarios/v2-relaxation-recorded.test.ts +134 -0
  471. package/src/scenarios/v2-revocation-honored.test.ts +66 -0
  472. package/src/scenarios/v2-subject-link-record.test.ts +102 -0
  473. package/src/scenarios/v2-unmapped-type-refused.test.ts +95 -0
  474. package/src/scenarios/v2-v1-events-translated.test.ts +105 -0
  475. package/src/scenarios/v2-v1-signed-webhook-accepted.test.ts +109 -0
  476. package/src/scenarios/v2-webhook-durable-delivery.test.ts +238 -0
  477. package/src/scenarios/v2-well-known-one-resource.test.ts +129 -0
  478. package/src/scenarios/vector-knn-roundtrip.test.ts +9 -7
  479. package/src/scenarios/verifier-gating.test.ts +8 -6
  480. package/src/scenarios/version-fold.test.ts +9 -8
  481. package/src/scenarios/version-negotiation.test.ts +12 -11
  482. package/src/scenarios/voice-bargein-no-partial-leak.test.ts +8 -6
  483. package/src/scenarios/voice-event-payloads-shape.test.ts +10 -11
  484. package/src/scenarios/voice-interim-not-durable.test.ts +7 -5
  485. package/src/scenarios/voice-streamref-tenant-bound.test.ts +5 -3
  486. package/src/scenarios/voice-synthesis-streaming.test.ts +11 -9
  487. package/src/scenarios/voice-transcription-streaming.test.ts +8 -6
  488. package/src/scenarios/voice-transcription-unadvertised.test.ts +5 -3
  489. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -2
  490. package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
  491. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
  492. package/src/scenarios/wasm-pack-load.test.ts +4 -3
  493. package/src/scenarios/wasm-pack-memory-cap.test.ts +5 -4
  494. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -1
  495. package/src/scenarios/webhook-negative.test.ts +11 -9
  496. package/src/scenarios/webhook-receiver-adversarial.test.ts +8 -7
  497. package/src/scenarios/webhook-sig-algorithm.test.ts +5 -3
  498. package/src/scenarios/webhook-signed-delivery.test.ts +16 -15
  499. package/src/scenarios/webhook-tenant-isolation.test.ts +16 -14
  500. package/src/scenarios/workflow-chain-deferred-parameters.test.ts +28 -30
  501. package/src/scenarios/workflow-chain-expansion.test.ts +21 -20
  502. package/src/scenarios/workflow-chain-host-expansion.test.ts +21 -20
  503. package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +14 -12
  504. package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +5 -4
  505. package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +9 -8
  506. package/src/scenarios/workflow-primary-output-annotation.test.ts +7 -6
  507. package/src/scenarios/workflow-variable-format.test.ts +24 -24
  508. package/src/scenarios/workload-identity-behavior.test.ts +15 -13
  509. package/src/scenarios/workload-identity-chain-bounds.test.ts +18 -17
  510. package/src/scenarios/workspace-behavior.test.ts +19 -18
  511. package/src/scenarios/workspace-capability-shape.test.ts +3 -2
  512. package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +6 -5
  513. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +7 -6
  514. package/src/scenarios/x-openwop-form-pack-manifest.test.ts +8 -7
  515. package/src/setup.ts +44 -6
  516. package/vitest.config.ts +5 -1
  517. package/api/.redocly.lint-ignore.yaml +0 -22
  518. package/api/asyncapi.yaml +0 -834
  519. package/api/grpc/openwop.proto +0 -251
  520. package/api/openapi.yaml +0 -2693
  521. package/api/redocly.yaml +0 -23
  522. package/schemas/README.md +0 -144
  523. package/schemas/a2a-task-state.schema.json +0 -78
  524. package/schemas/a2ui-surface-delta-frame.schema.json +0 -48
  525. package/schemas/agent-deployment-transition.schema.json +0 -49
  526. package/schemas/agent-deployment.schema.json +0 -54
  527. package/schemas/agent-eval-suite.schema.json +0 -140
  528. package/schemas/agent-inventory-response.schema.json +0 -115
  529. package/schemas/agent-manifest.schema.json +0 -156
  530. package/schemas/agent-org-chart.schema.json +0 -82
  531. package/schemas/agent-ref.schema.json +0 -63
  532. package/schemas/agent-roster-entry.schema.json +0 -81
  533. package/schemas/agent-roster-response.schema.json +0 -21
  534. package/schemas/ai-envelope.schema.json +0 -134
  535. package/schemas/annotation-create.schema.json +0 -37
  536. package/schemas/annotation.schema.json +0 -56
  537. package/schemas/artifact-type-pack-manifest.schema.json +0 -244
  538. package/schemas/audit-verify-result.schema.json +0 -90
  539. package/schemas/budget-policy.schema.json +0 -18
  540. package/schemas/capabilities.schema.json +0 -4160
  541. package/schemas/certification-bundle-v2.schema.json +0 -118
  542. package/schemas/channel-presence-payload.schema.json +0 -41
  543. package/schemas/channel-written-payload.schema.json +0 -55
  544. package/schemas/chat-card-pack-manifest.schema.json +0 -253
  545. package/schemas/compact-tool-descriptor.schema.json +0 -51
  546. package/schemas/compensation-policy.schema.json +0 -64
  547. package/schemas/conformance-certification-bundle.schema.json +0 -86
  548. package/schemas/connection-pack-manifest.schema.json +0 -353
  549. package/schemas/conversation-event.schema.json +0 -168
  550. package/schemas/conversation-turn.schema.json +0 -117
  551. package/schemas/core-conformance-mock-agent-config.schema.json +0 -152
  552. package/schemas/credential-provenance.schema.json +0 -18
  553. package/schemas/credential-reference.schema.json +0 -21
  554. package/schemas/debug-bundle.schema.json +0 -196
  555. package/schemas/dispatch-config.schema.json +0 -112
  556. package/schemas/envelopes/clarification.request.schema.json +0 -52
  557. package/schemas/envelopes/error.schema.json +0 -30
  558. package/schemas/envelopes/media.audio.schema.json +0 -38
  559. package/schemas/envelopes/media.file.schema.json +0 -37
  560. package/schemas/envelopes/media.image.schema.json +0 -33
  561. package/schemas/envelopes/schema.request.schema.json +0 -26
  562. package/schemas/envelopes/schema.response.schema.json +0 -22
  563. package/schemas/envelopes/ui.a2ui-surface.schema.json +0 -154
  564. package/schemas/error-envelope.schema.json +0 -25
  565. package/schemas/eval-summary.schema.json +0 -92
  566. package/schemas/export-bundle.schema.json +0 -66
  567. package/schemas/form-content-pack-manifest.schema.json +0 -243
  568. package/schemas/frontend-plugin-manifest.schema.json +0 -109
  569. package/schemas/goal.schema.json +0 -104
  570. package/schemas/heartbeat-evaluated.schema.json +0 -14
  571. package/schemas/heartbeat-state-changed.schema.json +0 -14
  572. package/schemas/localized-content-language-settings.schema.json +0 -26
  573. package/schemas/localized-content-page-response.schema.json +0 -60
  574. package/schemas/localized-content-page.schema.json +0 -62
  575. package/schemas/localized-content-section.schema.json +0 -51
  576. package/schemas/memory-entry.schema.json +0 -36
  577. package/schemas/memory-list-options.schema.json +0 -37
  578. package/schemas/node-pack-manifest.schema.json +0 -654
  579. package/schemas/orchestrator-decision.schema.json +0 -90
  580. package/schemas/org-chart-responsibility-view.schema.json +0 -26
  581. package/schemas/pack-lockfile.schema.json +0 -108
  582. package/schemas/prompt-kind.schema.json +0 -8
  583. package/schemas/prompt-pack-manifest.schema.json +0 -117
  584. package/schemas/prompt-ref.schema.json +0 -40
  585. package/schemas/prompt-template.schema.json +0 -149
  586. package/schemas/proposal.schema.json +0 -84
  587. package/schemas/registry-version-manifest.schema.json +0 -208
  588. package/schemas/residency.schema.json +0 -16
  589. package/schemas/run-ancestry-response.schema.json +0 -54
  590. package/schemas/run-diff-response.schema.json +0 -64
  591. package/schemas/run-event-payloads.schema.json +0 -4452
  592. package/schemas/run-event.schema.json +0 -211
  593. package/schemas/run-options.schema.json +0 -80
  594. package/schemas/run-orchestrator-decided-event.schema.json +0 -20
  595. package/schemas/run-snapshot.schema.json +0 -148
  596. package/schemas/security-advisory.schema.json +0 -109
  597. package/schemas/self-hosted-runner-dispatch-frame.schema.json +0 -79
  598. package/schemas/self-hosted-runner-registration.schema.json +0 -53
  599. package/schemas/self-hosted-runner-result-frame.schema.json +0 -38
  600. package/schemas/subject.schema.json +0 -56
  601. package/schemas/suspend-request.schema.json +0 -207
  602. package/schemas/tool-descriptor.schema.json +0 -63
  603. package/schemas/trigger-event.schema.json +0 -222
  604. package/schemas/trigger-subscription-registration.schema.json +0 -67
  605. package/schemas/trigger-subscription.schema.json +0 -26
  606. package/schemas/ui-plugin-message.schema.json +0 -90
  607. package/schemas/workflow-chain-pack-manifest.schema.json +0 -615
  608. package/schemas/workflow-definition.schema.json +0 -850
  609. package/schemas/workload-identity.schema.json +0 -79
  610. package/schemas/workspace-file-create.schema.json +0 -20
  611. package/schemas/workspace-file.schema.json +0 -39
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Certification bundle v3 (RFC 0168 §E; schemas/v2/certification-bundle.schema.json).
3
+ *
4
+ * What v3 adds over v2 and why:
5
+ * - a closed root and `witnessSha256` over the reporter record (RFC 0148 §C
6
+ * asked for it; the v2 schema listed it optional and no emitter wrote it);
7
+ * - `assertionCount`, `host.build`, per-profile `evidenceTier`/`witnessCount`,
8
+ * `detail` (required when any result is not executed-pass);
9
+ * - an Ed25519 attestation over the canonical JSON of
10
+ * { witnessSha256, host.build, suite.version, discovery.sha256 } — the same
11
+ * commit rebuilt once scored 283/22 and 303/2 and nothing in the bundle could
12
+ * tell them apart; the build id plus the signature can;
13
+ * - `evidenceTier: independent` REQUIRES a verifier signature under a key id
14
+ * distinct from the host's (RFC 0148 R5: a malicious generator fabricates
15
+ * witnesses — a self-signed "independent" claim is refused, not warned).
16
+ *
17
+ * This module is schema-agnostic on purpose (the schema check is a separate step
18
+ * in cli.ts) and pure: it never reads the environment.
19
+ */
20
+ import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, type KeyObject } from 'node:crypto';
21
+
22
+ export type BundleV3Result = 'executed-pass' | 'executed-fail' | 'skipped' | 'inapplicable' | 'blocked';
23
+
24
+ export interface BundleV3Requirement {
25
+ readonly id: string;
26
+ readonly scenario: string;
27
+ readonly result: BundleV3Result;
28
+ readonly assertions?: number;
29
+ readonly detail?: string;
30
+ }
31
+ export interface BundleV3Profile {
32
+ readonly id: string;
33
+ readonly evidenceTier: 'self' | 'steward' | 'independent';
34
+ readonly witnessCount: number;
35
+ readonly certified: boolean;
36
+ }
37
+ export interface BundleV3Relaxation { readonly obligation: string; readonly durability: 'session' | 'deployment' | 'persisted'; readonly reason: string }
38
+ export interface BundleV3Signature { readonly alg: 'ed25519'; readonly keyId: string; readonly sig: string; readonly over: readonly string[]; readonly verifierKeyId?: string }
39
+ export interface BundleV3 {
40
+ bundleVersion: '3';
41
+ generatedAt: string;
42
+ suite: { name: '@openwop/openwop-conformance'; version: string; targetMajor: 1 | 2; specArtifactsVersion: string; stampSha256?: string };
43
+ host: { name: string; version: string; vendor?: string; build: { kind: 'image-digest' | 'commit' | 'artifact-sha256'; id: string }; signingKeyId?: string; relaxations?: BundleV3Relaxation[] };
44
+ discovery: { url: string; sha256: string; protocolVersions: string[]; preferredVersion: string };
45
+ claimedProfiles: BundleV3Profile[];
46
+ results: { totals: Record<'executedPass' | 'executedFail' | 'skipped' | 'inapplicable' | 'blocked', number>; requirements: BundleV3Requirement[] };
47
+ witnessSha256: string;
48
+ assertionCount: number;
49
+ detail?: { nonPass: { id: string; result: string; reason: string }[] };
50
+ signature: BundleV3Signature;
51
+ verifierSignature?: { alg: 'ed25519'; keyId: string; sig: string };
52
+ }
53
+
54
+ export const SIGNATURE_OVER = ['witnessSha256', 'host.build', 'suite.version', 'discovery.sha256'] as const;
55
+
56
+ /** Deterministic JSON: keys sorted at every level, no whitespace. */
57
+ export function canonicalJSON(value: unknown): string {
58
+ if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(',')}]`;
59
+ if (value !== null && typeof value === 'object') {
60
+ return `{${Object.keys(value as Record<string, unknown>).sort().map((k) => `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`).join(',')}}`;
61
+ }
62
+ return JSON.stringify(value);
63
+ }
64
+
65
+ /** RFC 0148 §C — the digest over the reporter record (the requirement rows). */
66
+ export function witnessDigest(rows: readonly BundleV3Requirement[]): string {
67
+ const canonicalRows = [...rows].sort((a, b) => a.id.localeCompare(b.id)).map((r) => ({ id: r.id, scenario: r.scenario, result: r.result, ...(r.assertions === undefined ? {} : { assertions: r.assertions }), ...(r.detail === undefined ? {} : { detail: r.detail }) }));
68
+ return createHash('sha256').update(canonicalJSON(canonicalRows), 'utf8').digest('hex');
69
+ }
70
+
71
+ /** The bytes the attestation covers. */
72
+ export function attestationPayload(b: Pick<BundleV3, 'witnessSha256' | 'host' | 'suite' | 'discovery'>): Buffer {
73
+ return Buffer.from(canonicalJSON({ witnessSha256: b.witnessSha256, 'host.build': b.host.build, 'suite.version': b.suite.version, 'discovery.sha256': b.discovery.sha256 }), 'utf8');
74
+ }
75
+
76
+ function toBase64url(buf: Buffer): string { return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
77
+ function fromBase64url(s: string): Buffer { return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); }
78
+
79
+ export function signBundleV3(bundle: Omit<BundleV3, 'signature'>, privateKeyPem: string, keyId: string): BundleV3Signature {
80
+ const key = createPrivateKey(privateKeyPem);
81
+ if (key.asymmetricKeyType !== 'ed25519') throw new Error(`bundle v3 signing key must be Ed25519, got ${String(key.asymmetricKeyType)}`);
82
+ const sig = edSign(null, attestationPayload(bundle), key);
83
+ return { alg: 'ed25519', keyId, sig: toBase64url(sig), over: [...SIGNATURE_OVER] };
84
+ }
85
+
86
+ export function verifierSign(bundle: Pick<BundleV3, 'witnessSha256' | 'host' | 'suite' | 'discovery'>, privateKeyPem: string, keyId: string): NonNullable<BundleV3['verifierSignature']> {
87
+ const key = createPrivateKey(privateKeyPem);
88
+ return { alg: 'ed25519', keyId, sig: toBase64url(edSign(null, attestationPayload(bundle), key)) };
89
+ }
90
+
91
+ export function publicKeyFromPrivate(privateKeyPem: string): string {
92
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' }) as string;
93
+ }
94
+
95
+ export interface VerifyV3Options {
96
+ /** The host's public key (PEM). Without it the signature's bytes are checked for shape only and the verdict says `signatureVerified: false`. */
97
+ readonly hostPublicKeyPem?: string;
98
+ /** The verifier's public key (PEM) — required to verify an `independent` claim. */
99
+ readonly verifierPublicKeyPem?: string;
100
+ }
101
+ export interface V3Rejection { readonly kind: string; readonly detail: string; readonly profile?: string }
102
+ export interface V3Verdict {
103
+ readonly rejections: V3Rejection[];
104
+ readonly signatureVerified: boolean;
105
+ readonly verifierSignatureVerified: boolean;
106
+ readonly certifiedProfiles: string[];
107
+ }
108
+
109
+ export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3Verdict {
110
+ const rejections: V3Rejection[] = [];
111
+ if (bundle.bundleVersion !== '3') rejections.push({ kind: 'not-v3', detail: `bundleVersion is ${JSON.stringify(bundle.bundleVersion)}, expected "3"` });
112
+ const rows = bundle.results?.requirements ?? [];
113
+ const seen = new Set<string>();
114
+ for (const r of rows) {
115
+ if (seen.has(r.id)) rejections.push({ kind: 'duplicate-requirement', detail: `${r.id}: more than one row (RFC 0148 §A)` });
116
+ seen.add(r.id);
117
+ if (r.result !== 'executed-pass' && !(r.detail && r.detail.trim())) rejections.push({ kind: 'reason-missing', detail: `${r.id}: '${r.result}' recorded without a reason` });
118
+ if (r.result === 'executed-pass' && !(r.assertions && r.assertions > 0)) rejections.push({ kind: 'vacuous-pass', detail: `${r.id}: executed-pass with no assertions is a witness of nothing (RFC 0148 §C)` });
119
+ }
120
+ const count = (d: BundleV3Result): number => rows.filter((r) => r.result === d).length;
121
+ const expected = { executedPass: count('executed-pass'), executedFail: count('executed-fail'), skipped: count('skipped'), inapplicable: count('inapplicable'), blocked: count('blocked') };
122
+ for (const k of Object.keys(expected) as (keyof typeof expected)[]) if (bundle.results?.totals?.[k] !== expected[k]) rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
123
+ const digest = witnessDigest(rows);
124
+ if (bundle.witnessSha256 !== digest) rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows (${digest.slice(0, 12)})` });
125
+ const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
126
+ if (bundle.assertionCount !== assertions) rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
127
+ const nonPass = rows.filter((r) => r.result !== 'executed-pass');
128
+ if (nonPass.length > 0 && !bundle.detail) rejections.push({ kind: 'detail-missing', detail: `${nonPass.length} non-pass row(s) and no detail block` });
129
+ if (!bundle.host?.build?.kind || !bundle.host?.build?.id) rejections.push({ kind: 'build-missing', detail: 'host.build.{kind,id} is required — a bundle attributes to a BUILD, not a commit' });
130
+
131
+ // Signature
132
+ let signatureVerified = false;
133
+ const sig = bundle.signature;
134
+ if (!sig || sig.alg !== 'ed25519' || !sig.keyId || !sig.sig) rejections.push({ kind: 'signature-missing', detail: 'signature.{alg: ed25519, keyId, sig} is required in v3' });
135
+ else if (JSON.stringify(sig.over) !== JSON.stringify(SIGNATURE_OVER)) rejections.push({ kind: 'signature-over', detail: `signature.over must be ${JSON.stringify(SIGNATURE_OVER)}` });
136
+ else if (opts.hostPublicKeyPem) {
137
+ const key: KeyObject = createPublicKey(opts.hostPublicKeyPem);
138
+ signatureVerified = edVerify(null, attestationPayload(bundle), key, fromBase64url(sig.sig));
139
+ if (!signatureVerified) rejections.push({ kind: 'signature-invalid', detail: 'the attestation does not verify under the host key' });
140
+ }
141
+ // Independent tier
142
+ let verifierSignatureVerified = false;
143
+ const claimsIndependent = (bundle.claimedProfiles ?? []).some((p) => p.evidenceTier === 'independent');
144
+ if (claimsIndependent) {
145
+ const vs = bundle.verifierSignature;
146
+ if (!vs || !vs.keyId || !vs.sig) rejections.push({ kind: 'independent-unsigned', detail: 'evidenceTier independent requires a verifierSignature (RFC 0168 §E.2)' });
147
+ else if (vs.keyId === sig?.keyId || vs.keyId === bundle.host?.signingKeyId) rejections.push({ kind: 'independent-self-signed', detail: 'the verifier key must be distinct from the host key (RFC 0148 R5)' });
148
+ else if (opts.verifierPublicKeyPem) {
149
+ verifierSignatureVerified = edVerify(null, attestationPayload(bundle), createPublicKey(opts.verifierPublicKeyPem), fromBase64url(vs.sig));
150
+ if (!verifierSignatureVerified) rejections.push({ kind: 'verifier-signature-invalid', detail: 'the verifier attestation does not verify' });
151
+ } else rejections.push({ kind: 'independent-unverifiable', detail: 'an independent claim needs the verifier public key to verify; refused, not assumed' });
152
+ }
153
+ // Relaxations: a relaxed obligation's profile cannot certify (RFC 0173 §A.2).
154
+ const relaxed = new Set((bundle.host?.relaxations ?? []).map((r) => r.obligation.split('.')[0]));
155
+ const certifiedProfiles: string[] = [];
156
+ for (const p of bundle.claimedProfiles ?? []) {
157
+ if (p.certified && relaxed.size > 0 && [...relaxed].some((o) => p.id.includes(o))) rejections.push({ kind: 'relaxed-profile-certified', profile: p.id, detail: `${p.id} is marked certified while a relaxation on ${[...relaxed].join(', ')} is recorded (RFC 0173 §A.2)` });
158
+ else if (p.certified) certifiedProfiles.push(p.id);
159
+ }
160
+ if (expected.blocked > 0 && certifiedProfiles.length > 0) rejections.push({ kind: 'blocked-certified', detail: `${expected.blocked} blocked row(s): a bundle with blocked > 0 does not certify (RFC 0168 §E.1)` });
161
+ // RFC 0168 §E.2: the verifier REFUSES, it does not warn — but a refusal is
162
+ // scoped to what it names. A rejection carrying a `profile` removes that
163
+ // profile only (a relaxation on one obligation does not poison an unrelated
164
+ // one, RFC 0173 §A.2); a rejection that names no profile is a statement about
165
+ // the bundle — a bad witness digest, an unverifiable signature, a self-signed
166
+ // `independent` claim, a blocked row — and nothing in it certifies.
167
+ const bundleWide = rejections.filter((r) => !('profile' in r) || !r.profile);
168
+ const scoped = new Set(rejections.map((r) => r.profile).filter((p): p is string => typeof p === 'string'));
169
+ return {
170
+ rejections,
171
+ signatureVerified,
172
+ verifierSignatureVerified,
173
+ certifiedProfiles: bundleWide.length > 0 ? [] : certifiedProfiles.filter((p) => !scoped.has(p)),
174
+ };
175
+ }
@@ -26,7 +26,8 @@
26
26
 
27
27
  import { createHash } from 'node:crypto';
28
28
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
29
- import { join, relative, sep } from 'node:path';
29
+ import { createRequire } from 'node:module';
30
+ import { join, relative, sep, dirname } from 'node:path';
30
31
 
31
32
  export interface CorpusStamp {
32
33
  readonly suiteVersion?: string;
@@ -73,6 +74,33 @@ export function digestVendoredFiles(root: string): Record<string, string> {
73
74
  * Compare the stamp's `files` map against the bytes on disk. Pure: reads,
74
75
  * never writes, never throws on a mismatch — the caller decides how loud.
75
76
  */
77
+ /**
78
+ * Suite 2.0.0 (RFC 0168 §D.2): the contract is the `@openwop/spec-artifacts` peer.
79
+ * The suite embeds, at pack time, `dist/spec-artifacts.lock.json` =
80
+ * { version, stampSha256 } for the peer it was built against; at start it
81
+ * resolves the installed peer, recomputes its stamp digest, and refuses to run
82
+ * on a version or digest mismatch (a floating peer makes a bundle
83
+ * irreproducible). The peer's own files are then verified against its stamp.
84
+ */
85
+ export function verifyPeerContract(pkgRoot: string): StampVerdict {
86
+ const lockPath = join(pkgRoot, 'dist', 'spec-artifacts.lock.json');
87
+ if (!existsSync(lockPath)) return { kind: 'not-applicable', reason: 'dist/spec-artifacts.lock.json is absent — a repo checkout, not a packed 2.x suite' };
88
+ let lock: { version: string; stampSha256: string };
89
+ try { lock = JSON.parse(readFileSync(lockPath, 'utf8')); } catch { return { kind: 'mismatch', missing: ['dist/spec-artifacts.lock.json'], altered: [], extra: [] }; }
90
+ let peerRoot: string;
91
+ try { peerRoot = dirname(createRequire(join(pkgRoot, 'package.json')).resolve('@openwop/spec-artifacts/package.json')); }
92
+ catch { return { kind: 'mismatch', missing: ['node_modules/@openwop/spec-artifacts (peer dependency not installed)'], altered: [], extra: [] }; }
93
+ const stampPath = join(peerRoot, 'CORPUS-STAMP.json');
94
+ if (!existsSync(stampPath)) return { kind: 'mismatch', missing: ['@openwop/spec-artifacts/CORPUS-STAMP.json'], altered: [], extra: [] };
95
+ const stamp = JSON.parse(readFileSync(stampPath, 'utf8')) as { package: string; version: string; files: Record<string, string> };
96
+ const digest = createHash('sha256').update(JSON.stringify({ package: stamp.package, version: stamp.version, files: stamp.files })).digest('hex');
97
+ if (stamp.version !== lock.version || digest !== lock.stampSha256) return { kind: 'mismatch', missing: [], altered: [`@openwop/spec-artifacts ${stamp.version} (digest ${digest.slice(0, 12)}) ≠ the suite's lock ${lock.version} (${lock.stampSha256.slice(0, 12)})`], extra: [] };
98
+ const missing: string[] = []; const altered: string[] = [];
99
+ for (const [rel, d] of Object.entries(stamp.files)) { const p = join(peerRoot, ...rel.split('/')); if (!existsSync(p)) missing.push(rel); else if (sha256File(p) !== d) altered.push(rel); }
100
+ if (missing.length || altered.length) return { kind: 'mismatch', missing, altered, extra: [] };
101
+ return { kind: 'verified', files: Object.keys(stamp.files).length };
102
+ }
103
+
76
104
  export function verifyCorpusStamp(root: string, layout: string): StampVerdict {
77
105
  if (layout !== 'published') {
78
106
  return { kind: 'not-applicable', reason: `layout is '${layout}' — the vendored copy exists only in the published package` };
package/src/lib/driver.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { loadEnv } from './env.js';
13
+ import { seamPath, targetMajor } from './seams.js';
13
14
 
14
15
  export interface OpenWOPResponse {
15
16
  readonly status: number;
@@ -37,10 +38,17 @@ class OpenWOPDriver {
37
38
  const env = loadEnv();
38
39
  // An absolute URL is used as-is (a host may advertise its MCP server mount or
39
40
  // an A2A endpoint on another origin); a path is joined to the base URL.
40
- const url = /^https?:\/\//i.test(path) ? path : `${env.baseUrl}${path}`;
41
+ // Suite 2.0.0: under target major 2 a v1 seam path is rewritten to its
42
+ // api/seams-v2.yaml address (RFC 0168 §C.2) and every request names the
43
+ // contract it speaks with `OpenWOP-Version` (RFC 0172 §A.3) unless the
44
+ // scenario set one itself (the negotiation scenarios do).
45
+ const major = targetMajor();
46
+ const effectivePath = major === 2 ? seamPath(path) : path;
47
+ const url = /^https?:\/\//i.test(effectivePath) ? effectivePath : `${env.baseUrl}${effectivePath}`;
41
48
 
42
49
  const headers: Record<string, string> = {
43
50
  Accept: 'application/json',
51
+ ...(major === 2 && !Object.keys(init.headers ?? {}).some((h) => h.toLowerCase() === 'openwop-version') ? { 'OpenWOP-Version': '2.0' } : {}),
44
52
  ...(init.headers ?? {}),
45
53
  };
46
54
  if (init.body !== undefined && headers['Content-Type'] === undefined) {
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Suite 2.0.0 — the era-`2` event-log seed for the RFC 0176 persisted-data
3
+ * scenarios (`v2-v1-events-translated`, `v2-unmapped-type-refused`,
4
+ * `v2-fork-a-v1-run`, `v2-pinned-run-disposition`).
5
+ *
6
+ * A v2 host reading a run written by a v1 host translates every event through
7
+ * `spec/v2/event-codemap.json` at the storage boundary (persistence.md §The
8
+ * reader rule). To witness that from outside, the suite needs a run whose
9
+ * persisted log is in v1 vocabulary — which only a host that predates the cut
10
+ * has, unless a seam writes one. The seam these scenarios drive (seams-profile
11
+ * gated, RFC 0168 §C):
12
+ *
13
+ * POST /conformance/seams/sample/event-log/seed
14
+ * { eventLogSchemaVersion: 2, status, events: [{ type, sequence, payload, timestamp?, causationId? }] }
15
+ * → 201 { runId }
16
+ *
17
+ * The host MUST persist the rows verbatim (v1 `type` strings, the given
18
+ * `sequence` space, no era stamp — the run reads as era `2` by the absent-⇒-`2`
19
+ * rule) and MUST NOT translate them at write time; the point of the seam is
20
+ * that the read projection does the work. `status` is the snapshot status the
21
+ * seeded run holds (`completed` for a finished log, `running` for a run the
22
+ * pinned-run disposition must decide). The seam is catalogued in
23
+ * `api/seams-v2.yaml` (`seedEra2EventLog`); a host answering `404` /
24
+ * `403` / `405` records `blocked` naming it (never a pass), and a host that
25
+ * answers anything but `201 { runId }` records `blocked` with the status.
26
+ *
27
+ * The scenarios name the seam by its v1-shaped path
28
+ * (`/v1/host/sample/event-log/seed`); under target major 2 the driver rewrites
29
+ * it to the address above (lib/seams.ts).
30
+ */
31
+
32
+ import { existsSync, readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { driver, type OpenWOPResponse } from './driver.js';
35
+ import { loadEnv } from './env.js';
36
+ import { seamPath, seamsProfileAdvertised } from './seams.js';
37
+ import { readErrorCode } from './error-envelope.js';
38
+ import { V1_DIR } from './paths.js';
39
+
40
+ export const SEED_PATH_V1 = '/v1/host/sample/event-log/seed';
41
+ export const SEED_PATH = seamPath(SEED_PATH_V1);
42
+
43
+ export interface SeedEvent {
44
+ readonly type: string;
45
+ readonly sequence: number;
46
+ readonly payload: Record<string, unknown>;
47
+ readonly timestamp?: string;
48
+ readonly causationId?: string;
49
+ }
50
+
51
+ export interface ReadEvent {
52
+ readonly type?: unknown;
53
+ readonly sequence?: unknown;
54
+ readonly payload?: unknown;
55
+ readonly eventId?: unknown;
56
+ readonly timestamp?: unknown;
57
+ readonly causationId?: unknown;
58
+ }
59
+
60
+ export type Seeded =
61
+ | { readonly ok: true; readonly runId: string }
62
+ | { readonly ok: false; readonly kind: 'blocked' | 'inapplicable' | 'skipped'; readonly reason: string };
63
+
64
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
65
+ try { return await fn(); } catch { return null; }
66
+ }
67
+
68
+ /** The gate every era-2 scenario applies before touching the seam. */
69
+ export function era2Gate(doc: Record<string, unknown> | null): Seeded | null {
70
+ if (!doc) return { ok: false, kind: 'blocked', reason: 'discovery unreachable' };
71
+ if (!seamsProfileAdvertised(doc)) {
72
+ return { ok: false, kind: 'blocked', reason: `seams profile not advertised (conformance.seamsProfile !== openwop-conformance-seams-v2) — an era-2 log can only be seeded through ${SEED_PATH}` };
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /** Seed an era-2 log in v1 vocabulary; the reason names the seam when it is absent. */
78
+ export async function seedEra2Log(events: readonly SeedEvent[], status: 'completed' | 'running' | 'paused' = 'completed'): Promise<Seeded> {
79
+ const res = await http(() => driver.post(SEED_PATH_V1, { eventLogSchemaVersion: 2, status, events }));
80
+ if (res === null) return { ok: false, kind: 'blocked', reason: `${SEED_PATH} unreachable (fetch failed)` };
81
+ if (res.status === 404 || res.status === 403 || res.status === 405) {
82
+ return { ok: false, kind: 'blocked', reason: `no seam seeds an era-2 event log — ${SEED_PATH} answered ${res.status} (uncatalogued in api/seams-v2.yaml; the reader rule is seam-gated, RFC 0176 falsifiability §A.3)` };
83
+ }
84
+ const runId = (res.json as { runId?: unknown } | undefined)?.runId;
85
+ if (res.status !== 201 || typeof runId !== 'string') {
86
+ return { ok: false, kind: 'blocked', reason: `${SEED_PATH} answered ${res.status} ${readErrorCode(res.json) ?? ''} without a 201 { runId } — the seed contract is not honoured`.trim() };
87
+ }
88
+ return { ok: true, runId };
89
+ }
90
+
91
+ /** `GET /runs/{runId}/events/poll` under OpenWOP-Version: 2.0 (the driver adds the header). */
92
+ export async function pollEvents(runId: string, query = ''): Promise<OpenWOPResponse | null> {
93
+ return http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1${query}`));
94
+ }
95
+
96
+ export function eventsOf(body: unknown): ReadEvent[] {
97
+ const events = (body as { events?: unknown } | undefined)?.events;
98
+ return Array.isArray(events) ? (events as ReadEvent[]) : [];
99
+ }
100
+
101
+ /**
102
+ * `GET /runs/{runId}/events?streamMode=debug` under OpenWOP-Version: 2.0 —
103
+ * `debug` admits every event (events.md §Stream modes), so a `version.pinned`
104
+ * or a vendor row is visible. lib/sse.ts sends no version header, and a
105
+ * header-less request selects the v1 representation (RFC 0172 §A.3), so the
106
+ * stream is read here with the header the scenario is speaking.
107
+ */
108
+ export async function streamEvents(runId: string, timeoutMs = 8_000): Promise<{ status: number; events: ReadEvent[] } | null> {
109
+ const env = loadEnv();
110
+ const ctl = new AbortController();
111
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
112
+ try {
113
+ const res = await fetch(`${env.baseUrl}/runs/${encodeURIComponent(runId)}/events?streamMode=debug`, {
114
+ headers: { Accept: 'text/event-stream', Authorization: `Bearer ${env.apiKey}`, 'OpenWOP-Version': '2.0' },
115
+ signal: ctl.signal,
116
+ });
117
+ if (res.status !== 200 || res.body === null) return { status: res.status, events: [] };
118
+ let text = '';
119
+ try { text = await res.text(); } catch (err) { if ((err as { name?: string }).name !== 'AbortError') throw err; }
120
+ const events: ReadEvent[] = [];
121
+ for (const frame of text.split(/\r?\n\r?\n/)) {
122
+ const data = frame.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).replace(/^ /, '')).join('\n');
123
+ if (data.length === 0) continue;
124
+ try {
125
+ const parsed = JSON.parse(data) as unknown;
126
+ for (const e of Array.isArray(parsed) ? parsed : [parsed]) if (e && typeof e === 'object') events.push(e as ReadEvent);
127
+ } catch { /* a keep-alive or a non-JSON frame */ }
128
+ }
129
+ return { status: res.status, events };
130
+ } catch (err) {
131
+ if ((err as { name?: string }).name === 'AbortError') return { status: 0, events: [] };
132
+ return null;
133
+ } finally {
134
+ clearTimeout(timer);
135
+ }
136
+ }
137
+
138
+ /** `POST /runs/{runId}:fork`; null when the request could not be made. */
139
+ export async function forkRun(runId: string, body: Record<string, unknown>): Promise<OpenWOPResponse | null> {
140
+ return http(() => driver.post(`/runs/${encodeURIComponent(runId)}:fork`, body));
141
+ }
142
+
143
+ /**
144
+ * The v1→v2 rows the seeds use, taken from `spec/v2/event-codemap.json` when the
145
+ * corpus is present (the published tarball ships the codemap beside the
146
+ * schemas; a layout without it falls back to the same rows spelled here, which
147
+ * is what the codemap says for them — `counts.renamed: 36`).
148
+ */
149
+ const FALLBACK: ReadonlyArray<readonly [string, string]> = [
150
+ ['run.started', 'run.started'],
151
+ ['run.completed', 'run.completed'],
152
+ ['run.cancelled', 'run.cancelled'],
153
+ ['version.pinned', 'version.pinned'],
154
+ ['agent.toolCalled', 'agent.tool-called'],
155
+ ['agent.toolReturned', 'agent.tool-returned'],
156
+ ['run.resuming', 'run.resume-started'],
157
+ ];
158
+
159
+ let codemap: Map<string, string> | undefined;
160
+ export function codemapV1toV2(): Map<string, string> {
161
+ if (codemap) return codemap;
162
+ const m = new Map<string, string>(FALLBACK);
163
+ const candidate = V1_DIR ? join(V1_DIR, '..', 'v2', 'event-codemap.json') : null;
164
+ if (candidate && existsSync(candidate)) {
165
+ try {
166
+ const doc = JSON.parse(readFileSync(candidate, 'utf8')) as { rows?: Array<{ v1?: unknown; v2?: unknown }> };
167
+ for (const r of doc.rows ?? []) if (typeof r.v1 === 'string' && typeof r.v2 === 'string') m.set(r.v1, r.v2);
168
+ } catch { /* the fallback rows stand */ }
169
+ }
170
+ codemap = m;
171
+ return m;
172
+ }
173
+
174
+ /** A minimal era-2 log in v1 vocabulary: two renamed rows between run.started and run.completed. */
175
+ export function v1FixtureLog(workflowId = 'conformance-noop'): SeedEvent[] {
176
+ const t0 = Date.parse('2026-01-15T10:00:00.000Z');
177
+ const ts = (i: number) => new Date(t0 + i * 1000).toISOString();
178
+ return [
179
+ { type: 'run.started', sequence: 0, payload: { workflowId }, timestamp: ts(0) },
180
+ { type: 'agent.toolCalled', sequence: 1, payload: { agentId: 'conformance-agent', toolName: 'echo', callId: 'call-0001' }, timestamp: ts(1) },
181
+ { type: 'agent.toolReturned', sequence: 2, payload: { agentId: 'conformance-agent', toolName: 'echo', callId: 'call-0001', status: 'ok' }, timestamp: ts(2) },
182
+ { type: 'run.completed', sequence: 3, payload: { durationMs: 3000 }, timestamp: ts(3) },
183
+ ];
184
+ }
185
+
186
+ /** The observable part of an event for RFC 0041 §C byte-equivalence: type, sequence, payload (clock fields and minted ids excluded). */
187
+ export function observable(e: ReadEvent): string {
188
+ return JSON.stringify({ type: e.type, sequence: e.sequence, payload: canonical(e.payload) });
189
+ }
190
+
191
+ function canonical(v: unknown): unknown {
192
+ if (Array.isArray(v)) return v.map(canonical);
193
+ if (v !== null && typeof v === 'object') {
194
+ return Object.fromEntries(Object.keys(v as Record<string, unknown>).sort().map((k) => [k, canonical((v as Record<string, unknown>)[k])]));
195
+ }
196
+ return v;
197
+ }
package/src/lib/paths.ts CHANGED
@@ -36,6 +36,7 @@
36
36
 
37
37
  import { existsSync } from 'node:fs';
38
38
  import { fileURLToPath } from 'node:url';
39
+ import { createRequire } from 'node:module';
39
40
  import { dirname, join, resolve as pathResolve } from 'node:path';
40
41
 
41
42
  // `dirname(fileURLToPath(import.meta.url))` for an ESM module compiled or
@@ -77,7 +78,22 @@ interface ResolvedLayout {
77
78
  readonly layout: 'env-override' | 'repo' | 'published';
78
79
  }
79
80
 
80
- function resolveFromRoot(root: string, layout: ResolvedLayout['layout']): ResolvedLayout {
81
+ /**
82
+ * Suite 2.0.0 (RFC 0168 §D.2): in the published layout the CONTRACT (api/ and
83
+ * schemas/) is the `@openwop/spec-artifacts` peer package, not files vendored
84
+ * into this tarball. Resolve its root through Node's resolver from this package
85
+ * so the host's installed peer is what the suite validates against.
86
+ */
87
+ function resolvePeerRoot(): string | null {
88
+ try {
89
+ const req = createRequire(join(PKG_ROOT, 'package.json'));
90
+ return dirname(req.resolve('@openwop/spec-artifacts/package.json'));
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contractRoot: string = root): ResolvedLayout {
81
97
  // Two on-disk shapes for the layout root:
82
98
  // - Repo: <root>/schemas, <root>/api, <root>/conformance/fixtures,
83
99
  // <root>/conformance/{fixtures.md,coverage.md}, <root>/spec/v1/*.md
@@ -86,8 +102,8 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout']): Resolv
86
102
  // <root>/fixtures.md (when bundled), no spec/v1.
87
103
  // Probe by checking whether `schemas/` lives at the conformance pkg root
88
104
  // (vendored) vs one level up (repo).
89
- const schemasDir = join(root, 'schemas');
90
- const apiDir = join(root, 'api');
105
+ const schemasDir = join(contractRoot, 'schemas');
106
+ const apiDir = join(contractRoot, 'api');
91
107
  const repoFixturesDir = join(root, 'conformance', 'fixtures');
92
108
  const vendoredFixturesDir = join(root, 'fixtures');
93
109
  const fixturesDir = existsSync(repoFixturesDir) ? repoFixturesDir : vendoredFixturesDir;
@@ -166,7 +182,13 @@ function resolveLayout(): ResolvedLayout {
166
182
  if (parentHasSchemas) {
167
183
  return resolveFromRoot(parent, 'repo');
168
184
  }
185
+ const peer = resolvePeerRoot();
186
+ if (peer) {
187
+ return resolveFromRoot(PKG_ROOT, 'published', peer);
188
+ }
169
189
  if (pkgHasSchemas) {
190
+ // A pre-2.0 tarball layout (schemas vendored in-package); kept so an old
191
+ // layout still resolves, but the 2.x stamp check refuses to run without the peer.
170
192
  return resolveFromRoot(PKG_ROOT, 'published');
171
193
  }
172
194
  // Neither — return the published-style resolution rooted at PKG_ROOT
@@ -24,6 +24,20 @@
24
24
  * `~3`, … (vitest allows duplicate titles; the ledger does not allow duplicate
25
25
  * ids). A scenario MAY override the derived id for the current test with
26
26
  * `req()` when it wants a hand-authored, registry-listed id.
27
+ *
28
+ * requireExplicitIds (suite 2.0.0, RFC 0168 §A.1): from 2.0.0 `req(id, section,
29
+ * requirement)` is the ONLY assertion-message form in `src/scenarios` and
30
+ * `src/coherence` — `driver.describe(section, requirement)` is gone from every
31
+ * scenario, and `scripts/check-req-only.mjs` (root) fails the gate when a
32
+ * scenario reintroduces it, when an `expect` carries a bare string message,
33
+ * when an `it` body returns without `softSkip` / `seamAbsent`, or when two
34
+ * `it`s in one file share an explicit id. Every `it` therefore names the
35
+ * requirement it witnesses on the wire; the title-derived allocator below is
36
+ * the fallback for an `it` that makes no cited assertion at all. The ids the
37
+ * 2.0.0 sweep wrote are exactly the ids the allocator derived in 1.x (same
38
+ * stem + slug + `~n` order), so no bundle id changed at the cut-over; an `it`
39
+ * with an interpolated title carries a hand-minted id from the static parts of
40
+ * its title, unique within the file.
27
41
  */
28
42
 
29
43
  export const IT_ID_PREFIX = 'openwop.it.';
@@ -60,6 +74,27 @@ export function scenarioFileOfItId(requirementId: string): string | null {
60
74
  return `${rest.slice(0, dot)}.test.ts`;
61
75
  }
62
76
 
77
+ /**
78
+ * Every FILE-derived id back to its file: the per-test `openwop.it.<stem>.<title>`
79
+ * plus the per-file rows the ledger also carries, `openwop.scenario.<stem>` and
80
+ * `openwop.floor.<stem>`. Gate rows (`openwop.profile.*`, `openwop.family.*`),
81
+ * explicit `req()` ids and the `openwop.floor.any.<prefix>` GROUP name no single
82
+ * file and return null. A caller that understood only `it` ids read a per-file
83
+ * row as an un-cited requirement and demanded a `req()` literal that, by
84
+ * construction, never exists.
85
+ */
86
+ export function scenarioFileOfId(requirementId: string): string | null {
87
+ const it = scenarioFileOfItId(requirementId);
88
+ if (it !== null) return it;
89
+ for (const prefix of ['openwop.scenario.', 'openwop.floor.']) {
90
+ if (!requirementId.startsWith(prefix)) continue;
91
+ const rest = requirementId.slice(prefix.length);
92
+ if (rest.length === 0 || rest.startsWith('any.')) return null;
93
+ return `${rest}.test.ts`;
94
+ }
95
+ return null;
96
+ }
97
+
63
98
  /**
64
99
  * Collision suffixing: the first occurrence keeps the bare id, later ones get
65
100
  * `~2`, `~3`, … Keyed per file so a worker running many files never crosses
@@ -70,7 +105,11 @@ export class ItIdAllocator {
70
105
 
71
106
  allocate(scenarioFile: string, title: string): string {
72
107
  const base = itRequirementId(scenarioFile, title);
73
- const n = (this.seen.get(base) ?? 0) + 1;
108
+ let n = (this.seen.get(base) ?? 0) + 1;
109
+ // An id a sibling `it` already claimed through `req()` never reached this
110
+ // allocator, so a same-titled `it` that cites nothing must skip past it —
111
+ // otherwise the first explicit `exists` and the derived `exists` collide.
112
+ while (claimedExplicit.has(n === 1 ? base : `${base}~${n}`)) n++;
74
113
  this.seen.set(base, n);
75
114
  return n === 1 ? base : `${base}~${n}`;
76
115
  }
@@ -85,22 +124,33 @@ export class ItIdAllocator {
85
124
  // ---------------------------------------------------------------------------
86
125
 
87
126
  let explicitId: string | null = null;
127
+ /** Every id handed to `req()` in this worker — consulted by `ItIdAllocator`. */
128
+ const claimedExplicit = new Set<string>();
88
129
 
89
130
  /**
90
131
  * Attach a hand-authored requirement id to the CURRENT test. The per-`it`
91
132
  * 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:
133
+ * title id. Returns the SAME message shape `driver.describe` returns —
134
+ * `[<impl>@<version>] <section>: <requirement>` — so a failure still names
135
+ * the implementation under test:
94
136
  *
95
137
  * expect(x, req('openwop.auth.subject-link.leaver-deny', 'auth-profiles.md §Subject linking', 'deactivation MUST deny')).toBe(true)
96
138
  *
139
+ * The implementation label is read straight from `OPENWOP_IMPLEMENTATION_NAME`
140
+ * / `OPENWOP_IMPLEMENTATION_VERSION` with the defaults `env.ts` uses (`unknown`)
141
+ * rather than through `loadEnv()`, so a suite self-test can call `req()`
142
+ * without `OPENWOP_BASE_URL` set.
143
+ *
97
144
  * The id MUST be listed in `conformance/requirements.json` (the generator
98
145
  * collects `req(` first-argument literals); an unlisted id fails the registry
99
146
  * check, so a hand id cannot drift from the registry silently.
100
147
  */
101
148
  export function req(id: string, specSection: string, requirement: string): string {
102
149
  explicitId = id;
103
- return `${specSection}: ${requirement}`;
150
+ claimedExplicit.add(id);
151
+ const impl = process.env['OPENWOP_IMPLEMENTATION_NAME']?.trim() ?? 'unknown';
152
+ const version = process.env['OPENWOP_IMPLEMENTATION_VERSION']?.trim() ?? 'unknown';
153
+ return `[${impl}@${version}] ${specSection}: ${requirement}`;
104
154
  }
105
155
 
106
156
  /** setup.ts reads and clears the override after each test. */
@@ -61,6 +61,11 @@ export interface LedgerEntry {
61
61
  * program exists to make visible; the runner treats it as unclassified for
62
62
  * a claimed profile's floor. */
63
63
  readonly assertionCount?: number;
64
+ /** The scenario file that recorded it. An explicit `req()` id carries no file
65
+ * in its own text, so without this the `--certify` runner cannot attribute
66
+ * the row to a file and drops it — which is how every explicit requirement id
67
+ * went missing from bundle v3 while the per-`it` ids came through. */
68
+ readonly scenarioFile?: string;
64
69
  }
65
70
 
66
71
  const ledger = new Map<string, LedgerEntry>();
@@ -82,7 +87,7 @@ export function recordRequirement(
82
87
  requirementId: string,
83
88
  disposition: Disposition,
84
89
  detail?: string,
85
- extras?: { assertionCount?: number },
90
+ extras?: { assertionCount?: number; scenarioFile?: string },
86
91
  ): void {
87
92
  const prior = ledger.get(requirementId);
88
93
  if (prior !== undefined && prior.disposition !== disposition) {
@@ -102,6 +107,7 @@ export function recordRequirement(
102
107
  disposition,
103
108
  ...(detail === undefined ? {} : { detail }),
104
109
  ...(extras?.assertionCount === undefined ? {} : { assertionCount: extras.assertionCount }),
110
+ ...(extras?.scenarioFile === undefined ? {} : { scenarioFile: extras.scenarioFile }),
105
111
  };
106
112
  ledger.set(requirementId, entry);
107
113
  journal.push(entry);
@@ -124,6 +124,7 @@ export function resolveFileRecord(
124
124
  // them. See lib/spec-coherence.ts for why not a new disposition value.
125
125
  if (
126
126
  specCoherenceFile !== undefined
127
+ && process.env.OPENWOP_CORPUS_GATE !== '1' // suite 2.0.0: under the corpus gate the coherence scenario IS the subject
127
128
  && SPEC_COHERENCE_SCENARIOS.has(specCoherenceFile)
128
129
  && !states.includes('fail')
129
130
  && assertionCount === 0
@@ -292,7 +293,11 @@ export function deriveRequirementDispositions(
292
293
  // them. This is the granularity RFC 0148 §A describes and the G8 fix.
293
294
  const emitted = new Set(rows.map((r) => r.requirementId));
294
295
  for (const e of [...ledger].sort((a, b) => a.requirementId.localeCompare(b.requirementId))) {
295
- const file = scenarioFileOfItId(e.requirementId);
296
+ // Prefer the file the entry recorded: an explicit `req()` id
297
+ // (`openwop.requirement.…`) is authored, not file-derived, so deriving a
298
+ // file from the id alone returned null and the row was dropped — every
299
+ // explicit requirement id was missing from bundle v3 for that reason.
300
+ const file = e.scenarioFile ?? scenarioFileOfItId(e.requirementId);
296
301
  // Attribute only to files this run reported on: a worker's ledger can carry
297
302
  // rows from files outside the certified set (the suite's own lib tests, or
298
303
  // a filtered run), and those are not evidence about the host.