@openwop/openwop-conformance 1.106.0 → 1.123.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 (154) 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/llm-cache-key-recipe.ts +6 -5
  24. package/src/lib/mcp-fake-server.ts +235 -72
  25. package/src/lib/profiles.ts +132 -19
  26. package/src/lib/requirement-ledger.ts +98 -1
  27. package/src/lib/requirement-registry.ts +47 -4
  28. package/src/lib/scenario-disposition.ts +291 -0
  29. package/src/lib/soft-skip.ts +89 -0
  30. package/src/lib/workflow-chain-expansion.ts +189 -0
  31. package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
  32. package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
  33. package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
  34. package/src/scenarios/a2a-peer-authority.test.ts +99 -0
  35. package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
  36. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
  37. package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
  38. package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
  39. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
  40. package/src/scenarios/approval-gate-flow.test.ts +5 -4
  41. package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
  42. package/src/scenarios/auth-scim-profile.test.ts +5 -4
  43. package/src/scenarios/authorization-fail-closed.test.ts +4 -3
  44. package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
  45. package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
  46. package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
  47. package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
  48. package/src/scenarios/commitment-fired.test.ts +3 -2
  49. package/src/scenarios/compensation-behavior.test.ts +63 -0
  50. package/src/scenarios/compensation-profile.test.ts +144 -1
  51. package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
  52. package/src/scenarios/conformance-execution-witness.test.ts +104 -0
  53. package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
  54. package/src/scenarios/credential-payload-redaction.test.ts +4 -3
  55. package/src/scenarios/credentials-capability-shape.test.ts +4 -3
  56. package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
  57. package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
  58. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
  59. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  60. package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
  61. package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
  62. package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
  63. package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
  64. package/src/scenarios/distillation-shape.test.ts +2 -1
  65. package/src/scenarios/distillation-stable-archive.test.ts +4 -3
  66. package/src/scenarios/distillation-token-budget.test.ts +4 -3
  67. package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
  68. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
  69. package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
  70. package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
  71. package/src/scenarios/feedback-record-and-list.test.ts +4 -3
  72. package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
  73. package/src/scenarios/fixtures-valid.test.ts +16 -18
  74. package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
  75. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
  76. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
  77. package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
  78. package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
  79. package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
  80. package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
  81. package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
  82. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
  83. package/src/scenarios/mcp-stateless-request.test.ts +77 -0
  84. package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
  85. package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
  86. package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
  87. package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
  88. package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
  89. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
  90. package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
  91. package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
  92. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
  93. package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
  94. package/src/scenarios/memory-injection-budget.test.ts +4 -3
  95. package/src/scenarios/metric-emission.test.ts +4 -3
  96. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  97. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
  98. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
  99. package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
  100. package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
  101. package/src/scenarios/multi-region-idempotency.test.ts +4 -3
  102. package/src/scenarios/normative-example-extraction.test.ts +242 -0
  103. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
  104. package/src/scenarios/oauth-capability-shape.test.ts +4 -3
  105. package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
  106. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
  107. package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
  108. package/src/scenarios/otel-emission-grpc.test.ts +4 -3
  109. package/src/scenarios/otel-emission.test.ts +4 -3
  110. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
  111. package/src/scenarios/otel-trace-propagation.test.ts +4 -3
  112. package/src/scenarios/pack-registry-isolation.test.ts +4 -3
  113. package/src/scenarios/pack-registry-publish.test.ts +65 -3
  114. package/src/scenarios/pack-registry.test.ts +22 -0
  115. package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
  116. package/src/scenarios/profileDerivation.test.ts +2 -1
  117. package/src/scenarios/rate-limit-envelope.test.ts +2 -1
  118. package/src/scenarios/registry-public.test.ts +6 -5
  119. package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
  120. package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
  121. package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
  122. package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
  123. package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
  124. package/src/scenarios/requirement-ledger.test.ts +29 -6
  125. package/src/scenarios/runner-ledger.test.ts +279 -0
  126. package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
  127. package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
  128. package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
  129. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  130. package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
  131. package/src/scenarios/stream-text-fixture.test.ts +5 -4
  132. package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
  133. package/src/scenarios/subrun-approval-gate.test.ts +4 -3
  134. package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
  135. package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
  136. package/src/scenarios/test-seam-unauthenticated.test.ts +61 -11
  137. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
  138. package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
  139. package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
  140. package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
  141. package/src/scenarios/tool-hooks-shape.test.ts +2 -1
  142. package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
  143. package/src/scenarios/version-fold.test.ts +5 -4
  144. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
  145. package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
  146. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
  147. package/src/scenarios/wasm-pack-load.test.ts +4 -3
  148. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
  149. package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
  150. package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
  151. package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
  152. package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
  153. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
  154. package/src/setup.ts +95 -1
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { describe, it, expect } from 'vitest';
25
+ import { softSkip } from '../lib/soft-skip.js';
25
26
  import { driver } from '../lib/driver.js';
