@openwop/openwop-conformance 1.106.1 → 1.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +3 -3
  2. package/coverage.md +6 -0
  3. package/dist/cli.js +89 -45
  4. package/dist/lib/certification-bundle-verify.js +274 -0
  5. package/dist/lib/profiles.js +102 -19
  6. package/dist/lib/requirement-ledger.js +182 -0
  7. package/dist/lib/requirement-registry.js +110 -0
  8. package/dist/lib/scenario-disposition.js +249 -0
  9. package/dist/lib/soft-skip.js +82 -0
  10. package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
  11. package/fixtures.md +1 -1
  12. package/package.json +1 -1
  13. package/schemas/CORPUS-STAMP.json +2 -2
  14. package/schemas/README.md +1 -0
  15. package/schemas/capabilities.schema.json +2 -2
  16. package/schemas/compensation-policy.schema.json +64 -0
  17. package/schemas/run-snapshot.schema.json +5 -0
  18. package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
  19. package/schemas/workflow-definition.schema.json +4 -0
  20. package/src/cli.ts +99 -45
  21. package/src/lib/a2a-fake-peer.ts +290 -110
  22. package/src/lib/certification-bundle-verify.ts +356 -0
  23. package/src/lib/error-envelope.ts +77 -0
  24. package/src/lib/llm-cache-key-recipe.ts +6 -5
  25. package/src/lib/mcp-fake-server.ts +235 -72
  26. package/src/lib/profiles.ts +132 -19
  27. package/src/lib/requirement-ledger.ts +98 -1
  28. package/src/lib/requirement-registry.ts +47 -4
  29. package/src/lib/scenario-disposition.ts +291 -0
  30. package/src/lib/soft-skip.ts +89 -0
  31. package/src/lib/workflow-chain-expansion.ts +189 -0
  32. package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
  33. package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
  34. package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
  35. package/src/scenarios/a2a-peer-authority.test.ts +99 -0
  36. package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
  37. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
  38. package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
  39. package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
  40. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
  41. package/src/scenarios/approval-gate-flow.test.ts +5 -4
  42. package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
  43. package/src/scenarios/auth-scim-profile.test.ts +5 -4
  44. package/src/scenarios/authorization-fail-closed.test.ts +4 -3
  45. package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
  46. package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
  47. package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
  48. package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
  49. package/src/scenarios/commitment-fired.test.ts +3 -2
  50. package/src/scenarios/compensation-behavior.test.ts +63 -0
  51. package/src/scenarios/compensation-profile.test.ts +144 -1
  52. package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
  53. package/src/scenarios/conformance-execution-witness.test.ts +104 -0
  54. package/src/scenarios/conversationCapabilityNegotiation.test.ts +3 -2
  55. package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
  56. package/src/scenarios/credential-payload-redaction.test.ts +4 -3
  57. package/src/scenarios/credentials-capability-shape.test.ts +4 -3
  58. package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
  59. package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
  60. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
  61. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  62. package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
  63. package/src/scenarios/data-residency-admission.test.ts +6 -6
  64. package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
  65. package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
  66. package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
  67. package/src/scenarios/distillation-shape.test.ts +2 -1
  68. package/src/scenarios/distillation-stable-archive.test.ts +4 -3
  69. package/src/scenarios/distillation-token-budget.test.ts +4 -3
  70. package/src/scenarios/envelope-recovery-applied.test.ts +3 -2
  71. package/src/scenarios/envelope-refusal-shape.test.ts +4 -3
  72. package/src/scenarios/error-envelope-canonical-shape.test.ts +64 -0
  73. package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
  74. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
  75. package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
  76. package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
  77. package/src/scenarios/feedback-record-and-list.test.ts +4 -3
  78. package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
  79. package/src/scenarios/fixtures-valid.test.ts +16 -18
  80. package/src/scenarios/fs-path-traversal.test.ts +3 -2
  81. package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
  82. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
  83. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
  84. package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
  85. package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
  86. package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
  87. package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
  88. package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
  89. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
  90. package/src/scenarios/mcp-stateless-request.test.ts +77 -0
  91. package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
  92. package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
  93. package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
  94. package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
  95. package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
  96. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
  97. package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
  98. package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
  99. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
  100. package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
  101. package/src/scenarios/memory-injection-budget.test.ts +4 -3
  102. package/src/scenarios/metric-emission.test.ts +4 -3
  103. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  104. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
  105. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
  106. package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
  107. package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
  108. package/src/scenarios/multi-region-idempotency.test.ts +4 -3
  109. package/src/scenarios/normative-example-extraction.test.ts +242 -0
  110. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
  111. package/src/scenarios/oauth-capability-shape.test.ts +4 -3
  112. package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
  113. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
  114. package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
  115. package/src/scenarios/otel-emission-grpc.test.ts +4 -3
  116. package/src/scenarios/otel-emission.test.ts +4 -3
  117. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
  118. package/src/scenarios/otel-trace-propagation.test.ts +4 -3
  119. package/src/scenarios/pack-registry-isolation.test.ts +4 -3
  120. package/src/scenarios/pack-registry-publish.test.ts +65 -3
  121. package/src/scenarios/pack-registry.test.ts +22 -0
  122. package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
  123. package/src/scenarios/profileDerivation.test.ts +2 -1
  124. package/src/scenarios/provider-usage.test.ts +2 -2
  125. package/src/scenarios/rate-limit-envelope.test.ts +2 -1
  126. package/src/scenarios/registry-public.test.ts +6 -5
  127. package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
  128. package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
  129. package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
  130. package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
  131. package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
  132. package/src/scenarios/requirement-ledger.test.ts +29 -6
  133. package/src/scenarios/runner-ledger.test.ts +279 -0
  134. package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
  135. package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
  136. package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
  137. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  138. package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
  139. package/src/scenarios/stream-text-fixture.test.ts +5 -4
  140. package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
  141. package/src/scenarios/subrun-approval-gate.test.ts +4 -3
  142. package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
  143. package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
  144. package/src/scenarios/table-schema-enforcement.test.ts +2 -2
  145. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
  146. package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
  147. package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
  148. package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
  149. package/src/scenarios/tool-hooks-shape.test.ts +2 -1
  150. package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
  151. package/src/scenarios/version-fold.test.ts +5 -4
  152. package/src/scenarios/voice-streamref-tenant-bound.test.ts +2 -1
  153. package/src/scenarios/voice-transcription-streaming.test.ts +2 -1
  154. package/src/scenarios/voice-transcription-unadvertised.test.ts +2 -1
  155. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
  156. package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
  157. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
  158. package/src/scenarios/wasm-pack-load.test.ts +4 -3
  159. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
  160. package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
  161. package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
  162. package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
  163. package/src/scenarios/workload-identity-behavior.test.ts +7 -4
  164. package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
  165. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
  166. package/src/setup.ts +95 -1
