@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,104 @@
1
+ /**
2
+ * v2 — `capability-record-shape` (suite 2.0.0; RFC 0169 §A.1–§A.3;
3
+ * `spec/v2/core/capabilities.md` §2 "The capability record").
4
+ *
5
+ * Witness class: witnessable — unaided. Every family record at the v2 root
6
+ * carries `status`, `since`, `witness`; `until` is present iff the record is
7
+ * not `stable`; an `until` already in the past makes the document
8
+ * non-conformant. Which root keys are family records is read from the
9
+ * generated `schemas/v2/capabilities.schema.json` (a property whose schema
10
+ * requires `status`, `since`, `witness`), so metadata keys (§3.1) are never
11
+ * mistaken for records.
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { v2Discovery } from '../lib/v2.js';
18
+ import { SCHEMAS_DIR } from '../lib/paths.js';
19
+ import { softSkip } from '../lib/soft-skip.js';
20
+ import { req } from '../lib/requirement-ids.js';
21
+
22
+ const DOC = 'spec/v2/core/capabilities.md §2';
23
+ const STATUS = new Set(['stable', 'experimental', 'deprecated']);
24
+ const WITNESS = new Set(['witnessable-unaided', 'witnessable-gated', 'seam-gated', 'claims-check', 'negative-existence']);
25
+ const VERSION = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
26
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
27
+
28
+ async function discovery(): Promise<Record<string, unknown> | null> {
29
+ try { return await v2Discovery(); } catch { return null; }
30
+ }
31
+
32
+ /** Root keys the generated schema declares as family records. */
33
+ function familyKeys(): Set<string> {
34
+ const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'capabilities.schema.json'), 'utf8')) as { properties?: Record<string, { required?: string[] }> };
35
+ const out = new Set<string>();
36
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
37
+ const required = prop.required ?? [];
38
+ if (['status', 'since', 'witness'].every((r) => required.includes(r))) out.add(key);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** The records the host actually advertises, keyed by family. */
44
+ function records(doc: Record<string, unknown>): Array<[string, Record<string, unknown>]> {
45
+ const keys = familyKeys();
46
+ return Object.entries(doc)
47
+ .filter(([k, v]) => keys.has(k) && v !== null && typeof v === 'object' && !Array.isArray(v))
48
+ .map(([k, v]) => [k, v as Record<string, unknown>]);
49
+ }
50
+
51
+ /**
52
+ * `until` in the past: a date form is compared with today's UTC date; a
53
+ * `<major>.<minor>` form is "past" once the host already serves a version at or
54
+ * beyond it (the deprecation horizon has been reached on this very host).
55
+ */
56
+ function untilInPast(until: string, served: string[]): boolean {
57
+ if (DATE.test(until)) return until < new Date().toISOString().slice(0, 10);
58
+ const [maj, min] = until.split('.').map(Number) as [number, number];
59
+ return served.some((v) => {
60
+ const [a, b] = v.split('.').map(Number) as [number, number];
61
+ return a > maj || (a === maj && b >= min);
62
+ });
63
+ }
64
+
65
+ describe('v2 capability-record-shape (RFC 0169 §A.1–§A.3)', () => {
66
+ it('every family record carries status, since and witness from the wire-legal sets', async () => {
67
+ const doc = await discovery();
68
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
69
+ const recs = records(doc);
70
+ if (recs.length === 0) return softSkip('inapplicable', 'the host advertises no family record at the v2 root (metadata only)');
71
+ for (const [key, rec] of recs) {
72
+ expect(STATUS.has(String(rec['status'])), req('openwop.requirement.0169.capability-record-shape.required-fields', DOC, `${key}.status MUST be one of stable | experimental | deprecated`)).toBe(true);
73
+ expect(typeof rec['since'] === 'string' && VERSION.test(rec['since']), req('openwop.requirement.0169.capability-record-shape.required-fields', DOC, `${key}.since MUST be present as <major>.<minor>`)).toBe(true);
74
+ expect(WITNESS.has(String(rec['witness'])), req('openwop.requirement.0169.capability-record-shape.required-fields', DOC, `${key}.witness MUST be one of the five wire-legal classes (unwitnessable MUST NOT appear on a wire record)`)).toBe(true);
75
+ expect('supported' in rec, req('openwop.requirement.0169.capability-record-shape.required-fields', DOC, `${key}: \`supported\` does not exist in v2 — presence of the record is the claim (§A.2)`)).toBe(false);
76
+ }
77
+ });
78
+
79
+ it('`until` is present iff the record is experimental or deprecated', async () => {
80
+ const doc = await discovery();
81
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
82
+ const recs = records(doc);
83
+ if (recs.length === 0) return softSkip('inapplicable', 'the host advertises no family record at the v2 root (metadata only)');
84
+ for (const [key, rec] of recs) {
85
+ const hasUntil = rec['until'] !== undefined;
86
+ const stable = rec['status'] === 'stable';
87
+ expect(hasUntil, req('openwop.requirement.0169.capability-record-shape.until-iff-not-stable', DOC, `${key}: \`until\` is REQUIRED when status is experimental or deprecated and MUST NOT be present when stable (status=${String(rec['status'])})`)).toBe(!stable);
88
+ if (hasUntil) {
89
+ expect(typeof rec['until'] === 'string' && (VERSION.test(rec['until']) || DATE.test(rec['until'])), req('openwop.requirement.0169.capability-record-shape.until-iff-not-stable', DOC, `${key}.until MUST be <major>.<minor> or YYYY-MM-DD`)).toBe(true);
90
+ }
91
+ }
92
+ });
93
+
94
+ it('no advertised record carries an `until` already in the past', async () => {
95
+ const doc = await discovery();
96
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
97
+ const served = Array.isArray(doc['protocolVersions']) ? (doc['protocolVersions'] as unknown[]).filter((v): v is string => typeof v === 'string') : [];
98
+ const dated = records(doc).filter(([, rec]) => typeof rec['until'] === 'string');
99
+ if (dated.length === 0) return softSkip('inapplicable', 'no advertised family record carries an `until` (every record is stable, or none is advertised)');
100
+ for (const [key, rec] of dated) {
101
+ expect(untilInPast(rec['until'] as string, served), req('openwop.requirement.0169.capability-record-shape.until-not-past', 'spec/v2/core/capabilities.md §2 (`until` in the past)', `${key}.until=${String(rec['until'])} is in the past — the document is non-conformant (a validator answers 400 until_in_past)`)).toBe(false);
102
+ }
103
+ });
104
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * v2-chain-pin-exact — RFC 0177 §E.1; `spec/v2/core/workflow-chain-packs.md`
3
+ * §"Exact pins".
4
+ *
5
+ * Suite 2.0.0, target major 2. Every reference a chain makes to a node type
6
+ * MUST pin an exact version per referenced `typeId` (`core.ai.callPrompt@1.0.0`);
7
+ * a host MUST refuse to register a chain whose reference carries a range or no
8
+ * version. Ranges are a v2.x additive follow-up.
9
+ *
10
+ * 1. a chain manifest referencing `core.ai.callPrompt@^1` is refused at
11
+ * register (4xx with an error code).
12
+ * 2. the same chain referencing `core.ai.callPrompt@1.0.0` registers.
13
+ *
14
+ * Both go through the seams-profile publish seam
15
+ * (`/conformance/seams/packs-test/…`, RFC 0168 §C.2), gated on
16
+ * `workflowChainPacks` + the seams profile. The exact-pin manifest is first
17
+ * validated against `schemas/v2/workflow-chain-pack-manifest.schema.json`: the
18
+ * schema's `FragmentNode.typeId` pattern has no room for `@<version>` and no
19
+ * sibling pin field exists, so while that gap stands the legs record `blocked`
20
+ * naming it (RFC 0177 §E.1 schema follow-up) rather than a pass.
21
+ *
22
+ * @see RFCS/0177-v2-registry-packs-and-extension-tail.md §E.1
23
+ * @see spec/v2/core/workflow-chain-packs.md §"Exact pins"
24
+ * @see schemas/v2/workflow-chain-pack-manifest.schema.json
25
+ */
26
+
27
+ import { describe, it, expect } from 'vitest';
28
+ import { gzipSync } from 'node:zlib';
29
+ import { driver } from '../lib/driver.js';
30
+ import { readErrorCode } from '../lib/error-envelope.js';
31
+ import { req } from '../lib/requirement-ids.js';
32
+ import { softSkip, type SoftSkipKind } from '../lib/soft-skip.js';
33
+ import { seamsProfileAdvertised, targetMajor } from '../lib/seams.js';
34
+ import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
35
+
36
+ const SECTION = 'workflow-chain-packs.md §"Exact pins" (RFC 0177 §E.1)';
37
+ const SCHEMA_GAP = 'workflow-chain-pack-manifest.schema.json carries no per-typeId version pin (FragmentNode.typeId rejects `core.ai.callPrompt@1.0.0` and no pin field exists) — RFC 0177 §E.1 schema follow-up';
38
+
39
+ function tarEntry(name: string, data: Buffer): Buffer {
40
+ const h = Buffer.alloc(512, 0);
41
+ h.write(name, 0, 100, 'utf8');
42
+ h.write('0000644\0', 100, 8, 'utf8');
43
+ h.write('0000000\0', 108, 8, 'utf8');
44
+ h.write('0000000\0', 116, 8, 'utf8');
45
+ h.write(`${data.length.toString(8).padStart(11, '0')}\0`, 124, 12, 'utf8');
46
+ h.write('00000000000\0', 136, 12, 'utf8');
47
+ h.write(' ', 148, 8, 'utf8');
48
+ h.write('0', 156, 1, 'utf8');
49
+ h.write('ustar\0', 257, 6, 'utf8');
50
+ h.write('00', 263, 2, 'utf8');
51
+ let sum = 0;
52
+ for (const b of h) sum += b;
53
+ h.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 8, 'utf8');
54
+ const pad = (512 - (data.length % 512)) % 512;
55
+ return Buffer.concat([h, data, Buffer.alloc(pad, 0)]);
56
+ }
57
+ function packTarball(files: Record<string, string>): Buffer {
58
+ return gzipSync(Buffer.concat([...Object.entries(files).map(([n, c]) => tarEntry(n, Buffer.from(c, 'utf8'))), Buffer.alloc(1024, 0)]));
59
+ }
60
+ function freshName(slug: string): string {
61
+ return `core.openwop.v2-${slug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
62
+ }
63
+ function chainPack(name: string, reference: string): Record<string, unknown> {
64
+ return {
65
+ name,
66
+ version: '1.0.0',
67
+ kind: 'workflow-chain',
68
+ engines: { openwop: '>=2.0.0 <3.0.0' },
69
+ chains: [{
70
+ chainId: `${name}.chain`,
71
+ version: '1.0.0',
72
+ label: 'Pin fixture',
73
+ description: 'A one-node chain whose only reference is the pin under test.',
74
+ parameters: { type: 'object', properties: {} },
75
+ dag: { nodes: [{ id: 'n1', typeId: reference }], edges: [] },
76
+ }],
77
+ };
78
+ }
79
+ async function publish(manifest: Record<string, unknown>) {
80
+ const name = manifest['name'] as string;
81
+ const version = manifest['version'] as string;
82
+ return driver.put(`/v1/packs-test/${encodeURIComponent(name)}/-/${encodeURIComponent(version)}.tgz`, packTarball({ 'pack.json': JSON.stringify(manifest, null, 2) }), {
83
+ headers: { 'Content-Type': 'application/octet-stream' },
84
+ });
85
+ }
86
+ async function preflight(): Promise<{ kind: SoftSkipKind; reason: string } | null> {
87
+ if (targetMajor() !== 2) return { kind: 'inapplicable', reason: 'suite 2.0.0 v2 scenario: OPENWOP_TARGET_MAJOR is not 2' };
88
+ let doc: Record<string, unknown> | null;
89
+ try {
90
+ doc = await v2Discovery();
91
+ } catch {
92
+ doc = null;
93
+ }
94
+ if (!doc) return { kind: 'blocked', reason: 'discovery unreachable — /.well-known/openwop (OpenWOP-Version: 2.0) did not answer 200 JSON' };
95
+ if (!(await gateFamily('workflowChainPacks'))) return { kind: 'inapplicable', reason: 'v2 discovery does not advertise the workflowChainPacks family (RFC 0169 §A.2)' };
96
+ if (!seamsProfileAdvertised(doc)) return { kind: 'blocked', reason: 'host does not advertise conformance.seamsProfile: openwop-conformance-seams-v2 — the packs-test publish seam is the only register path the suite can drive (RFC 0168 §C.1)' };
97
+ if (!v2Validator('workflow-chain-pack-manifest')(chainPack('core.openwop.pin-probe', 'core.ai.callPrompt@1.0.0')).ok) return { kind: 'blocked', reason: SCHEMA_GAP };
98
+ return null;
99
+ }
100
+
101
+ describe('v2-chain-pin-exact (RFC 0177 §E.1)', () => {
102
+ it('a chain reference carrying a range (core.ai.callPrompt@^1) is refused at register', async () => {
103
+ const skip = await preflight();
104
+ if (skip) return softSkip(skip.kind, skip.reason);
105
+ const res = await publish(chainPack(freshName('chain-range'), 'core.ai.callPrompt@^1'));
106
+ if (res.status === 404) return softSkip('blocked', 'packs-test publish seam answered 404 — seams profile advertised but the seam is not mounted');
107
+ expect(res.status >= 400 && res.status < 500, req('openwop.requirement.0177.chain-pin-exact.range-refused', SECTION, `a ranged reference MUST be refused at register (got ${res.status})`)).toBe(true);
108
+ expect(typeof readErrorCode(res.json), req('openwop.requirement.0177.chain-pin-exact.range-refused', SECTION, 'the refusal MUST carry an error code in the canonical envelope')).toBe('string');
109
+ });
110
+
111
+ it('a chain reference pinning an exact version (core.ai.callPrompt@1.0.0) registers', async () => {
112
+ const skip = await preflight();
113
+ if (skip) return softSkip(skip.kind, skip.reason);
114
+ const res = await publish(chainPack(freshName('chain-exact'), 'core.ai.callPrompt@1.0.0'));
115
+ if (res.status === 404) return softSkip('blocked', 'packs-test publish seam answered 404 — seams profile advertised but the seam is not mounted');
116
+ expect([200, 201].includes(res.status), req('openwop.requirement.0177.chain-pin-exact.exact-accepted', SECTION, `an exact pin MUST register (got ${res.status} ${readErrorCode(res.json) ?? ''})`)).toBe(true);
117
+ });
118
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * v2-coherence-not-in-bundle — RFC 0168 §D.1; `spec/v2/core/conformance.md`
3
+ * §"Two products, two ledgers".
4
+ *
5
+ * Suite 2.0.0. Corpus-coherence checks run in the spec repo's CI and MUST NOT
6
+ * appear in a host bundle; the bundle schema forbids their ids. Unaided and
7
+ * fixture-based — no host is consulted:
8
+ *
9
+ * 1. disjoint by construction: every id in `evidence/corpus-ledger.json`
10
+ * belongs to a scenario in `src/coherence/` and to none in
11
+ * `src/scenarios/` (a derived `openwop.it.<stem>.…` id resolves to its
12
+ * file; an explicit `req()` id is located by its literal in the sources),
13
+ * so a bundle produced from the scenarios directory cannot carry one.
14
+ * 2. the bundle schema rejects a v3 fixture whose `results.requirements[]`
15
+ * carries an id under `openwop.it.spec-corpus-validity.` and accepts the
16
+ * same fixture under an `openwop.requirement.` id.
17
+ *
18
+ * @see RFCS/0168-v2-evidence-and-conformance.md §D.1
19
+ * @see spec/v2/core/conformance.md §"Two products, two ledgers"
20
+ * @see schemas/v2/certification-bundle.schema.json
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
25
+ import { generateKeyPairSync } from 'node:crypto';
26
+ import { join } from 'node:path';
27
+ import { SCHEMAS_DIR, SCENARIOS_DIR } from '../lib/paths.js';
28
+ import { req } from '../lib/requirement-ids.js';
29
+ import { scenarioFileOfId } from '../lib/requirement-ids.js';
30
+ import { softSkip } from '../lib/soft-skip.js';
31
+ import { v2Validator } from '../lib/v2.js';
32
+ import { signBundleV3, witnessDigest, type BundleV3, type BundleV3Requirement } from '../lib/certification-bundle-v3.js';
33
+
34
+ const SECTION = 'conformance.md §"Two products, two ledgers" (RFC 0168 §D.1)';
35
+ const LEDGER = join(SCHEMAS_DIR, '..', 'evidence', 'corpus-ledger.json');
36
+ const COHERENCE_DIR = SCENARIOS_DIR === null ? null : join(SCENARIOS_DIR, '..', 'coherence');
37
+
38
+ function sources(dir: string): Map<string, string> {
39
+ return new Map(readdirSync(dir).filter((f) => f.endsWith('.test.ts')).map((f) => [f, readFileSync(join(dir, f), 'utf8')]));
40
+ }
41
+
42
+ function bundleWith(rows: BundleV3Requirement[]): BundleV3 {
43
+ const { privateKey } = generateKeyPairSync('ed25519');
44
+ const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string;
45
+ const unsigned: Omit<BundleV3, 'signature'> = {
46
+ bundleVersion: '3',
47
+ generatedAt: new Date().toISOString(),
48
+ suite: { name: '@openwop/openwop-conformance', version: '2.0.0', targetMajor: 2, specArtifactsVersion: '2.0.0' },
49
+ host: { name: 'fixture-host', version: '0.0.0', build: { kind: 'commit', id: 'fixture' } },
50
+ discovery: { url: 'https://host.invalid/.well-known/openwop', sha256: 'a'.repeat(64), protocolVersions: ['2.0'], preferredVersion: '2.0' },
51
+ claimedProfiles: [{ id: 'openwop-core-v2', evidenceTier: 'self', witnessCount: rows.length, certified: true }],
52
+ results: { totals: { executedPass: rows.length, executedFail: 0, skipped: 0, inapplicable: 0, blocked: 0 }, requirements: rows },
53
+ witnessSha256: witnessDigest(rows),
54
+ assertionCount: rows.reduce((n, r) => n + (r.assertions ?? 0), 0),
55
+ };
56
+ return { ...unsigned, signature: signBundleV3(unsigned, pem, 'fixture-host-key') };
57
+ }
58
+
59
+ describe('v2-coherence-not-in-bundle (RFC 0168 §D.1)', () => {
60
+ it('every corpus-ledger id has a scenario in src/coherence and none in src/scenarios — the two id sets are disjoint by construction', () => {
61
+ if (!existsSync(LEDGER)) return softSkip('blocked', 'evidence/corpus-ledger.json is absent from this layout — the corpus id set cannot be read');
62
+ if (SCENARIOS_DIR === null || COHERENCE_DIR === null || !existsSync(COHERENCE_DIR)) return softSkip('blocked', 'src/scenarios or src/coherence is absent from this layout');
63
+ const ledger = JSON.parse(readFileSync(LEDGER, 'utf8')) as { requirements: Record<string, unknown> };
64
+ const ids = Object.keys(ledger.requirements);
65
+ expect(ids.length, req('openwop.requirement.0168.coherence-not-in-bundle.disjoint-by-construction', SECTION, 'the corpus ledger MUST carry at least one requirement id')).toBeGreaterThan(0);
66
+ const coherence = sources(COHERENCE_DIR);
67
+ const scenarios = sources(SCENARIOS_DIR);
68
+ const coherenceText = [...coherence.values()].join('\n');
69
+ const scenariosText = [...scenarios.values()].join('\n');
70
+ for (const id of ids) {
71
+ // `openwop.profile.*` / `openwop.family.*` are capability-GATE rows minted
72
+ // at run time by gateFamily(), not by a `req()` literal and not derived
73
+ // from a file. The same gate id is recorded by a corpus run and a host run
74
+ // alike, so it is shared by construction and carries no corpus content —
75
+ // the disjointness rule does not reach it.
76
+ if (id.startsWith('openwop.profile.') || id.startsWith('openwop.family.')) continue;
77
+ const file = scenarioFileOfId(id);
78
+ if (file !== null) {
79
+ expect(coherence.has(file), req('openwop.requirement.0168.coherence-not-in-bundle.disjoint-by-construction', SECTION, `${id} MUST belong to a src/coherence scenario (${file})`)).toBe(true);
80
+ expect(scenarios.has(file), req('openwop.requirement.0168.coherence-not-in-bundle.disjoint-by-construction', SECTION, `${id} MUST NOT belong to a src/scenarios file (${file}) — it would enter a host bundle`)).toBe(false);
81
+ } else {
82
+ const literal = `'${id}'`;
83
+ expect(coherenceText.includes(literal), req('openwop.requirement.0168.coherence-not-in-bundle.disjoint-by-construction', SECTION, `${id} MUST be cited by req() in a src/coherence scenario`)).toBe(true);
84
+ expect(scenariosText.includes(literal), req('openwop.requirement.0168.coherence-not-in-bundle.disjoint-by-construction', SECTION, `${id} MUST NOT be cited by req() in a src/scenarios file — it would enter a host bundle`)).toBe(false);
85
+ }
86
+ }
87
+ });
88
+
89
+ it('the bundle schema rejects a v3 bundle carrying a corpus-coherence id and accepts one under openwop.requirement.', () => {
90
+ const validate = v2Validator('certification-bundle');
91
+ const corpusRow: BundleV3Requirement = { id: 'openwop.it.spec-corpus-validity.every-fixture-validates', scenario: 'spec-corpus-validity.test.ts', result: 'executed-pass', assertions: 1 };
92
+ const hostRow: BundleV3Requirement = { id: 'openwop.requirement.0168.coherence-not-in-bundle.fixture', scenario: 'v2-coherence-not-in-bundle.test.ts', result: 'executed-pass', assertions: 1 };
93
+ const control = validate(bundleWith([hostRow]));
94
+ expect(control.ok, req('openwop.requirement.0168.coherence-not-in-bundle.schema-forbids-coherence-ids', SECTION, `a v3 bundle whose rows are openwop.requirement. ids MUST validate (${control.errors})`)).toBe(true);
95
+ const tainted = validate(bundleWith([hostRow, corpusRow]));
96
+ expect(tainted.ok, req('openwop.requirement.0168.coherence-not-in-bundle.schema-forbids-coherence-ids', SECTION, 'a v3 bundle carrying an openwop.it.spec-corpus-validity. id MUST be rejected by the bundle schema')).toBe(false);
97
+ });
98
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * RFC 0173 §B — `compensation-read-projection` (suite 2.0.0, target major 2; gated on `compensation`).
3
+ *
4
+ * Compensation is a core obligation with a declared witness: a host that
5
+ * advertises `compensation` MUST serve `GET /runs/{runId}/compensation`
6
+ * (`api/v2/openapi.yaml` `getRunCompensation`,
7
+ * `schemas/v2/compensation-projection.schema.json`) — `{ runId, status, plan[],
8
+ * attempts[] }` — so the obligation is deployed-wire evidence rather than
9
+ * seam-only (RFC 0151 G9; RFC 0173 §B row C6.6; `spec/v2/core/security-defaults.md`
10
+ * §Compensation).
11
+ *
12
+ * A fresh run of the noop fixture has nothing to compensate: the projection
13
+ * validates with `status: "none"`, an empty plan and no attempts. An unknown
14
+ * run is `404 not_found` through the canonical envelope.
15
+ *
16
+ * @see spec/v2/core/security-defaults.md §Compensation
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+ import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
22
+ import { readErrorCode } from '../lib/error-envelope.js';
23
+ import { softSkip } from '../lib/soft-skip.js';
24
+ import { req } from '../lib/requirement-ids.js';
25
+
26
+ const FIXTURE = 'conformance-noop';
27
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
28
+
29
+ async function discovery(): Promise<Record<string, unknown> | null> {
30
+ try { return await v2Discovery(); } catch { return null; }
31
+ }
32
+
33
+ async function waitTerminal(runId: string, timeoutMs: number): Promise<void> {
34
+ const deadline = Date.now() + timeoutMs;
35
+ while (Date.now() < deadline) {
36
+ const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
37
+ if (res.status === 200 && TERMINAL.has(String((res.json as { status?: unknown } | null)?.status))) return;
38
+ await new Promise((r) => setTimeout(r, 250));
39
+ }
40
+ }
41
+
42
+ describe('RFC 0173 §B — compensation-read-projection (gated on compensation)', () => {
43
+ it('GET /runs/{runId}/compensation on a fresh run validates with status none', async () => {
44
+ const doc = await discovery();
45
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
46
+ if (!(await gateFamily('compensation'))) return softSkip('inapplicable', 'compensation family not advertised — no obligation (gate recorded under openwop.family.compensation)');
47
+ const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
48
+ if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to project`);
49
+
50
+ const create = await driver.post('/runs', { workflowId: FIXTURE });
51
+ expect(create.status, req('openwop.requirement.0173.compensation-read-projection', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
52
+ const runId = (create.json as { runId: string }).runId;
53
+ await waitTerminal(runId, 10_000);
54
+
55
+ const res = await driver.get(`/runs/${encodeURIComponent(runId)}/compensation`);
56
+ expect(
57
+ res.status,
58
+ req('openwop.requirement.0173.compensation-read-projection', 'security-defaults.md §Compensation', 'a host advertising `compensation` MUST serve GET /runs/{runId}/compensation with 200 (RFC 0173 §B — the read projection is the obligation\'s witness)'),
59
+ ).toBe(200);
60
+ const check = v2Validator('compensation-projection')(res.json);
61
+ expect(
62
+ check.ok,
63
+ req('openwop.requirement.0173.compensation-read-projection', 'compensation-projection.schema.json', `the projection MUST validate: ${check.errors}`),
64
+ ).toBe(true);
65
+ const body = res.json as { runId?: unknown; status?: unknown; plan?: unknown[]; attempts?: unknown[] };
66
+ expect(body.runId, req('openwop.requirement.0173.compensation-read-projection', 'compensation-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
67
+ expect(
68
+ body.status,
69
+ req('openwop.requirement.0173.compensation-read-projection', 'compensation-projection.schema.json status', 'a run with nothing to compensate MUST project status "none"'),
70
+ ).toBe('none');
71
+ expect(body.attempts, req('openwop.requirement.0173.compensation-read-projection', 'compensation-projection.schema.json attempts', 'no attempt exists before an unwind')).toEqual([]);
72
+ });
73
+
74
+ it('an unknown run projects 404 not_found through the canonical envelope', async () => {
75
+ const doc = await discovery();
76
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
77
+ if (!(await gateFamily('compensation'))) return softSkip('inapplicable', 'compensation family not advertised — no obligation (gate recorded under openwop.family.compensation)');
78
+ const res = await driver.get('/runs/conformance-no-such-run-0173/compensation');
79
+ expect(res.status, req('openwop.requirement.0173.compensation-read-projection.not-found', 'openapi.yaml getRunCompensation 404', 'an unknown runId MUST answer 404')).toBe(404);
80
+ expect(
81
+ readErrorCode(res.json),
82
+ req('openwop.requirement.0173.compensation-read-projection.not-found', 'errors.md §Envelope', 'the 404 MUST carry the registered not_found code in the flat envelope'),
83
+ ).toBe('not_found');
84
+ });
85
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * v2 — `configurable-closed` (suite 2.0.0; RFC 0171 §D.1;
3
+ * `spec/v2/core/runs.md` §"RunOptions" / `configurable`;
4
+ * `schemas/v2/configurable.schema.json`).
5
+ *
6
+ * Witness class: witnessable — unaided. `configurable` is a closed, nested,
7
+ * versioned object. A dotted key (`ai.provider` as a string key inside a
8
+ * section) and an unknown root key MUST be rejected with
9
+ * `400 validation_error`. The schema leg is server-free (the closure is a
10
+ * property of the standalone artifact); the two wire legs post the bodies to
11
+ * `POST /runs` and record `blocked` when the host refuses the control body
12
+ * for a reason unrelated to `configurable`.
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
17
+ import { v2Discovery, v2Validator } from '../lib/v2.js';
18
+ import { readErrorCode } from '../lib/error-envelope.js';
19
+ import { softSkip } from '../lib/soft-skip.js';
20
+ import { req } from '../lib/requirement-ids.js';
21
+
22
+ const DOC = 'spec/v2/core/runs.md §configurable';
23
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
24
+ const DOTTED = { version: 1, ai: { 'ai.provider': 'x' } };
25
+ const UNKNOWN = { version: 1, unknownKey: 1 };
26
+ const CONTROL = { version: 1 };
27
+
28
+ async function discovery(): Promise<Record<string, unknown> | null> {
29
+ try { return await v2Discovery(); } catch { return null; }
30
+ }
31
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
32
+ try { return await fn(); } catch { return null; }
33
+ }
34
+
35
+ /** Post a body; when the CONTROL body is itself refused the wire leg is blocked. */
36
+ async function probe(configurable: Record<string, unknown>): Promise<OpenWOPResponse | { reason: string }> {
37
+ if (!(await discovery())) return { reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
38
+ const control = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID, configurable: CONTROL }));
39
+ if (control === null) return { reason: 'POST /runs unreachable (fetch failed)' };
40
+ if (control.status !== 201) return { reason: `POST /runs with the control body { version: 1 } answered ${control.status} ${readErrorCode(control.json) ?? ''} — the create itself is refused, so the closure of configurable cannot be observed (fixture not seeded?)`.trim() };
41
+ const res = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID, configurable }));
42
+ return res ?? { reason: 'POST /runs unreachable (fetch failed)' };
43
+ }
44
+
45
+ describe('v2 configurable-closed (RFC 0171 §D.1)', () => {
46
+ it('the standalone schema rejects a dotted key and an unknown root key and accepts the versioned control', () => {
47
+ const validate = v2Validator('configurable');
48
+ expect(validate(CONTROL).ok, req('openwop.requirement.0171.configurable-closed.schema', DOC, '{ version: 1 } is the smallest valid configurable (version REQUIRED, const 1)')).toBe(true);
49
+ expect(validate(DOTTED).ok, req('openwop.requirement.0171.configurable-closed.schema', DOC, 'a dotted key (`ai.provider` as a string key) MUST fail the closed nested schema')).toBe(false);
50
+ expect(validate(UNKNOWN).ok, req('openwop.requirement.0171.configurable-closed.schema', DOC, 'an unknown root key MUST fail the closed schema (additionalProperties: false in the standalone artifact)')).toBe(false);
51
+ expect(validate({ ai: { provider: 'x' } }).ok, req('openwop.requirement.0171.configurable-closed.schema', DOC, 'a configurable without `version` MUST fail')).toBe(false);
52
+ });
53
+
54
+ it('POST /runs rejects a dotted key with 400 validation_error', async () => {
55
+ const res = await probe(DOTTED);
56
+ if ('reason' in res) return softSkip('blocked', res.reason);
57
+ expect(res.status, req('openwop.requirement.0171.configurable-closed.dotted-key', DOC, 'a dotted key inside a section MUST be rejected with 400')).toBe(400);
58
+ expect(readErrorCode(res.json), req('openwop.requirement.0171.configurable-closed.dotted-key', DOC, 'the refusal code MUST be validation_error')).toBe('validation_error');
59
+ });
60
+
61
+ it('POST /runs rejects an unknown root key with 400 validation_error', async () => {
62
+ const res = await probe(UNKNOWN);
63
+ if ('reason' in res) return softSkip('blocked', res.reason);
64
+ expect(res.status, req('openwop.requirement.0171.configurable-closed.unknown-key', DOC, 'an unknown root key MUST be rejected with 400')).toBe(400);
65
+ expect(readErrorCode(res.json), req('openwop.requirement.0171.configurable-closed.unknown-key', DOC, 'the refusal code MUST be validation_error')).toBe('validation_error');
66
+ });
67
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * v2 — `dual-stack-negotiation` (suite 2.0.0; RFC 0172 §A.3–§A.4;
3
+ * `spec/v2/core/versioning.md` §1.3 "The request header", §1.4 "The response
4
+ * header", §5 "The overlap").
5
+ *
6
+ * Witness class: witnessable — gated on two majors. Unless `protocolVersions[]`
7
+ * carries both a `1.x` and a `2.x` member the file records `inapplicable`. On a
8
+ * dual-advertising host: a run created through `/v1/runs` with NO
9
+ * `OpenWOP-Version` header is readable through `GET /runs/{runId}` under
10
+ * `OpenWOP-Version: 2.0`; an unlisted major (`9.0`) is `406
11
+ * protocol_version_unsupported` with `details.protocolVersions[]`;
12
+ * `OpenWOP-Version: 2.0` on a `/v1/…` path is `400 protocol_version_mismatch`;
13
+ * every response's `OpenWOP-Version` equals the contract that produced it. The
14
+ * header-less v1 call bypasses the driver (which stamps `OpenWOP-Version: 2.0`
15
+ * under target major 2) with a raw `fetch` against the same base URL.
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
20
+ import { loadEnv } from '../lib/env.js';
21
+ import { v2Discovery } from '../lib/v2.js';
22
+ import { readErrorCode } from '../lib/error-envelope.js';
23
+ import { softSkip } from '../lib/soft-skip.js';
24
+ import { req } from '../lib/requirement-ids.js';
25
+
26
+ const DOC = 'spec/v2/core/versioning.md §1.3';
27
+ const NOOP_WORKFLOW_ID = 'conformance-noop';
28
+
29
+ async function discovery(): Promise<Record<string, unknown> | null> {
30
+ try { return await v2Discovery(); } catch { return null; }
31
+ }
32
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
33
+ try { return await fn(); } catch { return null; }
34
+ }
35
+
36
+ /** Raw request with exactly the headers given — no OpenWOP-Version unless named. */
37
+ async function raw(method: string, path: string, headers: Record<string, string>, body?: unknown): Promise<OpenWOPResponse | null> {
38
+ try {
39
+ const env = loadEnv();
40
+ const init: RequestInit = { method, headers: { Accept: 'application/json', Authorization: `Bearer ${env.apiKey}`, ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...headers } };
41
+ if (body !== undefined) init.body = JSON.stringify(body);
42
+ const res = await fetch(`${env.baseUrl}${path}`, init);
43
+ const text = await res.text();
44
+ let json: unknown;
45
+ try { json = text.length > 0 ? JSON.parse(text) : undefined; } catch { json = undefined; }
46
+ return { status: res.status, headers: res.headers, text, json };
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function major(res: OpenWOPResponse | null): string | null {
53
+ const v = res?.headers.get('openwop-version')?.trim();
54
+ return v === undefined ? null : (v.split('.')[0] ?? null);
55
+ }
56
+
57
+ async function gate(): Promise<{ versions: string[] } | { kind: 'blocked' | 'inapplicable'; reason: string }> {
58
+ const doc = await discovery();
59
+ if (!doc) return { kind: 'blocked', reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
60
+ const versions = Array.isArray(doc['protocolVersions']) ? (doc['protocolVersions'] as unknown[]).filter((v): v is string => typeof v === 'string') : [];
61
+ const has1 = versions.some((v) => v.startsWith('1.'));
62
+ const has2 = versions.some((v) => v.startsWith('2.'));
63
+ if (!(has1 && has2)) return { kind: 'inapplicable', reason: `protocolVersions [${versions.join(', ')}] does not carry both a 1.x and a 2.x member — dual-stack negotiation is gated on two majors` };
64
+ return { versions };
65
+ }
66
+
67
+ describe('v2 dual-stack-negotiation (RFC 0172 §A.3–§A.4 — gated on two majors)', () => {
68
+ it('a run created through /v1/runs with no header is readable through /runs under OpenWOP-Version: 2.0', async () => {
69
+ const g = await gate();
70
+ if ('kind' in g) return softSkip(g.kind, g.reason);
71
+ const created = await raw('POST', '/v1/runs', {}, { workflowId: NOOP_WORKFLOW_ID });
72
+ if (created === null) return softSkip('blocked', 'POST /v1/runs unreachable (fetch failed)');
73
+ const runId = (created.json as { runId?: unknown } | undefined)?.runId;
74
+ if (created.status !== 201 || typeof runId !== 'string') return softSkip('blocked', `POST /v1/runs {workflowId: ${NOOP_WORKFLOW_ID}} with no header answered ${created.status} ${readErrorCode(created.json) ?? ''} — the v1 create was refused (fixture not seeded?)`);
75
+ expect(major(created), req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §1.4', 'a /v1/ response MUST report the 1.x contract that produced it')).toBe('1');
76
+ const read = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`, { headers: { 'OpenWOP-Version': '2.0' } }));
77
+ if (read === null) return softSkip('blocked', 'GET /runs/{runId} unreachable (fetch failed)');
78
+ expect(read.status, req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §5', 'the overlap: a run created through /v1/runs MUST be readable through GET /runs/{runId} with OpenWOP-Version: 2.0')).toBe(200);
79
+ expect((read.json as { runId?: unknown } | undefined)?.runId, req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §5', 'the v2 read MUST name the same run')).toBe(runId);
80
+ expect(major(read), req('openwop.requirement.0172.dual-stack-negotiation.cross-major-read', 'spec/v2/core/versioning.md §1.4', 'the v2 read MUST report the 2.x contract that produced it')).toBe('2');
81
+ });
82
+
83
+ it('an unlisted major is 406 protocol_version_unsupported with details.protocolVersions[]', async () => {
84
+ const g = await gate();
85
+ if ('kind' in g) return softSkip(g.kind, g.reason);
86
+ const res = await http(() => driver.get('/.well-known/openwop', { authenticated: false, headers: { 'OpenWOP-Version': '9.0' } }));
87
+ if (res === null) return softSkip('blocked', 'GET /.well-known/openwop unreachable (fetch failed)');
88
+ expect(res.status, req('openwop.requirement.0172.dual-stack-negotiation.unlisted-major', DOC, 'a header naming a major not in protocolVersions[] MUST be answered 406')).toBe(406);
89
+ expect(readErrorCode(res.json), req('openwop.requirement.0172.dual-stack-negotiation.unlisted-major', DOC, 'the refusal code MUST be protocol_version_unsupported')).toBe('protocol_version_unsupported');
90
+ const echoed = (res.json as { details?: { protocolVersions?: unknown } } | undefined)?.details?.protocolVersions;
91
+ expect(Array.isArray(echoed) ? [...echoed].sort() : echoed, req('openwop.requirement.0172.dual-stack-negotiation.unlisted-major', DOC, 'details.protocolVersions[] MUST echo the advertised list')).toEqual([...g.versions].sort());
92
+ });
93
+
94
+ it('OpenWOP-Version: 2.0 on a /v1/ path is 400 protocol_version_mismatch', async () => {
95
+ const g = await gate();
96
+ if ('kind' in g) return softSkip(g.kind, g.reason);
97
+ const res = await http(() => driver.get('/v1/openapi.json', { authenticated: false, headers: { 'OpenWOP-Version': '2.0' } }));
98
+ if (res === null) return softSkip('blocked', 'GET /v1/openapi.json unreachable (fetch failed)');
99
+ expect(res.status, req('openwop.requirement.0172.dual-stack-negotiation.v1-path-mismatch', DOC, 'a /v1/… path with OpenWOP-Version other than 1 MUST be answered 400')).toBe(400);
100
+ expect(readErrorCode(res.json), req('openwop.requirement.0172.dual-stack-negotiation.v1-path-mismatch', DOC, 'the refusal code MUST be protocol_version_mismatch')).toBe('protocol_version_mismatch');
101
+ });
102
+
103
+ it('every response reports the contract that produced it', async () => {
104
+ const g = await gate();
105
+ if ('kind' in g) return softSkip(g.kind, g.reason);
106
+ const v1 = await raw('GET', '/v1/openapi.json', {});
107
+ const v1Named = await raw('GET', '/v1/openapi.json', { 'OpenWOP-Version': '1.0' });
108
+ const v2 = await http(() => driver.get('/.well-known/openwop', { authenticated: false, headers: { 'OpenWOP-Version': '2.0' } }));
109
+ if (v1 === null || v1Named === null || v2 === null) return softSkip('blocked', 'a probe was unreachable (fetch failed)');
110
+ expect(major(v1), req('openwop.requirement.0172.dual-stack-negotiation.response-header', 'spec/v2/core/versioning.md §1.4', 'a header-less /v1/ response MUST carry OpenWOP-Version: 1.<minor> — the contract used, never another (silent downgrade)')).toBe('1');
111
+ expect(major(v1Named), req('openwop.requirement.0172.dual-stack-negotiation.response-header', 'spec/v2/core/versioning.md §1.4', 'a /v1/ response under OpenWOP-Version: 1.0 MUST carry OpenWOP-Version: 1.<minor>')).toBe('1');
112
+ expect(major(v2), req('openwop.requirement.0172.dual-stack-negotiation.response-header', 'spec/v2/core/versioning.md §1.4', 'a response under OpenWOP-Version: 2.0 MUST carry OpenWOP-Version: 2.<minor>')).toBe('2');
113
+ for (const res of [v1, v1Named, v2]) {
114
+ const v = res.headers.get('openwop-version')?.trim() ?? '';
115
+ expect(g.versions.includes(v), req('openwop.requirement.0172.dual-stack-negotiation.response-header', 'spec/v2/core/versioning.md §1.4', `the reported contract (${v}) MUST be a member of the advertised protocolVersions[]`)).toBe(true);
116
+ }
117
+ });
118
+ });