@openwop/openwop-conformance 2.42.5 → 2.42.7

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 (123) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +3 -3
  3. package/coverage.md +1 -1
  4. package/dist/lib/soft-skip.js +13 -0
  5. package/dist/spec-artifacts.lock.json +2 -2
  6. package/package.json +2 -2
  7. package/requirements.json +430 -162
  8. package/scenario-majors.json +16 -3
  9. package/schemas/CORPUS-STAMP.json +64 -63
  10. package/src/lib/audit-checkpoint.ts +72 -0
  11. package/src/lib/era2-seed.ts +30 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/soft-skip.ts +14 -0
  14. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  15. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  16. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  17. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  18. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  19. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  20. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  21. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  22. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  25. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  26. package/src/scenarios/append-ordering.test.ts +26 -21
  27. package/src/scenarios/artifact-auth.test.ts +3 -1
  28. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  29. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  30. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  31. package/src/scenarios/auth-mtls.test.ts +7 -6
  32. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  33. package/src/scenarios/auth-subject-link.test.ts +19 -18
  34. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  35. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  36. package/src/scenarios/compensation-behavior.test.ts +10 -5
  37. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  38. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  39. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  40. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  41. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  42. package/src/scenarios/cost-attribution.test.ts +3 -1
  43. package/src/scenarios/debugBundle.test.ts +12 -4
  44. package/src/scenarios/discovery.test.ts +6 -2
  45. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  46. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  47. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  48. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  49. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  50. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  51. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  52. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  53. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  54. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  55. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  56. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  57. package/src/scenarios/eventOrdering.test.ts +17 -10
  58. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  59. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  60. package/src/scenarios/highConcurrency.test.ts +9 -9
  61. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  62. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  63. package/src/scenarios/idempotency.test.ts +6 -6
  64. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  65. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  66. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  67. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  68. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  69. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  70. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  71. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  72. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  73. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  74. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  75. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  76. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  77. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  78. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  79. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  80. package/src/scenarios/pack-registry.test.ts +3 -2
  81. package/src/scenarios/pause-resume.test.ts +30 -11
  82. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  83. package/src/scenarios/provider-usage.test.ts +2 -2
  84. package/src/scenarios/redaction.test.ts +7 -5
  85. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  86. package/src/scenarios/registry-public.test.ts +5 -1
  87. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  88. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  89. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  90. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  91. package/src/scenarios/replayDeterminism.test.ts +9 -4
  92. package/src/scenarios/runner-ledger.test.ts +4 -0
  93. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  94. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  95. package/src/scenarios/streamReconnect.test.ts +2 -2
  96. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  97. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  98. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  99. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  100. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  101. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  102. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  103. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +39 -24
  104. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  105. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  106. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  107. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  108. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  109. package/src/scenarios/v2-min-client-version.test.ts +2 -0
  110. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  111. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  112. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  113. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  114. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  115. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  116. package/src/scenarios/verifier-gating.test.ts +2 -2
  117. package/src/scenarios/version-fold.test.ts +2 -2
  118. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  119. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  120. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  121. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  122. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  123. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -48,7 +48,7 @@ import { CANARIES, CANARY_MARKER, getCanary } from '../lib/canaries.js';
48
48
  import { pollUntilTerminal } from '../lib/polling.js';
49
49
  import { isFixtureAdvertised } from '../lib/fixtures.js';
50
50
  import { req } from '../lib/requirement-ids.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
 
53
53
  const NOOP_WORKFLOW_ID = 'conformance-noop';
54
54
  const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
@@ -77,8 +77,9 @@ describe.skipIf(SKIP_NO_NOOP)('redaction-adversarial: workflow input canaries',
77
77
  `/v1/runs/${encodeURIComponent(runId)}/events/poll`,
78
78
  );
79
79
  if (events.status !== 200) {
80
- // Host doesn't expose polling; SSE is the alternative. Skip-equivalent.
81
- return softSkip('blocked', 'precondition not met — `events.status !== 200` returned early (Host doesn\'t expose polling; SSE is the alternative. Skip-equivalent.) (seam, prior step, or fixture unavailable)');
80
+ // events/poll is a core endpoint (rest-endpoints.md); without it the
81
+ // canary check below never runs, so the leg is blocked, not passed.
82
+ return blockedDespiteAssertions('GET /v1/runs/{runId}/events/poll did not answer 200 — the canary non-disclosure is unobserved');
82
83
  }
83
84
 
84
85
  const eventsText = events.text;