@@ -20,35 +20,25 @@
20
20
  import { describe, it, expect } from 'vitest';
21
21
  import Ajv2020 from 'ajv/dist/2020.js';
22
22
  import addFormats from 'ajv-formats';
23
- import { readFileSync } from 'node:fs';
23
+ import { readFileSync, readdirSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
25
  import { SCHEMAS_DIR } from '../lib/paths.js';
26
26
 
27
27
  function compileWorkflowDefinition(): ReturnType<Ajv2020['compile']> {
28
28
  const ajv = new Ajv2020({ strict: false, allErrors: true });
29
29
  addFormats(ajv);
30
- // Register cross-file `$ref` targetssame pattern as
31
- // `fixtures-valid.test.ts`. Without these, Ajv throws
32
- // `missingRef` when compiling `workflow-definition.schema.json`
33
- // because it references agent-ref + prompt-ref by URL.
34
- const agentRefSchema = JSON.parse(
35
- readFileSync(join(SCHEMAS_DIR, 'agent-ref.schema.json'), 'utf8'),
36
- ) as Record<string, unknown>;
37
- const promptRefSchema = JSON.parse(
38
- readFileSync(join(SCHEMAS_DIR, 'prompt-ref.schema.json'), 'utf8'),
39
- ) as Record<string, unknown>;
40
- const promptKindSchema = JSON.parse(
41
- readFileSync(join(SCHEMAS_DIR, 'prompt-kind.schema.json'), 'utf8'),
42
- ) as Record<string, unknown>;
43
- ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
44
- ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
45
- ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
46
- ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
47
- ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
48
- const schema = JSON.parse(
49
- readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8'),
50
- ) as Record<string, unknown>;
51
- return ajv.compile(schema);
30
+ // Register EVERY schema in the corpus (S14, 2026-08-16) a fixed ref list is
31
+ // a claim about the schema graph that nothing keeps true; the fourth cross-file
32
+ // `$ref` (#1009) broke the previous list at describe time.
33
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
34
+ const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as Record<string, unknown>;
35
+ ajv.addSchema(s, file);
36
+ ajv.addSchema(s, `./${file}`);
37
+ }
38
+ return (
39
+ ajv.getSchema('workflow-definition.schema.json') ??
40
+ ajv.compile(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8')) as Record<string, unknown>)
41
+ );
52
42
  }
53
43
 
54
44
  /** Build the minimal-required shape of a WorkflowDefinition. Tests
@@ -33,6 +33,7 @@
33
33
  */
34
34
 
35
35
  import { describe, it, expect } from 'vitest';
36
+ import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
36
37
  import { driver } from '../lib/driver.js';
37
38
  import { behaviorGate } from '../lib/behavior-gate.js';
38
39
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -76,7 +77,7 @@ async function resolve(body: Record<string, unknown>): Promise<{ status: number;
76
77
  }
77
78
 
78
79
  function reasonOf(json: unknown): string | undefined {
79
- return (json as { error?: { code?: string } })?.error?.code;
80
+ return readErrorCode(json);
80
81
  }
81
82
 
82
83
  const IDENTITY = { scheme: 'spiffe', subject: 'spiffe://example/dispatcher', issuer: 'spiffe://example' };
@@ -154,9 +155,11 @@ describe('RFC 0154 §A — workload identity resolution (capability-gated behavi
154
155
  if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
155
156
  const r = await resolve({ identity: { scheme: 'spiffe', subject: 'spiffe://example/unknown' } });
156
157
  if (r === null || r.status < 400) return;
157
- const err = (r.json as { error?: { code?: string; retriable?: boolean } }).error;
158
+ // canonical flat envelope: code = `error`, retriable = `details.retriable`
159
+ // (the legacy nested `error.{code,retriable}` the §20 catalog prescribed until
160
+ // 2026-08-16 is tolerated by the helpers for the deprecation window)
158
161
  expect(
159
- err?.retriable,
162
+ readRetriable(r.json),
160
163
  driver.describe(
161
164
  'spec/v1/host-sample-test-seams.md §20',
162
165
  'an identity that does not resolve will not resolve on retry. Marking it retriable invites ' +
@@ -166,7 +169,7 @@ describe('RFC 0154 §A — workload identity resolution (capability-gated behavi
166
169
  expect(
167
170
  ['identity_unverified', 'identity_unresolvable', 'audience_mismatch', 'delegation_expired', 'sender_constraint_missing'],
168
171
  driver.describe('RFCS/0154 §A', 'failures use a closed reason vocabulary'),
169
- ).toContain(err?.code);
172
+ ).toContain(readErrorCode(r.json));
170
173
  });
171
174
 
172
175
  it('a resolution response carries no credential material', async () => {
@@ -24,6 +24,7 @@
24
24
  * @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
25
25
  */
26
26
  import { describe, it, expect } from 'vitest';
27
+ import { softSkip } from '../lib/soft-skip.js';
27
28
  import { randomUUID } from 'node:crypto';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -42,11 +43,11 @@ const asTenantB = { headers: { Authorization: `Bearer ${tenantBKey ?? ''}` } };
42
43
 
43
44
  describe('workspace-cross-tenant-isolation (black-box): a §C file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
44
45
  it('a file written by owner A is unreadable + un-enumerable by a second-tenant credential', async () => {
45
- if (!(await workspaceSupported())) return; // capability not advertised — skip
46
+ if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
46
47
  if (!tenantBKey) {
47
48
  // eslint-disable-next-line no-console
48
49
  console.warn('[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
49
- return;
50
+ return softSkip('blocked', '[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
50
51
  }
51
52
 
52
53
  const path = `wct-blackbox-${randomUUID()}.md`;
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { describe, it, expect } from 'vitest';
22
+ import { softSkip } from '../lib/soft-skip.js';
22
23
  import { driver } from '../lib/driver.js';
23
24
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
24
25
 
@@ -40,11 +41,11 @@ const SEAM_PATH = 'WCT1-SECRET.md';
40
41
 
41
42
  describe('workspace-cross-tenant-isolation: a workspace file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
42
43
  it('a file written under {tenant A, workspace A} is not readable under a different owner', async () => {
43
- if (!(await workspaceSupported())) return; // capability not advertised — skip
44
+ if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
44
45
 
45
46
  // Owner A writes a file.
46
47
  const put = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'put', path: SEAM_PATH, content: 'A-only secret body' });
47
- if (put.status === 404) return; // seam unwired — soft-skip the behavioral probe
48
+ if (put.status === 404) return softSkip('blocked', 'seam unwired — soft-skip the behavioral probe');
48
49
  expect(put.status, driver.describe('agent-workspace.md §C PUT', 'seam put MUST succeed for the owning workspace')).toBe(200);
49
50
 
50
51
  // A DIFFERENT workspace (same tenant) MUST NOT read it.
package/src/setup.ts CHANGED
@@ -30,6 +30,11 @@ import { setMultiAgentCapabilities } from './lib/multi-agent-capabilities.js';
30
30
  import { OtelCollector, setCollector } from './lib/otel-collector.js';
31
31
  import { McpFakeServer, setMcpFakeServer } from './lib/mcp-fake-server.js';
32
32
  import { A2AFakePeer, setA2AFakePeer } from './lib/a2a-fake-peer.js';
33
+ import { afterAll, afterEach, beforeAll, expect } from 'vitest';
34
+ import { basename } from 'node:path';
35
+ import { recordRequirement, journalLength, journalSince } from './lib/requirement-ledger.js';
36
+ import { requirementIdForFile, resolveFileRecord, type FileTestState } from './lib/scenario-disposition.js';
37
+ import { softSkipDisposition } from './lib/soft-skip.js';
33
38
  import type { DiscoveryPayload } from './lib/profiles.js';
34
39
 
35
40
  const SUITE_INIT_TIMEOUT_MS = 5_000;
@@ -181,7 +186,15 @@ async function maybeStartA2AFakePeer(): Promise<void> {
181
186
  }
182
187
  const portEnv = process.env.OPENWOP_A2A_FAKE_PEER_PORT;
183
188
  const requestedPort = portEnv ? Number(portEnv) : 0;
184
- const peer = new A2AFakePeer();
189
+ // Dual-era since 1.112.0. Order = card shape for a header-less GET (first
190
+ // wins); both eras are spoken on the RPC path. Default keeps the 0.3 card so
191
+ // today's 0.3 clients (which read `card.url`) keep working; set
192
+ // OPENWOP_A2A_FAKE_PEER_VERSIONS=1.0,0.3 to serve the 1.0 card by default.
193
+ const versionsEnv = process.env.OPENWOP_A2A_FAKE_PEER_VERSIONS;
194
+ const versions = versionsEnv
195
+ ? (versionsEnv.split(',').map((v) => v.trim()).filter((v) => v === '1.0' || v === '0.3') as Array<'1.0' | '0.3'>)
196
+ : undefined;
197
+ const peer = new A2AFakePeer(versions && versions.length > 0 ? { protocolVersions: versions } : undefined);
185
198
  await peer.start(requestedPort);
186
199
  setA2AFakePeer(peer);
187
200
  // eslint-disable-next-line no-console
@@ -195,3 +208,84 @@ await loadHostFixtures();
195
208
  await maybeStartOtelCollector();
196
209
  await maybeStartMcpFakeServer();
197
210
  await maybeStartA2AFakePeer();
211
+
212
+ // ─── RFC 0148 §A file-level disposition recording (S6, acceptance item 2) ─────
213
+ //
214
+ // Every scenario FILE records exactly one disposition for its own requirement
215
+ // id when it finishes. `afterEach` collects per-test states; `afterAll` folds
216
+ // them (fileDisposition) and records — `executed-fail` / `executed-pass` /
217
+ // `inapplicable` or `skipped` (when a behaviorGate in the file recorded that
218
+ // reason for the profile it gates on) / else `blocked` (an all-skipped file
219
+ // with no reason is an unclassified return, and §A resolves it to blocked).
220
+ // When the runner set OPENWOP_LEDGER_PATH the recording also lands in the JSONL
221
+ // sink, which is how `--certify` gets requirement-level dispositions instead of
222
+ // inferring them from per-file pass/fail/skip.
223
+ //
224
+ // Setup-file hooks apply to every file in the worker; state is keyed by file.
225
+ const _fileStates = new Map<string, FileTestState[]>();
226
+ const _fileAssertions = new Map<string, number>();
227
+ const _ledgerMarks = new Map<string, number>();
228
+ function _fileOf(task: { file?: { filepath?: string; name?: string } } | undefined): string | null {
229
+ const f = task?.file?.filepath ?? task?.file?.name;
230
+ return typeof f === 'string' && f.length > 0 ? basename(f) : null;
231
+ }
232
+ beforeAll(({}, suite) => {
233
+ // Mark the ledger journal BEFORE the file's tests run, so a behaviorGate
234
+ // decision made by the very first test is inside the file's window.
235
+ const s = suite as unknown as { filepath?: string; name?: string; file?: { filepath?: string; name?: string } };
236
+ const file = _fileOf({ file: s.file ?? s });
237
+ if (file !== null && !_ledgerMarks.has(file)) _ledgerMarks.set(file, journalLength());
238
+ });
239
+ afterEach(({ task }) => {
240
+ const file = _fileOf(task as { file?: { filepath?: string; name?: string } });
241
+ if (file === null) return;
242
+ if (!_ledgerMarks.has(file)) _ledgerMarks.set(file, 0);
243
+ const state = task.result?.state;
244
+ const arr = _fileStates.get(file) ?? [];
245
+ arr.push(state === 'pass' ? 'pass' : state === 'fail' ? 'fail' : 'skip');
246
+ _fileStates.set(file, arr);
247
+ // RFC 0148 §C assertionCount: how many `expect` calls this test actually made.
248
+ // A leg that early-returns from a gate makes zero, and a file of such legs is
249
+ // an `executed-pass` with assertionCount 0 — visible, and unclassified for a
250
+ // claimed floor.
251
+ let calls = 0;
252
+ try {
253
+ calls = (expect.getState() as { assertionCalls?: number }).assertionCalls ?? 0;
254
+ } catch {
255
+ /* no state — count 0 */
256
+ }
257
+ _fileAssertions.set(file, (_fileAssertions.get(file) ?? 0) + calls);
258
+ });
259
+ afterAll(({}, suite) => {
260
+ // vitest 4: the suite/file task is the SECOND argument. For a file-level
261
+ // afterAll registered from a setup file, `suite` is the File task itself.
262
+ const s = suite as unknown as { filepath?: string; name?: string; file?: { filepath?: string; name?: string } };
263
+ const file = _fileOf({ file: s.file ?? s });
264
+ if (file === null || !file.endsWith('.test.ts')) return;
265
+ const states = _fileStates.get(file) ?? [];
266
+ // Did a behaviorGate in this file record inapplicable/skipped for its profile?
267
+ const mark = _ledgerMarks.get(file) ?? 0;
268
+ const since = journalSince(mark);
269
+ const gateReason = since.some((e) => e.disposition === 'inapplicable')
270
+ ? 'inapplicable'
271
+ : since.some((e) => e.disposition === 'skipped')
272
+ ? 'skipped'
273
+ : undefined;
274
+ const assertionCount = _fileAssertions.get(file) ?? 0;
275
+ // RFC 0148 §A: a pass with zero assertions is an unclassified return. If the
276
+ // file said why (softSkip / seamAbsent / behaviorGate), that is its
277
+ // disposition; if it said nothing, the runner resolves it to `blocked` with
278
+ // the marker detail — never to a pass. Floors still REJECT that row, so the
279
+ // honest bundle row and the pressure to say why both survive. The rule is
280
+ // `resolveFileRecord` (pinned by conformance-execution-witness.test.ts).
281
+ const { disposition, detail } = resolveFileRecord(states, gateReason, assertionCount, softSkipDisposition(file));
282
+ try {
283
+ recordRequirement(requirementIdForFile(file), disposition, detail, { assertionCount });
284
+ } catch {
285
+ /* a scenario that recorded its own file id first wins; never fail the file for bookkeeping */
286
+ }
287
+ _fileStates.delete(file);
288
+ _fileAssertions.delete(file);
289
+ _ledgerMarks.delete(file);
290
+ });
291
+