26
27
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
27
28
 
@@ -36,9 +37,9 @@ async function deadLetterSupported(): Promise<boolean> {
36
37
 
37
38
  describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork-eligible (RFC 0053 §C)', () => {
38
39
  it('a retry-exhausted run emits run.dead_lettered with attempts', async () => {
39
- if (!(await deadLetterSupported())) return; // capability-gated
40
+ if (!(await deadLetterSupported())) return softSkip('inapplicable', 'capability-gated');
40
41
  const res = await driver.post('/v1/host/sample/deadletter/exhaust', { scenario: 'exhaust-retries' });
41
- if (res.status === 404) return; // seam unwired — soft-skip
42
+ if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
42
43
  const body = res.json as { event?: { type?: string; payload?: { attempts?: number; runId?: string } } } | undefined;
43
44
  expect(
44
45
  body?.event?.type,
@@ -51,9 +52,9 @@ describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork
51
52
  });
52
53
 
53
54
  it('the dead-lettered run is fork-eligible (RFC 0011)', async () => {
54
- if (!(await deadLetterSupported())) return; // capability-gated
55
+ if (!(await deadLetterSupported())) return softSkip('inapplicable', 'capability-gated');
55
56
  const res = await driver.post('/v1/host/sample/deadletter/exhaust', { scenario: 'fork-after-dead-letter' });
56
- if (res.status === 404) return; // seam unwired — soft-skip
57
+ if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
57
58
  const body = res.json as { forkEligible?: boolean } | undefined;
58
59
  expect(
59
60
  body?.forkEligible,
@@ -14,15 +14,16 @@
14
14
  */
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
+ import { softSkip } from '../lib/soft-skip.js';
17
18
  import { driver } from '../lib/driver.js';
18
19
  import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
19
20
 
20
21
  describe('distillation-index-roundtrip (RFC 0062 §B)', () => {
21
22
  it('an indexEmitted run updates a retrievable memory-index manifest', async () => {
22
23
  const cap = await readDistillationCap();
23
- if (cap?.supported !== true || cap?.indexEmitted !== true) return;
24
+ if (cap?.supported !== true || cap?.indexEmitted !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true || cap?.indexEmitted !== true` returned early');
24
25
  const res = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000, indexEmitted: true });
25
- if (res === null) return; // seam absent — soft-skip
26
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
26
27
  expect(
27
28
  res.body.indexUpdated === true || res.body.event?.distillation?.indexUpdated === true,
28
29
  driver.describe('RFC 0062 §B', 'an indexEmitted distillation MUST report updating the memory index'),
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { describe, it, expect } from 'vitest';
15
+ import { softSkip } from '../lib/soft-skip.js';
15
16
  import { driver } from '../lib/driver.js';
16
17
  import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
17
18
 
@@ -19,14 +20,14 @@ const CANARY = 'sk-canary-rfc0062-do-not-leak-qrs456';
19
20
 
20
21
  describe('distillation-secret-carryforward (RFC 0062 §B)', () => {
21
22
  it('a redacted secret in source memory never appears in the distilled output', async () => {
22
- if ((await readDistillationCap())?.supported !== true) return;
23
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
23
24
  const res = await invokeDistill({
24
25
  memoryRef: 'conformance-distill',
25
26
  tokenBudget: 8000,
26
27
  includeSecretCanary: true,
27
28
  sources: [{ content: `notes with embedded secret ${CANARY}` }],
28
29
  });
29
- if (res === null) return; // seam absent — soft-skip
30
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
30
31
  expect(
31
32
  JSON.stringify(res.body).includes(CANARY),
32
33
  driver.describe('RFC 0062 §B', 'SR-1 carry-forward: a redacted secret MUST NOT re-appear in the archive or memory.compacted event'),
@@ -12,13 +12,14 @@
12
12
  */
13
13
 
14
14
  import { describe, it, expect } from 'vitest';
15
+ import { softSkip } from '../lib/soft-skip.js';
15
16
  import { driver } from '../lib/driver.js';
16
17
  import { readDistillationCap } from '../lib/distillation.js';
17
18
 
18
19
  describe('distillation-shape: advertisement (RFC 0062 §A)', () => {
19
20
  it('capabilities.memory.distillation is absent or a well-formed object', async () => {
20
21
  const cap = await readDistillationCap();
21
- if (cap === null) return; // not advertised — valid
22
+ if (cap === null) return softSkip('inapplicable', 'not advertised — valid');
22
23
  expect(
23
24
  typeof cap.supported,
24
25
  driver.describe('capabilities.schema.json §memory.distillation', 'distillation.supported MUST be a boolean when the block is present'),
@@ -10,21 +10,22 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
+ import { softSkip } from '../lib/soft-skip.js';
13
14
  import { driver } from '../lib/driver.js';
14
15
  import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
15
16
 
16
17
  describe('distillation-stable-archive (RFC 0062 §B)', () => {
17
18
  it('identical sources + budget produce an identical archive checksum', async () => {
18
- if ((await readDistillationCap())?.supported !== true) return;
19
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
19
20
  const req = {
20
21
  memoryRef: 'conformance-distill',
21
22
  tokenBudget: 8000,
22
23
  sources: ['s1', 's2', 's3'],
23
24
  };
24
25
  const a = await invokeDistill(req);
25
- if (a === null) return; // seam absent — soft-skip
26
+ if (a === null) return softSkip('blocked', 'seam absent — soft-skip');
26
27
  const b = await invokeDistill(req);
27
- if (b === null) return;
28
+ if (b === null) return softSkip('blocked', 'precondition not met — `b === null` returned early (seam, prior step, or fixture unavailable)');
28
29
  expect(
29
30
  typeof a.body.archiveChecksum === 'string' && (a.body.archiveChecksum as string).length > 0,
30
31
  driver.describe('RFC 0062 §B', 'a distillation run MUST produce a non-empty archive checksum'),
@@ -11,15 +11,16 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
16
17
 
17
18
  describe('distillation-token-budget (RFC 0062 §B)', () => {
18
19
  it('within budget tokensUsed ≤ tokenBudget; an un-meetable budget fails atomically', async () => {
19
- if ((await readDistillationCap())?.supported !== true) return;
20
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
20
21
 
21
22
  const ok = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000 });
22
- if (ok === null) return; // seam absent — soft-skip
23
+ if (ok === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  const dist = ok.body.event?.distillation ?? {};
24
25
  expect(
25
26
  typeof dist.tokenBudget === 'number' && typeof dist.tokensUsed === 'number',
@@ -32,7 +33,7 @@ describe('distillation-token-budget (RFC 0062 §B)', () => {
32
33
 
33
34
  // A budget too small to distill the corpus MUST fail closed, no partial archive.
34
35
  const tooSmall = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 1 });
35
- if (tooSmall === null) return;
36
+ if (tooSmall === null) return softSkip('blocked', 'precondition not met — `tooSmall === null` returned early (seam, prior step, or fixture unavailable)');
36
37
  expect(
37
38
  tooSmall.status >= 400 && tooSmall.body.error === 'token_budget_exceeded',
38
39
  driver.describe('RFC 0062 §B', 'an un-meetable budget MUST fail with token_budget_exceeded'),
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { describe, it, expect } from 'vitest';
12
+ import { softSkip } from '../lib/soft-skip.js';
12
13
  import { driver } from '../lib/driver.js';
13
14
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
14
15
 
@@ -17,14 +18,14 @@ const CANARY = 'sk-canary-rfc0056-do-not-leak-abc123';
17
18
  describe('feedback-correction-redaction (RFC 0056 §E)', () => {
18
19
  it('secret-shaped material in correction/note is redacted in the annotation list', async () => {
19
20
  const cap = await readFeedbackCap();
20
- if (cap?.supported !== true) return;
21
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
21
22
  const runId = await seedRun('feedback-redact');
22
- if (!runId) return;
23
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
23
24
  const post = await driver.post(`/v1/runs/${runId}/annotations`, {
24
25
  signal: { kind: 'correction', correction: `please use ${CANARY}` },
25
26
  note: CANARY,
26
27
  });
27
- if (post.status === 501 || post.status === 404) return;
28
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
28
29
  expect(post.status).toBe(201);
29
30
  const list = await driver.get(`/v1/runs/${runId}/annotations`);
30
31
  expect(
@@ -13,17 +13,18 @@
13
13
  */
14
14
 
15
15
  import { describe, it, expect } from 'vitest';
16
+ import { softSkip } from '../lib/soft-skip.js';
16
17
  import { driver } from '../lib/driver.js';
17
18
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
18
19
 
19
20
  describe('feedback-cross-tenant-isolation (RFC 0056 §E)', () => {
20
21
  it('a run\'s annotation list contains only that run\'s annotations', async () => {
21
22
  const cap = await readFeedbackCap();
22
- if (cap?.supported !== true) return;
23
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
23
24
  const runId = await seedRun('feedback-cti');
24
- if (!runId) return;
25
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
25
26
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'label', label: 'cti-probe' } });
26
- if (post.status === 501 || post.status === 404) return;
27
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
27
28
  expect(post.status).toBe(201);
28
29
  const list = await driver.get(`/v1/runs/${runId}/annotations`);
29
30
  const ann = (list.json as { annotations?: Array<{ target?: { runId?: string } }> } | undefined)?.annotations ?? [];
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
+ import { softSkip } from '../lib/soft-skip.js';
11
12
  import { driver } from '../lib/driver.js';
12
13
  import { pollUntilTerminal } from '../lib/polling.js';
13
14
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
@@ -15,11 +16,11 @@ import { readFeedbackCap, seedRun } from '../lib/feedback.js';
15
16
  describe('feedback-fork-not-copied (RFC 0056 §D)', () => {
16
17
  it('a fork of an annotated run starts with zero annotations', async () => {
17
18
  const cap = await readFeedbackCap();
18
- if (cap?.supported !== true) return;
19
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
19
20
  const runId = await seedRun('feedback-fork');
20
- if (!runId) return;
21
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
21
22
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'flag' } });
22
- if (post.status === 501 || post.status === 404) return;
23
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
23
24
  expect(post.status).toBe(201);
24
25
  try {
25
26
  await pollUntilTerminal(runId, { timeoutMs: 10_000 });
@@ -27,9 +28,9 @@ describe('feedback-fork-not-copied (RFC 0056 §D)', () => {
27
28
  return;
28
29
  }
29
30
  const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'branch' });
30
- if (fork.status !== 200 && fork.status !== 201) return; // fork unsupported — soft-skip
31
+ if (fork.status !== 200 && fork.status !== 201) return softSkip('inapplicable', 'fork unsupported — soft-skip (fork.status !== 200 && fork.status !== 201)');
31
32
  const forkId = (fork.json as { runId?: string } | undefined)?.runId;
32
- if (!forkId) return;
33
+ if (!forkId) return softSkip('blocked', 'precondition not met — `!forkId` returned early (seam, prior step, or fixture unavailable)');
33
34
  const list = await driver.get(`/v1/runs/${forkId}/annotations`);
34
35
  const ann = (list.json as { annotations?: unknown[] } | undefined)?.annotations ?? [];
35
36
  expect(
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
+ import { softSkip } from '../lib/soft-skip.js';
10
11
  import { driver } from '../lib/driver.js';
11
12
  import { pollUntilTerminal } from '../lib/polling.js';
12
13
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
@@ -14,16 +15,16 @@ import { readFeedbackCap, seedRun } from '../lib/feedback.js';
14
15
  describe('feedback-on-terminal-run (RFC 0056 §C)', () => {
15
16
  it('annotating a terminal run is accepted', async () => {
16
17
  const cap = await readFeedbackCap();
17
- if (cap?.supported !== true) return;
18
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
18
19
  const runId = await seedRun('feedback-terminal');
19
- if (!runId) return;
20
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
20
21
  try {
21
22
  await pollUntilTerminal(runId, { timeoutMs: 10_000 });
22
23
  } catch {
23
24
  return; // run didn't reach terminal in time — soft-skip
24
25
  }
25
26
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'flag' }, note: 'post-hoc review' });
26
- if (post.status === 501 || post.status === 404) return;
27
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
27
28
  expect(
28
29
  post.status,
29
30
  driver.describe('RFC 0056 §C', 'a host MUST accept an annotation on a terminal run'),
@@ -7,17 +7,18 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
+ import { softSkip } from '../lib/soft-skip.js';
10
11
  import { driver } from '../lib/driver.js';
11
12
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
12
13
 
13
14
  describe('feedback-record-and-list (RFC 0056 §C)', () => {
14
15
  it('POST an annotation then GET returns it', async () => {
15
16
  const cap = await readFeedbackCap();
16
- if (cap?.supported !== true) return;
17
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
17
18
  const runId = await seedRun('feedback-rl');
18
- if (!runId) return;
19
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
19
20
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'rating', rating: 5 } });
20
- if (post.status === 501 || post.status === 404) return;
21
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
21
22
  expect(
22
23
  post.status,
23
24
  driver.describe('RFC 0056 §C', 'POST annotation returns 201 with the persisted annotation'),
@@ -11,17 +11,18 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readFeedbackCap } from '../lib/feedback.js';
16
17
 
17
18
  describe('feedback-unsupported-501 (RFC 0056 §C)', () => {
18
19
  it('POST annotations returns 501 capability_not_provided when feedback is unadvertised', async () => {
19
20
  const cap = await readFeedbackCap();
20
- if (cap?.supported === true) return; // host supports feedback — 501 N/A
21
+ if (cap?.supported === true) return softSkip('inapplicable', 'host supports feedback — 501 N/A (cap?.supported === true)');
21
22
  const res = await driver.post('/v1/runs/probe-run-rfc0056/annotations', {
22
23
  signal: { kind: 'flag' },
23
24
  });
24
- if (res.status === 404 || res.status === 405) return; // route absent — host predates RFC 0056
25
+ if (res.status === 404 || res.status === 405) return softSkip('blocked', 'route absent — host predates RFC 0056');
25
26
  expect(
26
27
  res.status,
27
28
  driver.describe('rest-endpoints.md / RFC 0056 §C', 'unadvertised feedback MUST return 501, not 404'),
@@ -24,24 +24,22 @@ const PROMPT_TEMPLATE_SCHEMA_PATH = join(SCHEMAS_DIR, 'prompt-template.schema.js
24
24
  describe('fixtures: workflow-definition schema validity', () => {
25
25
  const ajv = new Ajv2020({ allErrors: true, strict: false });
26
26
  addFormats(ajv);
27
- // Pre-load peer schemas that workflow-definition cross-`$ref`s:
28
- // - agent-ref.schema.json — `WorkflowNode.agent` (Phase 1 multi-agent)
29
- // - prompt-ref.schema.json — `WorkflowDefinition.defaults.promptRefs.*`
30
- // (RFC 0029 §B resolution-chain layer 3)
31
- // - prompt-kind.schema.json — transitively referenced by prompt-ref's
32
- // object form when validating PromptRef variants
33
- // Register each under both the canonical $id and the relative file
34
- // name so Ajv resolves either way the host schema spelled the ref.
35
- const agentRefSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'agent-ref.schema.json'), 'utf8'));
36
- const promptRefSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'prompt-ref.schema.json'), 'utf8'));
37
- const promptKindSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'prompt-kind.schema.json'), 'utf8'));
38
- ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
39
- ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
40
- ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
41
- ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
42
- ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
43
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
44
- const validate = ajv.compile(schema);
27
+ // Pre-load EVERY schema in the corpus (S14, 2026-08-16). This used to be a
28
+ // fixed list of the peer schemas workflow-definition cross-`$ref`s (agent-ref,
29
+ // prompt-ref, prompt-kind) — and when `compensation-policy.schema.json` became
30
+ // the fourth cross-file `$ref` (#1009), `ajv.compile` threw at describe time
31
+ // here and in every downstream runner that resolves the sibling corpus with a
32
+ // pinned suite. A fixed list is a claim about the schema graph that nothing
33
+ // keeps true; enumerating the directory is not. Each schema is registered
34
+ // under both its bare file name and `./<name>` so Ajv resolves either spelling
35
+ // of a relative ref (canonical `$id`s resolve on their own).
36
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
37
+ const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as { $id?: string };
38
+ ajv.addSchema(s, file);
39
+ ajv.addSchema(s, `./${file}`);
40
+ }
41
+ // The root schema is already registered under its $id above; compile via getSchema.
42
+ const validate = ajv.getSchema('workflow-definition.schema.json') ?? ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
45
43
 
46
44
  const files = readdirSync(FIXTURES_DIR)
47
45
  .filter((f) => f.endsWith('.json')) // top-level only — sub-dirs hold non-workflow fixtures
@@ -11,13 +11,14 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readHeartbeatCap } from '../lib/heartbeat.js';
16
17
 
17
18
  describe('heartbeat-capability-shape: advertisement (RFC 0060 §A)', () => {
18
19
  it('capabilities.heartbeat is absent or a well-formed object', async () => {
19
20
  const cap = await readHeartbeatCap();
20
- if (cap === null) return; // not advertised — valid
21
+ if (cap === null) return softSkip('inapplicable', 'not advertised — valid');
21
22
  expect(
22
23
  typeof cap.supported,
23
24
  driver.describe('capabilities.schema.json §heartbeat', 'heartbeat.supported MUST be a boolean when the block is present'),
@@ -10,16 +10,17 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
+ import { softSkip } from '../lib/soft-skip.js';
13
14
  import { driver } from '../lib/driver.js';
14
15
  import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
15
16
 
16
17
  describe('heartbeat-fires-once-per-tick (RFC 0060 §B.1)', () => {
17
18
  it('one tick emits exactly one heartbeat.evaluated', async () => {
18
- if (!heartbeatSupported(await readHeartbeatCap())) return;
19
+ if (!heartbeatSupported(await readHeartbeatCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!heartbeatSupported(await readHeartbeatCap())` returned early');
19
20
  const res = await tickHeartbeat({ heartbeatId: 'conformance-hb', observedState: { n: 0 } });
20
- if (res === null) return; // seam absent — soft-skip
21
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
21
22
  const evaluated = (res.json as { evaluated?: unknown[] } | undefined)?.evaluated;
22
- if (!Array.isArray(evaluated)) return; // host doesn't surface per-tick events on the seam
23
+ if (!Array.isArray(evaluated)) return softSkip('blocked', 'host doesn\'t surface per-tick events on the seam');
23
24
  expect(
24
25
  evaluated.length,
25
26
  driver.describe('RFC 0060 §B.1', 'a single tick MUST emit exactly one heartbeat.evaluated'),
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
16
17
 
@@ -21,20 +22,20 @@ function changedCount(json: unknown): number | null {
21
22
 
22
23
  describe('heartbeat-idempotent-no-spam (RFC 0060 §B.5)', () => {
23
24
  it('an unchanged tick enqueues nothing; only a transition does', async () => {
24
- if (!heartbeatSupported(await readHeartbeatCap())) return;
25
+ if (!heartbeatSupported(await readHeartbeatCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!heartbeatSupported(await readHeartbeatCap())` returned early');
25
26
  const hb = 'conformance-hb-spam';
26
27
  const first = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 0 } });
27
- if (first === null) return; // seam absent — soft-skip
28
+ if (first === null) return softSkip('blocked', 'seam absent — soft-skip');
28
29
  const second = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 0 } });
29
- if (second === null) return;
30
+ if (second === null) return softSkip('blocked', 'precondition not met — `second === null` returned early (seam, prior step, or fixture unavailable)');
30
31
  const unchanged = changedCount(second.json);
31
- if (unchanged === null) return; // host doesn't surface stateChanged on the seam
32
+ if (unchanged === null) return softSkip('blocked', 'host doesn\'t surface stateChanged on the seam');
32
33
  expect(
33
34
  unchanged,
34
35
  driver.describe('RFC 0060 §B.5', 'an unchanged tick MUST NOT emit heartbeat.stateChanged'),
35
36
  ).toBe(0);
36
37
  const transition = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 3 } });
37
- if (transition === null) return;
38
+ if (transition === null) return softSkip('blocked', 'precondition not met — `transition === null` returned early (seam, prior step, or fixture unavailable)');
38
39
  expect(
39
40
  changedCount(transition.json),
40
41
  driver.describe('RFC 0060 §B.5', 'a transitioning tick MUST emit exactly one heartbeat.stateChanged'),
@@ -10,18 +10,19 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
+ import { softSkip } from '../lib/soft-skip.js';
13
14
  import { driver } from '../lib/driver.js';
14
15
  import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
15
16
 
16
17
  describe('heartbeat-runtime-bound (RFC 0060 §B.2)', () => {
17
18
  it('an over-budget predicate is reported as timeout', async () => {
18
- if (!heartbeatSupported(await readHeartbeatCap())) return;
19
+ if (!heartbeatSupported(await readHeartbeatCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!heartbeatSupported(await readHeartbeatCap())` returned early');
19
20
  // `simulateSlowMs` is a host-seam hint asking the predicate to overrun
20
21
  // its maxRuntimeMs budget; hosts not honoring it surface no `timeout`.
21
22
  const res = await tickHeartbeat({ heartbeatId: 'conformance-hb-slow', observedState: {}, simulateSlowMs: 60_000 });
22
- if (res === null) return; // seam absent — soft-skip
23
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  const evaluated = (res.json as { evaluated?: Array<{ status?: unknown }> } | undefined)?.evaluated;
24
- if (!Array.isArray(evaluated) || evaluated.length === 0) return; // host doesn't surface per-tick events
25
+ if (!Array.isArray(evaluated) || evaluated.length === 0) return softSkip('blocked', 'host doesn\'t surface per-tick events (!Array.isArray(evaluated) || evaluated.length === 0)');
25
26
  expect(
26
27
  evaluated.every((e) => e.status === 'timeout'),
27
28
  driver.describe('RFC 0060 §B.2', 'an over-budget predicate MUST be terminated and reported status:"timeout"'),