@@ -139,7 +140,7 @@ describe.skipIf(SKIP_NO_NOOP)('redaction-adversarial: bearer-shaped strings roun
139
140
  const events = await driver.get(
140
141
  `/v1/runs/${encodeURIComponent(runId)}/events/poll`,
141
142
  );
142
- if (events.status !== 200) return softSkip('blocked', 'precondition not met — `events.status !== 200` returned early (seam, prior step, or fixture unavailable)');
143
+ if (events.status !== 200) return blockedDespiteAssertions('GET /v1/runs/{runId}/events/poll did not answer 200 — the canary non-disclosure is unobserved');
143
144
 
144
145
  expect(events.text.includes(canary), req('openwop.it.redactionAdversarial.anthropic-shaped-canary-embedded-in-non-credential-input-must-not-appear-verbati',
145
146
  'capabilities.md §"Secrets" + NFR-7',
@@ -215,7 +215,11 @@ describe('registry-public: tarball + signature + Ed25519 verify roundtrip', () =
215
215
  // `method=ed25519` — the simpler path. Extending to `manual` would
216
216
  // require the tarball extractor from registry/scripts/verify-
217
217
  // signatures.mjs which is intentionally out of scope here.
218
- if (method !== 'ed25519') return softSkip('blocked', 'precondition not met — `method !== \'ed25519\'` returned early (seam, prior step, or fixture unavailable)');
218
+ // partial-witness-ok: tarball, signature, public key and SRI were asserted
219
+ // above. `manual` is a conforming signing method whose pack.json-in-tarball
220
+ // verification this scenario does not implement, a suite limitation, not a
221
+ // host shortfall.
222
+ if (method !== 'ed25519') return softSkip('inapplicable', `${PACK_NAME} is signed method=manual; this scenario verifies only method=ed25519 signatures`);
219
223
 
220
224
  const verified = cryptoVerify(null, tarball.bytes, publicKey, sig.bytes);
221
225
  expect(verified, req('openwop.it.registry-public.tarball-sig-public-key-all-retrievable-sri-matches-ed25519-verifies-for', 'spec/v1/registry-operations.md', `Ed25519 signature over ${PACK_NAME}@${PACK_VERSION}.tgz MUST verify against ${manifest.signing!.keyId}`))
@@ -45,7 +45,7 @@
45
45
  */
46
46
 
47
47
  import { describe, it, expect } from 'vitest';
48
- import { softSkip } from '../lib/soft-skip.js';
48
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
49
49
  import { driver } from '../lib/driver.js';
50
50
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
51
51
  import { req } from '../lib/requirement-ids.js';
@@ -246,9 +246,12 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
246
246
  workflowId: 'conformance-phase4-replay-divergence',
247
247
  });
248
248
  if (createRes.status === 404 || createRes.status === 422) {
249
- softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
250
- ctx.skip(); // fixture not advertised
251
- return softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
249
+ // The host advertises refusalDivergenceEmission and its mock seam took the
250
+ // program (asserted above), but the fixture run would not start: the
251
+ // requirement was never observed. ctx.skip() throws, so this note is the
252
+ // row's disposition (`blocked`), exactly as before 2026-09-27.
253
+ blockedDespiteAssertions('the mock-AI seam accepted the program but POST /v1/runs of conformance-phase4-replay-divergence answered 404/422 — the fixture is not seeded, so no divergence was driven');
254
+ return ctx.skip();
252
255
  }
253
256
  expect(createRes.status).toBe(201);
254
257
  const sourceRunId = (createRes.json as { runId: string }).runId;
@@ -336,9 +339,9 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
336
339
  workflowId: 'conformance-phase4-replay-divergence',
337
340
  });
338
341
  if (createRes.status === 404 || createRes.status === 422) {
339
- softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
340
- ctx.skip();
341
- return softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
342
+ // As in the first case: ctx.skip() throws, so the note is the disposition.
343
+ blockedDespiteAssertions('the mock-AI seam accepted the program but POST /v1/runs of conformance-phase4-replay-divergence answered 404/422 — the fixture is not seeded, so no divergence was driven');
344
+ return ctx.skip();
342
345
  }
343
346
  expect(createRes.status).toBe(201);
344
347
  const sourceRunId = (createRes.json as { runId: string }).runId;
@@ -283,8 +283,8 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
283
283
  'host does not advertise `replay.supported: true`, so a replay fork cannot occur and this MUST NOT '
284
284
  + 'has nothing to constrain on this host',
285
285
  );
286
- ctx.skip();
287
- return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `replayCap?.supported !== true` returned early');
286
+ // recordRequirement above carries the disposition; ctx.skip() throws.
287
+ return ctx.skip();
288
288
  }
289
289
 
290
290
  // ── LEG 2 — THE MUST NOT. A replay fork re-emits; it must not deliver. ───
@@ -308,8 +308,8 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
308
308
  `replay fork returned ${replay.status} — the re-emission this requirement is stated over never happened, `
309
309
  + 'so the absence of deliveries below would prove nothing',
310
310
  );
311
- ctx.skip();
312
- return softSkip('blocked', 'precondition not met — `forkDeclined(replay.status, \'fanout-suppression replay fork\')` returned early (seam, prior step, or fixture unavailable)');
311
+ // recordRequirement above carries the disposition; ctx.skip() throws.
312
+ return ctx.skip();
313
313
  }
314
314
  expect(replay.status, req('openwop.it.replay-fanout-suppression.delivers-for-a-live-run-suppresses-for-a-replay-fork-and-delivers-again-for-a-br', 'webhooks.md §"Register"', 'replay fork should be accepted')).toBe(201);
315
315
  const replayRunId = (replay.json as { runId: string }).runId;
@@ -373,6 +373,9 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
373
373
  });
374
374
  if (branch.status === 501 || branch.status === 400) {
375
375
  // branch not offered on this range — leg 2 still stands on its own.
376
+ // partial-witness-ok: the MUST NOT (leg 2) was observed and recorded
377
+ // executed-pass above; leg 3 only pins the out-of-scope boundary, on a
378
+ // fork mode the host may not offer.
376
379
  return softSkip('blocked', 'precondition not met — `branch.status === 501 || branch.status === 400` returned early (branch not offered on this range — leg 2 still stands on its own.) (seam, prior step, or fixture unavailable)');
377
380
  }
378
381
  expect(branch.status, req('openwop.it.replay-fanout-suppression.delivers-for-a-live-run-suppresses-for-a-replay-fork-and-delivers-again-for-a-br', 'replay.md §"Host-initiated fan-out is an external effect"', 'branch fork should be accepted')).toBe(201);
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { driver } from '../lib/driver.js';
33
- import { softSkip } from '../lib/soft-skip.js';
33
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
34
34
  import { forkDeclined } from '../lib/fork-availability.js';
35
35
  import { pollUntilTerminal } from '../lib/polling.js';
36
36
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -246,7 +246,7 @@ describe.skipIf(SKIP_NO_MULTI)(
246
246
  );
247
247
  if (forkDeclined(fork2.status, 'arbitrary-event fork 2')) {
248
248
  ctx.skip();
249
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork2.status, \'arbitrary-event fork 2\')` returned early (seam, prior step, or fixture unavailable)');
249
+ return blockedDespiteAssertions('arbitrary-event fork 2 was declined — fork determinism is unobserved');
250
250
  }
251
251
  expect(fork2.status).toBe(201);
252
252
  const fork2Id = (fork2.json as { runId: string }).runId;
@@ -55,7 +55,7 @@ import { readFileSync } from 'node:fs';
55
55
  import { join } from 'node:path';
56
56
  import { V1_DIR } from '../lib/paths.js';
57
57
  import { req } from '../lib/requirement-ids.js';
58
- import { softSkip } from '../lib/soft-skip.js';
58
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
59
59
 
60
60
  const WORKFLOW_ID = 'conformance-replay-side-effect';
61
61
  const EFFECT_NODE = 'effect';
@@ -178,7 +178,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
178
178
  });
179
179
  if (forkDeclined(fork.status, 'side-effect-suppression replay fork')) {
180
180
  ctx.skip();
181
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork.status, \'side-effect-suppression replay fork\')` returned early (seam, prior step, or fixture unavailable)');
181
+ return blockedDespiteAssertions('the replay fork was declined — side-effect suppression is unobserved');
182
182
  }
183
183
  expect(fork.status, req('openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne', 'replay.md §"Side-effect suppression in replay" requirement 1', 'fork should be accepted')).toBe(201);
184
184
  const forkRunId = (fork.json as { runId: string }).runId;
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
20
  import { driver } from '../lib/driver.js';
21
- import { softSkip } from '../lib/soft-skip.js';
21
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
22
22
  import { forkDeclined } from '../lib/fork-availability.js';
23
23
  import { pollUntilTerminal } from '../lib/polling.js';
24
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -121,7 +121,10 @@ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: same fromSeq + same workflow
121
121
  mode: 'replay',
122
122
  fromSeq: 0,
123
123
  });
124
- if (forkDeclined(fork2.status, 'determinism fork 2')) return softSkip('blocked', 'precondition not met — `forkDeclined(fork2.status, \'determinism fork 2\')` returned early (seam, prior step, or fixture unavailable)');
124
+ // Fork 1 of the same run was accepted (asserted above), so a decline here
125
+ // leaves the determinism comparison unobserved on a host that can fork:
126
+ // `blocked`, not the partial-witness pass a plain softSkip records.
127
+ if (forkDeclined(fork2.status, 'determinism fork 2')) return blockedDespiteAssertions(`the second replay fork of ${originalRunId} was declined (${fork2.status}) after the first was accepted — there is no second event list to compare`);
125
128
  expect(fork2.status).toBe(201);
126
129
  const fork2Id = (fork2.json as { runId: string }).runId;
127
130
  await pollUntilTerminal(fork2Id, { timeoutMs: 10_000 });
@@ -129,11 +132,13 @@ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: same fromSeq + same workflow
129
132
  // Phase 4: fetch both fork event streams.
130
133
  const fork1Events = await driver.get(`/v1/runs/${encodeURIComponent(fork1Id)}/events/poll`);
131
134
  const fork2Events = await driver.get(`/v1/runs/${encodeURIComponent(fork2Id)}/events/poll`);
132
- if (fork1Events.status !== 200 || fork2Events.status !== 200) return softSkip('blocked', 'precondition not met — `fork1Events.status !== 200 || fork2Events.status !== 200` returned early (seam, prior step, or fixture unavailable)');
135
+ // rest-endpoints.md §"Required endpoints": /events/poll is a MUST, so a
136
+ // conforming host always makes the two event lists readable.
137
+ if (fork1Events.status !== 200 || fork2Events.status !== 200) return blockedDespiteAssertions(`GET /events/poll for the forks answered ${fork1Events.status} / ${fork2Events.status} — the event lists were never compared`);
133
138
 
134
139
  const fork1Body = fork1Events.json as { events?: RawEvent[] };
135
140
  const fork2Body = fork2Events.json as { events?: RawEvent[] };
136
- if (!fork1Body.events || !fork2Body.events) return softSkip('blocked', 'precondition not met — `!fork1Body.events || !fork2Body.events` returned early (seam, prior step, or fixture unavailable)');
141
+ if (!fork1Body.events || !fork2Body.events) return blockedDespiteAssertions('a fork /events/poll body carried no events[] — the event lists were never compared');
137
142
 
138
143
  // Phase 5: assert structural identity (modulo timestamps + IDs).
139
144
  expect(fork1Body.events.length, req('openwop.it.replayDeterminism.two-replay-forks-of-the-same-point-produce-structurally-identical-event-lists',
@@ -251,10 +251,14 @@ describe('RFC 0148 §A (S6) — the runner derivation', () => {
251
251
  describe('RFC 0148 §A — softSkip notes the reason for an early return, per file', () => {
252
252
  it('notes are keyed to the current test file, worst-first when mixed, and read back with joined reasons', () => {
253
253
  resetSoftSkips();
254
+ // partial-witness-ok: not a skip — this leg unit-tests softSkip itself, and
255
+ // resetSoftSkips() below clears every note it writes before the leg ends.
254
256
  expect(softSkip('inapplicable', 'host does not advertise X'), req('openwop.it.runner-ledger.notes-are-keyed-to-the-current-test-file-worst-first-when-mixed-and-read-back-wi', 'RFC 0148 §A', 'notes are keyed to the current test file, worst-first when mixed, and read back with joined reasons')).toBeUndefined();
257
+ // partial-witness-ok: not a skip — the subject under test (de-duplication).
255
258
  softSkip('inapplicable', 'host does not advertise X'); // de-duplicated
256
259
  let d = softSkipDisposition('runner-ledger.test.ts');
257
260
  expect(d).toEqual({ kind: 'inapplicable', reason: 'host does not advertise X' });
261
+ // partial-witness-ok: not a skip — the subject under test (worst-first fold).
258
262
  softSkip('blocked', 'seam not mounted');
259
263
  d = softSkipDisposition('runner-ledger.test.ts');
260
264
  expect(d?.kind).toBe('blocked');
@@ -52,6 +52,8 @@ describe('runtime-capabilities: /.well-known/openwop forward-compat shape', () =
52
52
  // of v1 hosts will omit it. Assertion passes trivially; don't
53
53
  // force a value on a host that doesn't advertise any.
54
54
  expect(caps).toBeUndefined();
55
+ // partial-witness-ok: discovery answered 200 and runtimeCapabilities is OPTIONAL; when
56
+ // absent, the IF-present MUSTs below bind nothing.
55
57
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `caps === undefined` returned early');
56
58
  }
57
59
 
@@ -58,7 +58,7 @@ describe('speech-synthesis-roundtrip (RFC 0105 §A)', () => {
58
58
  audio !== undefined,
59
59
  req('openwop.it.speech-synthesis-roundtrip.synthesizes-an-audio-asset-with-exactly-one-of-url-base64-and-echoes-the-voiceid', 'RFC 0105 §A', 'the response MUST carry an `audio` object'),
60
60
  ).toBe(true);
61
- if (!audio) return softSkip('blocked', 'precondition not met — `!audio` returned early (seam, prior step, or fixture unavailable)');
61
+ if (!audio) throw new Error('unreachable: the assertion above has already failed this leg');
62
62
 
63
63
  const hasUrl = typeof audio.url === 'string' && (audio.url as string).length > 0;
64
64
  const hasBase64 = typeof audio.base64 === 'string' && (audio.base64 as string).length > 0;
@@ -22,7 +22,7 @@ import { subscribe, type SseEvent } from '../lib/sse.js';
22
22
  import { pollUntilTerminal } from '../lib/polling.js';
23
23
  import { isFixtureAdvertised } from '../lib/fixtures.js';
24
24
  import { req } from '../lib/requirement-ids.js';
25
- import { softSkip } from '../lib/soft-skip.js';
25
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
26
26
 
27
27
  const WORKFLOW_ID = 'conformance-cancellable';
28
28
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -84,7 +84,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-reconnect: Last-Event-ID resume per spe
84
84
  // because the run already completed and the server closed before we
85
85
  // got events. Skip the rest of this scenario; the host is fast enough
86
86
  // that the reconnect path doesn't apply.
87
- return softSkip('blocked', 'precondition not met — `lastSeen < 0` returned early (First connection emitted no events with a parseable sequence — e.g. because the run already completed and the server closed before we got events. Skip the rest of …');
87
+ return blockedDespiteAssertions('the first SSE connection yielded no event with a parseable sequence within 1s — the Last-Event-ID resume is unobserved');
88
88
  }
89
89
 
90
90
  // Phase 3: reconnect with Last-Event-ID set to the last seq we saw.
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
- import { softSkip } from '../lib/soft-skip.js';
13
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
14
14
  import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
15
15
  import { req } from '../lib/requirement-ids.js';
16
16
 
@@ -27,7 +27,7 @@ describe('subrun-approval-gate (RFC 0063 §C)', () => {
27
27
  ).toBe(true);
28
28
 
29
29
  const rejected = await invokeSubRunAttest({ ...base, approvalAction: 'reject' });
30
- if (rejected === null) return softSkip('blocked', 'precondition not met — `rejected === null` returned early (seam, prior step, or fixture unavailable)');
30
+ if (rejected === null) return blockedDespiteAssertions('the sub-run attestation seam answered accept but not reject — the no-merge-on-reject rule is unobserved');
31
31
  expect(
32
32
  rejected.merged,
33
33
  req('openwop.it.subrun-approval-gate.accept-merges-the-child-outputs-reject-does-not', 'RFC 0063 §C', 'a `reject` approval MUST NOT merge the child outputs'),
@@ -24,7 +24,7 @@ import { pollUntilTerminal, pollUntilStatus } from '../lib/polling.js';
24
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
25
25
  import { setHostCapability, resetHostCapabilities, isToggleAvailable } from '../lib/host-toggle.js';
26
26
  import { req } from '../lib/requirement-ids.js';
27
- import { softSkip } from '../lib/soft-skip.js';
27
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
28
28
 
29
29
  const PARENT = 'conformance-subworkflow-input-mapping';
30
30
  const CHILD = 'conformance-subworkflow-input-mapping-child';
@@ -102,7 +102,7 @@ describe.skipIf(SKIP)('subworkflow-input-mapping: parent → child variable seed
102
102
  const subwfCompleted = events.find(
103
103
  (e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
104
104
  );
105
- if (!subwfCompleted) return softSkip('blocked', 'precondition not met — `!subwfCompleted` returned early (seam, prior step, or fixture unavailable)');
105
+ if (!subwfCompleted) return blockedDespiteAssertions('the advertised fixture\'s parent run has no node.completed for subwf-call — the unset-variable projection is unobserved');
106
106
  const childRunId = subwfCompleted.payload?.outputs?.childRunId;
107
107
 
108
108
  const childRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
@@ -84,13 +84,6 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
84
84
  if (!(await toolCatalogGate('openwop-tool-catalog'))) return;
85
85
  const validate = descriptorValidator();
86
86
 
87
- // ---- Leg 3: auth-gated (unauthenticated list MUST be 401) -------------
88
- const unauth = await driver.get(toolsPath(), { authenticated: false });
89
- expect(
90
- unauth.status === 401,
91
- req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST require authentication (401 unauthenticated)`),
92
- ).toBe(true);
93
-
94
87
  // ---- Leg 1: the list (§B) -------------------------------------------
95
88
  // unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISES the
96
89
  // catalog but 404s the list (or answers 500 / a non-list 200, which the
@@ -99,7 +92,17 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
99
92
  // fails under OPENWOP_REQUIRE_BEHAVIOR=true), and any other non-200 or
100
93
  // non-list body FAILS.
101
94
  const read = await listTools();
95
+ // The unserved check runs before any assertion: after the 401 assertion below,
96
+ // seamAbsent's `blocked` note recorded a partial-witness pass at major 1.
102
97
  if (read.unserved) return seamAbsent(`host advertises toolCatalog but ${toolsPath()} answered ${read.status}`);
98
+
99
+ // ---- Leg 3: auth-gated (unauthenticated list MUST be 401) -------------
100
+ const unauth = await driver.get(toolsPath(), { authenticated: false });
101
+ expect(
102
+ unauth.status === 401,
103
+ req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST require authentication (401 unauthenticated)`),
104
+ ).toBe(true);
105
+
103
106
  expect(
104
107
  read.status,
105
108
  req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST return 200 to an authenticated caller on a host advertising toolCatalog`),
@@ -50,7 +50,7 @@ describe('tool-session-lifecycle (RFC 0078 §D)', () => {
50
50
  opened.length >= 1 && closed.length >= 1,
51
51
  req('openwop.it.tool-session-lifecycle.brackets-the-call-events-with-tool-session-opened-first-closed-last-one-sessioni', 'tool-catalog.md §D', 'a tool session MUST emit tool.session.opened + tool.session.closed'),
52
52
  ).toBe(true);
53
- if (opened.length === 0 || closed.length === 0) return softSkip('blocked', 'precondition not met — `opened.length === 0 || closed.length === 0` returned early (seam, prior step, or fixture unavailable)');
53
+ if (opened.length === 0 || closed.length === 0) throw new Error('unreachable: the assertion above has already failed this leg');
54
54
 
55
55
  const open = opened[0]!;
56
56
  const close = closed[closed.length - 1]!;
@@ -48,7 +48,7 @@ import { driver } from '../lib/driver.js';
48
48
  import { behaviorGate } from '../lib/behavior-gate.js';
49
49
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
50
50
  import { req } from '../lib/requirement-ids.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
 
53
53
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
54
54
 
@@ -213,7 +213,7 @@ describe.skipIf(HTTP_SKIP)('trigger-ingestion: behavioral ingestion + SSRF (capa
213
213
  verification: { mode: 'none' },
214
214
  webhook: { method: 'POST', headers: { Authorization: 'Bearer canary' }, body: { x: 1 } },
215
215
  });
216
- if (del.status === 404 || del.status === 403) return softSkip('blocked', 'precondition not met — `del.status === 404 || del.status === 403` returned early (seam, prior step, or fixture unavailable)');
216
+ if (del.status === 404 || del.status === 403) return blockedDespiteAssertions('the ingest seam answered the SSRF probe but not the webhook delivery — ingestion content redaction is unobserved');
217
217
  const delBody = del.json as
218
218
  | { deliveryEvent?: Record<string, unknown>; triggerEvent?: { webhook?: { headers?: Record<string, string> } } }
219
219
  | undefined;
@@ -306,3 +306,18 @@ describe('RFC 0209 §C.9 — no render before root (declared unwitnessable here;
306
306
  return softSkip('inapplicable', 'unwitnessable by this suite (RFC 0209 §C.9 Falsifiability verdict): a render-side guarantee, and a server-oriented suite cannot observe a renderer. The fold guard the server CAN observe is openwop.requirement.0209.fold-guarded, above. The render half is witnessed as a reference-app client probe (tier: reference-impl, the a2ui-surface-no-code-exec precedent): openwop/openwop-app frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx (openwop-app#4121, ADR 0749). Removing the `if (!state.renderable)` guard in frontend/react/src/chat/a2ui/v09/A2uiV09Surface.tsx turns 2 of its 4 cases red. This row records that the suite reached the requirement, and it is not a pass.');
307
307
  });
308
308
  });
309
+
310
+ describe('RFC 0220 §C — the a2uiSurface family witness (seam-gated)', () => {
311
+ it('a version-2 surface is admitted and a version-1 body under schema version 2 is refused, in the same run', async () => {
312
+ const g = await gate((f) => f >= 2, 'a floor of at least 2');
313
+ if (!g.ok) return softSkip(g.kind, g.reason);
314
+ const { runId } = g.ctx;
315
+ const R = (why: string) => req('openwop.family.a2uiSurface', 'spec/v2/ext/a2uiSurface/README.md §Conformance', why);
316
+ try {
317
+ const good = await emit(runId, envelope(2, v2(sid('fam-good'), (POSITIVE['messages'] as Json[]))));
318
+ expect(good.status, R(`the positive version-2 surface MUST be admitted (201), got ${code(good)}`)).toBe(201);
319
+ const mixed = await emit(runId, envelope(2, V1_SURFACE));
320
+ expect(code(mixed), R('a version-1 body under schema version 2 MUST be refused with envelope_invalid (control: a host that admits everything fails here)')).toBe('422 envelope_invalid');
321
+ } finally { await done(runId); }
322
+ });
323
+ });
@@ -29,6 +29,15 @@ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
29
29
  import { startEffectReceiver } from '../lib/effect-receiver.js';
30
30
  import { req } from '../lib/requirement-ids.js';
31
31
 
32
+ /**
33
+ * The retry leg hands the host the address of the suite's OWN receiver
34
+ * (`lib/effect-receiver.ts`, since 2.42.4) and the host calls it — a connection
35
+ * the host originates back to the harness. `host-callback-declaration` requires
36
+ * that to be declared; 2.42.4 added the receiver and not the declaration, so
37
+ * the self-check failed on every host that runs the full catalog.
38
+ */
39
+ export const REQUIRES_HOST_CALLBACK = 'the host makes the retried outbound effect call to the suite-owned effect receiver (OPENWOP_WEBHOOK_RECEIVER_PORT)';
40
+
32
41
  const FIXTURE = 'conformance-noop';
33
42
  const KEYING = ['business-identity', 'activity-recipe'];
34
43
  const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
@@ -73,6 +82,9 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
73
82
  const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> } | null;
74
83
  const effects = body?.effects ?? [];
75
84
  if (res.status === 200 && check.ok && body?.runId === runId && effects.length === 0) {
85
+ // partial-witness-ok: no assertion has passed at runtime. The only earlier
86
+ // expect runs solely when the create is not 201, and then it throws, so this
87
+ // skip is a zero-assertion `inapplicable` (the gate reads source order).
76
88
  return softSkip('inapplicable', 'the noop fixture issued no external effect — the ledger read is well-formed but the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
77
89
  }
78
90
  expect(
@@ -31,10 +31,12 @@ import { v2Discovery } from '../lib/v2.js';
31
31
  import { softSkip } from '../lib/soft-skip.js';
32
32
  import { req } from '../lib/requirement-ids.js';
33
33
  import { readErrorCode } from '../lib/error-envelope.js';
34
- import { codemapV1toV2, era2Gate, eventsOf, pollEvents, seedEra2Log, v1FixtureLog, type ReadEvent } from '../lib/era2-seed.js';
34
+ import { appendEra2Event, codemapV1toV2, era2Gate, eventsOf, pollEvents, seedEra2Log, v1FixtureLog, type ReadEvent } from '../lib/era2-seed.js';
35
35
 
36
36
  const ID = 'openwop.requirement.0176.era-2-append-vocabulary';
37
37
  const DOC = 'spec/v2/core/persistence.md §The writer rule';
38
+ /** A type the codemap renames (v1 `agent.reasoning.delta`); artifact event, no projection effect. */
39
+ const WITNESS_TYPE = 'agent.reasoning-delta';
38
40
 
39
41
  async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
40
42
  try {
@@ -88,20 +90,31 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
88
90
  // Neither answer is asserted: the resume transition's event set is not
89
91
  // pinned to a renamed type, and a seeded run's pause is host latitude. If
90
92
  // no renamed type is appended, the leg records `blocked` below, not a pass.
93
+ // 2.42.6: the writer-rule witness proper. `appendEra2Event` (host-sample-test-
94
+ // seams.md §27) hands the host ONE event of a type the codemap RENAMES
95
+ // (`agent.reasoning-delta`, stored v1 `agent.reasoning.delta`) and the host's
96
+ // production writer chooses the stored spelling; the read below is the witness.
97
+ // It is an artifact event, so it moves no projection. A host that does not
98
+ // serve the seam falls through to the pre-seam path below and ends in the
99
+ // annotated partial witness, never `blocked` (see the next comment); the gate
100
+ // above already made a host without the seams profile `inapplicable`.
101
+ const appended = await appendEra2Event(runId, WITNESS_TYPE, { agentId: 'conformance', delta: 'era-2 writer-rule witness', sequence: 0 });
102
+ // A served seam that breaks its contract is `blocked`. An UNSERVED seam is not:
103
+ // §27 is optional and newer than this leg, so its absence falls through to the
104
+ // pre-seam path and ends in the partial witness below — it never denies a
105
+ // host certification it held before the seam existed.
106
+ if (!appended.ok && !appended.unserved) return softSkip(appended.kind, appended.reason);
107
+ const seamServed = appended.ok;
108
+
91
109
  const enc = encodeURIComponent(runId);
92
110
  const paused = await http(() => driver.post(`/runs/${enc}:pause`, { reason: 'conformance', drainPolicy: 'immediate' }));
93
- const resumed = paused?.status === 202 ? await http(() => driver.post(`/runs/${enc}:resume`, { reason: 'conformance' })) : null;
94
-
95
- // One canonical mutation so the HOST's own writer appends. Cancel is the
96
- // universally available terminal transition; a host that refuses it on a
97
- // seeded run records `blocked` rather than a pass — unless the pause and
98
- // resume above were both accepted, which already drove the host's writer.
99
- const cancelled = await http(() => driver.post(`/runs/${enc}/cancel`, {}));
100
- const cancelOk = cancelled !== null && (cancelled.status === 200 || cancelled.status === 202 || cancelled.status === 204);
101
- const pauseResumeOk = paused?.status === 202 && resumed?.status === 202;
102
- if (!cancelOk && !pauseResumeOk) {
103
- return softSkip('blocked', `POST /runs/{runId}/cancel answered ${cancelled?.status ?? 'no response'} on a seeded era-2 run (pause ${paused?.status ?? 'no response'}, resume ${resumed?.status ?? 'not attempted'}) — no canonical mutation drove the host's writer, so the append is unwitnessed`);
104
- }
111
+ if (paused?.status === 202) await http(() => driver.post(`/runs/${enc}:resume`, { reason: 'conformance' }));
112
+
113
+ // A canonical mutation as well, so the host's own lifecycle writer appends
114
+ // beside the seam's append. The seam append above already drove the
115
+ // production writer, so a host that refuses cancel on a SEEDED run no longer
116
+ // leaves the rule unwitnessed — the answer is not asserted here.
117
+ await http(() => driver.post(`/runs/${enc}/cancel`, {}));
105
118
 
106
119
  const after = await pollEvents(runId);
107
120
  if (after === null) return softSkip('blocked', 'the event read failed after the append');
@@ -157,18 +170,20 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
157
170
  // writer rule is unobserved.
158
171
  const renamedV2 = new Set([...codemapV1toV2()].filter(([v1, v2]) => v1 !== v2).map(([, v2]) => v2));
159
172
  const appendedTypes = rows.slice(seedCount).map((r) => String(r.type ?? ''));
160
- if (!appendedTypes.some((t) => renamedV2.has(t))) {
161
- // partial-witness-ok: HOTFIX 2.42.5 (2026-09-27). 2.42.3 and 2.42.4 made this
162
- // `blockedDespiteAssertions`, which is bundle-fatal (RFC 0168 §E.1). No catalogued
163
- // seam lets ANY host make a seeded era-2 run append a type the codemap renames,
164
- // so the row blocked on every host and no host could certify. That is the suite's
165
- // gap, not a host's. The append, the read-back and the sequence rules above ARE
166
- // observed. The codemap-renaming writer rule is NOT, and this partial witness says
167
- // so in its detail; the acceptance predicate refuses a partial-witness row. The
168
- // seams-contract addition that makes the rule witnessable is in progress
169
- // (openwop-1f); when it lands, this becomes a real assertion again.
170
- return softSkip('inapplicable', `partial witness — the codemap-renaming writer rule is UNWITNESSED: every type the host appended to the era-2 log (${appendedTypes.join(', ') || 'none'}) is spelled identically in v1 and v2 (spec/v2/event-codemap.json), so a host writing v2 names reads back the same as one writing v1 names — the writer rule is unobserved. The leg needs a canonical mutation that appends a renamed type (e.g. run.resuming → run.resume-started); pause ${paused?.status ?? 'no response'} / resume ${resumed?.status ?? 'not attempted'} on the seeded run appended none, and the seed seam (api/seams-v2.yaml seedEra2EventLog) cannot seed a paused or interrupt-suspended era-2 run to resume. A seams-contract addition to seed one is in progress.`);
173
+ if (!seamServed && !appendedTypes.some((t) => renamedV2.has(t))) {
174
+ // partial-witness-ok: the host does not serve the optional appendEra2Event
175
+ // seam (host-sample-test-seams.md §27), and none of its own appends was a
176
+ // codemap-RENAMED type, so the writer rule is unobserved here. The append,
177
+ // the read-back and the sequence rules above ARE observed. This is the
178
+ // 2.42.5 disposition, kept for hosts without the seam; the acceptance
179
+ // predicate refuses a partial-witness row. (A host without the seam whose
180
+ // own pause/resume appended a renamed type falls through: that IS a witness.)
181
+ return softSkip('inapplicable', `partial witness — the codemap-renaming writer rule is UNWITNESSED: ${appended.ok ? '' : appended.reason}; every type the host appended to the era-2 log (${appendedTypes.join(', ') || 'none'}) is spelled identically in v1 and v2 (spec/v2/event-codemap.json)`);
171
182
  }
183
+ if (seamServed) expect(
184
+ appendedTypes.includes(WITNESS_TYPE),
185
+ req(ID, DOC, `the event appended through appendEra2Event (${WITNESS_TYPE}, a codemap-RENAMED type) MUST read back under its v2 name through the translated read — it read as ${appendedTypes.join(', ') || 'nothing'}: a host that stored the v2 spelling in an era-2 log makes the reader map it a second time (event_type_unmapped), and one that bypassed the storage boundary leaks the v1 spelling`),
186
+ ).toBe(true);
172
187
  });
173
188
 
174
189
  it('the run keeps the era it was created with; an append does not promote it to era 3', async () => {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * v2-ext-family-claims — RFC 0220 §B, the `claims-check` witness for the 11
3
+ * discovery-only extension families (suite 2.42.7, target major 2).
4
+ *
5
+ * Each family is a reservation: v2 defines no portable operation for it, so the
6
+ * only thing a server-oriented suite can check is the claim itself. A host that
7
+ * advertises `extensions["<org>.<extensionName>"]` records `executed-pass` under
8
+ * `openwop.family.<key>` when the claim is well formed (lib/ext-claims.ts), and
9
+ * `executed-fail` when it is not. A host that does not advertise the family
10
+ * records `inapplicable`.
11
+ *
12
+ * Why not behaviorGate: an ext family is outside every profile
13
+ * (spec/v2/ext/README.md), so strict mode has nothing to demand here — a host
14
+ * that serves no `kanban` has made no promise about one. The gate is the claim.
15
+ *
16
+ * Every req() id is a literal so the requirement registry collects it. That a
17
+ * leg exists for every claims-check family in the declaration is a corpus fact,
18
+ * checked by the self-test in src/lib/ext-claims.test.ts, not by a host run.
19
+ *
20
+ * @see RFCS/0220-ext-families-graduate-on-evidence.md
21
+ * @see spec/v2/ext/README.md
22
+ * @see spec/v2/core/capabilities.md §3.2
23
+ */
24
+ import { describe, it, expect } from 'vitest';
25
+ import { v2Discovery } from '../lib/v2.js';
26
+ import { extDeclaration, classifyExtClaim } from '../lib/ext-claims.js';
27
+ import { softSkip } from '../lib/soft-skip.js';
28
+ import { req } from '../lib/requirement-ids.js';
29
+
30
+ const LEGS = ['brand', 'canvas', 'chat', 'coordination', 'dataIntegration', 'entities', 'kanban', 'knowledge', 'launchStudio', 'messaging', 'webResearch'] as const;
31
+
32
+ async function leg(key: (typeof LEGS)[number], cite: (why: string) => string): Promise<undefined> {
33
+ const decl = extDeclaration();
34
+ if (!decl) return softSkip('blocked', 'spec/v2/declaration.json is not resolvable in this layout — the extension name and the org registry come from it, and guessing either would make the suite the registry (conformance.md §Whose fact is the reason?)');
35
+ const family = decl.families.find((f) => f.key === key);
36
+ if (!family) return softSkip('blocked', `the declaration has no ext row for ${key} — this leg is stale against the corpus`);
37
+ const doc = await v2Discovery();
38
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
39
+ const claims = classifyExtClaim(doc, family, decl);
40
+ if (claims.length === 1 && claims[0].state === 'absent') return softSkip('inapplicable', `no extensions["<org>.${family.extensionName}"] record under a registered org — the host does not advertise ${key}`);
41
+ for (const c of claims) {
42
+ if (c.state === 'absent') continue;
43
+ expect(c.state, cite(c.state === 'malformed' ? `extensions["${c.key}"]: ${c.why}` : `extensions["${c.key}"] is a well-formed claim`)).toBe('well-formed');
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ const S = (key: string): string => `spec/v2/ext/${key}/README.md §Conformance`;
49
+
50
+ describe('RFC 0220 §B — an extension family is claimed in a well-formed extensions record (claims-check)', () => {
51
+ it('brand', () => leg('brand', (why) => req('openwop.family.brand', S('brand'), why)));
52
+ it('canvas', () => leg('canvas', (why) => req('openwop.family.canvas', S('canvas'), why)));
53
+ it('chat', () => leg('chat', (why) => req('openwop.family.chat', S('chat'), why)));
54
+ it('coordination', () => leg('coordination', (why) => req('openwop.family.coordination', S('coordination'), why)));
55
+ it('dataIntegration', () => leg('dataIntegration', (why) => req('openwop.family.dataIntegration', S('dataIntegration'), why)));
56
+ it('entities', () => leg('entities', (why) => req('openwop.family.entities', S('entities'), why)));
57
+ it('kanban', () => leg('kanban', (why) => req('openwop.family.kanban', S('kanban'), why)));
58
+ it('knowledge', () => leg('knowledge', (why) => req('openwop.family.knowledge', S('knowledge'), why)));
59
+ it('launchStudio', () => leg('launchStudio', (why) => req('openwop.family.launchStudio', S('launchStudio'), why)));
60
+ it('messaging', () => leg('messaging', (why) => req('openwop.family.messaging', S('messaging'), why)));
61
+ it('webResearch', () => leg('webResearch', (why) => req('openwop.family.webResearch', S('webResearch'), why)));
62
+ });