@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
@@ -31,6 +31,7 @@
31
31
  */
32
32
 
33
33
  import { describe, it, expect } from 'vitest';
34
+ import { softSkip } from '../lib/soft-skip.js';
34
35
  import { driver } from '../lib/driver.js';
35
36
 
36
37
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -63,7 +64,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
63
64
  ]);
64
65
  if (probe.status === 404) {
65
66
  ctx.skip(); // host doesn't expose the simulator seam
66
- return;
67
+ return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
67
68
  }
68
69
  expect(
69
70
  probe.status,
@@ -89,7 +90,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
89
90
  ]);
90
91
  if (probe.status === 404) {
91
92
  ctx.skip();
92
- return;
93
+ return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
93
94
  }
94
95
  expect(probe.status).toBe(200);
95
96
  expect(
@@ -115,7 +116,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
115
116
  ]);
116
117
  if (probe.status === 404) {
117
118
  ctx.skip();
118
- return;
119
+ return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
119
120
  }
120
121
  expect(probe.status).toBe(200);
121
122
  const redirects = probe.body.cacheRedirects ?? [];
@@ -144,7 +145,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
144
145
  ]);
145
146
  if (probe.status === 404) {
146
147
  ctx.skip();
147
- return;
148
+ return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
148
149
  }
149
150
  expect(probe.status).toBe(200);
150
151
  expect(
@@ -165,7 +166,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
165
166
  const p1 = await simulatePartition(claims);
166
167
  if (p1.status === 404) {
167
168
  ctx.skip();
168
- return;
169
+ return softSkip('blocked', 'precondition not met — `p1.status === 404` returned early (seam, prior step, or fixture unavailable)');
169
170
  }
170
171
  expect(p1.status).toBe(200);
171
172
  const p2 = await simulatePartition([claims[2]!, claims[0]!, claims[1]!]);
@@ -190,7 +191,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
190
191
  ]);
191
192
  if (probe.status === 404) {
192
193
  ctx.skip();
193
- return;
194
+ return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
194
195
  }
195
196
  expect(
196
197
  probe.status,
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
+ import { softSkip } from '../lib/soft-skip.js';
32
33
  import { driver } from '../lib/driver.js';
33
34
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
34
35
 
@@ -58,7 +59,7 @@ describe('multi-region-idempotency: capability shape', () => {
58
59
  console.warn(
59
60
  '[multi-region-idempotency] capabilities.idempotency.crossRegion not advertised; skipping',
60
61
  );
61
- return;
62
+ return softSkip('blocked', 'precondition not met — `!idem || idem.crossRegion === undefined` returned early (seam, prior step, or fixture unavailable)');
62
63
  }
63
64
 
64
65
  expect(ALLOWED.has(idem.crossRegion), driver.describe(
@@ -82,7 +83,7 @@ describe('multi-region-idempotency: capability shape', () => {
82
83
 
83
84
  if (crossRegion !== 'reconciled-records' && crossRegion !== 'fenced-effects') {
84
85
  // Single-region hosts have no conflicts to count — skip.
85
- return;
86
+ return softSkip('blocked', 'precondition not met — `crossRegion !== \'reconciled-records\' && crossRegion !== \'fenced-effects\'` returned early (seam, prior step, or fixture unavailable)');
86
87
  }
87
88
 
88
89
  const advertised = new Set(observability?.metrics?.names ?? []);
@@ -117,7 +118,7 @@ describe('multi-region-idempotency: granular multiRegion advertisement shape (RF
117
118
  'idempotency',
118
119
  );
119
120
  const mr = idem?.multiRegion;
120
- if (mr === undefined) return; // host doesn't advertise the granular block — soft-skip
121
+ if (mr === undefined) return softSkip('inapplicable', 'host doesn\'t advertise the granular block — soft-skip');
121
122
 
122
123
  expect(
123
124
  typeof mr.supported,
@@ -0,0 +1,242 @@
1
+ /**
2
+ * RFC 0149 §D — normative examples are extracted and validated against the
3
+ * same schemas conformance uses.
4
+ *
5
+ * "Normative examples MUST be extracted into fixtures and validated against
6
+ * the same schemas used by conformance" (§D). Until now a fenced example was
7
+ * prose to every validator in the corpus: `capability-example-root-layout`
8
+ * catches one wrong SHAPE (the `capabilities` wrapper), but an example could
9
+ * carry a misspelled field, a wrong enum value, or a stale required property
10
+ * and every gate stayed green — a green corpus teaching a non-conformant
11
+ * document.
12
+ *
13
+ * Mechanism — a declaration, not a heuristic:
14
+ *
15
+ * <!-- normative-example: <name>.schema.json -->
16
+ * ```json
17
+ * { ... }
18
+ * ```
19
+ *
20
+ * The HTML comment on the line immediately before a ```json / ```jsonc fence
21
+ * declares that the fenced body is a WHOLE instance of the named schema. The
22
+ * scenario extracts each declared example at test time and validates it with
23
+ * Ajv against `schemas/<name>` — the identical schema files and registration
24
+ * (`fixtures-valid.test.ts` enumerates the directory the same way) that every
25
+ * other conformance leg uses. Extraction is in-process rather than committed
26
+ * under `fixtures/`: a committed copy of a prose example is a second source
27
+ * of truth that drifts, which is the defect §D exists to remove.
28
+ *
29
+ * Non-vacuity guards (RFC 0148 §A — an extractor that matches nothing must
30
+ * not pass):
31
+ * - the corpus MUST contain declared examples (floor asserted);
32
+ * - every fenced JSON block in spec/v1 that parses and validates as a whole
33
+ * instance of some non-permissive schema MUST be declared — an example
34
+ * that IS a schema instance is a normative example, and the marker is how
35
+ * it says so; a new whole-document example added without the marker fails
36
+ * here rather than silently escaping validation;
37
+ * - every declared example MUST parse as strict JSON (jsonc comments and
38
+ * ellipses are prose, not instances) and MUST validate;
39
+ * - every discovery-shaped example (root `protocolVersion` +
40
+ * `supportedEnvelopes`) MUST be declared against `capabilities.schema.json`
41
+ * ("all normative discovery examples ... validate" — RFC 0149 acceptance);
42
+ * - §E: no declared discovery example carries credential- or tenant-shaped
43
+ * keys.
44
+ *
45
+ * Server-free; always-on in the corpus layout (spec prose is not shipped in the
46
+ * published package, so `V1_DIR === null` skips the describe there).
47
+ */
48
+
49
+ import { describe, it, expect } from 'vitest';
50
+ import { readFileSync, readdirSync } from 'node:fs';
51
+ import { join } from 'node:path';
52
+ import Ajv2020 from 'ajv/dist/2020.js';
53
+ import addFormats from 'ajv-formats';
54
+ import type { ValidateFunction } from 'ajv';
55
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
56
+
57
+ export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: extracts fenced prose examples and validates them against the schema files on disk; no host is contacted';
58
+
59
+ const MARKER = /^<!--\s*normative-example:\s*([a-z0-9-]+\.schema\.json)\s*-->\s*$/;
60
+
61
+ interface Fenced {
62
+ readonly file: string;
63
+ /** 1-based line of the ``` opener. */
64
+ readonly line: number;
65
+ readonly info: string;
66
+ readonly body: string;
67
+ /** Schema declared by a marker on the preceding line, if any. */
68
+ readonly declared: string | null;
69
+ }
70
+
71
+ function fencedBlocks(dir: string): Fenced[] {
72
+ const out: Fenced[] = [];
73
+ for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
74
+ const lines = readFileSync(join(dir, name), 'utf8').split('\n');
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const open = /^```(json|jsonc)(\s.*)?$/.exec(lines[i]!.trim());
77
+ if (open === null) continue;
78
+ const body: string[] = [];
79
+ let j = i + 1;
80
+ while (j < lines.length && lines[j]!.trim() !== '```') {
81
+ body.push(lines[j]!);
82
+ j++;
83
+ }
84
+ const prev = i > 0 ? (lines[i - 1] ?? '') : '';
85
+ const m = MARKER.exec(prev.trim());
86
+ out.push({ file: name, line: i + 1, info: open[1] ?? 'json', body: body.join('\n'), declared: m?.[1] ?? null });
87
+ i = j;
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /** Stray markers: a marker line NOT immediately followed by a fence opener. */
94
+ function strayMarkers(dir: string): string[] {
95
+ const out: string[] = [];
96
+ for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
97
+ const lines = readFileSync(join(dir, name), 'utf8').split('\n');
98
+ for (let i = 0; i < lines.length; i++) {
99
+ if (!MARKER.test(lines[i]!.trim())) continue;
100
+ const next = (lines[i + 1] ?? '').trim();
101
+ if (!/^```(json|jsonc)(\s|$)/.test(next)) out.push(`spec/v1/${name}:${i + 1}`);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+
107
+ function parseStrict(body: string): unknown | undefined {
108
+ try {
109
+ return JSON.parse(body) as unknown;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function isDiscoveryShaped(v: unknown): boolean {
116
+ return (
117
+ v !== null &&
118
+ typeof v === 'object' &&
119
+ !Array.isArray(v) &&
120
+ typeof (v as Record<string, unknown>)['protocolVersion'] === 'string' &&
121
+ Array.isArray((v as Record<string, unknown>)['supportedEnvelopes'])
122
+ );
123
+ }
124
+
125
+ /** Register every schema in the directory (S14 pattern) and hand back validators by file name. */
126
+ function validators(): { byFile: Map<string, ValidateFunction>; permissive: Set<string> } {
127
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
128
+ addFormats(ajv);
129
+ const files = readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json')).sort();
130
+ const docs = new Map<string, { $id?: string }>();
131
+ for (const f of files) {
132
+ const doc = JSON.parse(readFileSync(join(SCHEMAS_DIR, f), 'utf8')) as { $id?: string };
133
+ docs.set(f, doc);
134
+ ajv.addSchema(doc);
135
+ }
136
+ const byFile = new Map<string, ValidateFunction>();
137
+ for (const [f, doc] of docs) {
138
+ const v = doc.$id === undefined ? undefined : ajv.getSchema(doc.$id);
139
+ if (v !== undefined) byFile.set(f, v);
140
+ }
141
+ // A schema that accepts `{}` AND `{x:1}` constrains nothing at the root; a
142
+ // whole-document match against it says nothing about the example, so it is
143
+ // excluded from the "must be declared" auto-detection (still usable as an
144
+ // explicit declaration).
145
+ const permissive = new Set([...byFile.entries()].filter(([, v]) => v({}) === true && v({ x: 1 }) === true).map(([f]) => f));
146
+ return { byFile, permissive };
147
+ }
148
+
149
+ const CREDENTIAL_KEY = /^(apiKey|api_key|token|accessToken|refreshToken|secret|clientSecret|password|authorization|bearer|tenantId|tenant_id|organizationId)$/i;
150
+ function credentialShapedKeys(v: unknown, path = '$'): string[] {
151
+ const out: string[] = [];
152
+ if (Array.isArray(v)) {
153
+ v.forEach((x, i) => out.push(...credentialShapedKeys(x, `${path}[${i}]`)));
154
+ } else if (v !== null && typeof v === 'object') {
155
+ for (const [k, x] of Object.entries(v as Record<string, unknown>)) {
156
+ if (CREDENTIAL_KEY.test(k)) out.push(`${path}.${k}`);
157
+ out.push(...credentialShapedKeys(x, `${path}.${k}`));
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+
163
+ describe.skipIf(V1_DIR === null)('RFC 0149 §D — normative examples are extracted and validated against the conformance schemas', () => {
164
+ // `describe.skipIf` still RUNS this factory in the published layout (vitest
165
+ // collects skipped suites), so nothing here may touch the prose directory
166
+ // when it is absent — the guards below keep collection green in the tarball.
167
+ const v1Dir = V1_DIR ?? '';
168
+ const blocks = V1_DIR === null ? [] : fencedBlocks(v1Dir);
169
+ const declared = blocks.filter((b) => b.declared !== null);
170
+ const { byFile, permissive } = V1_DIR === null ? { byFile: new Map<string, ValidateFunction>(), permissive: new Set<string>() } : validators();
171
+
172
+ it('the extractor finds fenced examples and the corpus declares normative ones (non-vacuity floor)', () => {
173
+ expect(blocks.length, 'spec/v1 MUST contain fenced json/jsonc examples').toBeGreaterThan(100);
174
+ expect(declared.length, 'spec/v1 MUST declare normative examples with <!-- normative-example: … -->').toBeGreaterThanOrEqual(20);
175
+ expect(byFile.size, 'schemas/ MUST register').toBeGreaterThan(50);
176
+ });
177
+
178
+ it('every marker names a schema that exists and sits immediately above a json/jsonc fence', () => {
179
+ const unknown = declared.filter((b) => !byFile.has(b.declared as string)).map((b) => `spec/v1/${b.file}:${b.line} → ${String(b.declared)}`);
180
+ expect(unknown, 'normative-example markers naming a schema that does not exist in schemas/').toEqual([]);
181
+ expect(strayMarkers(v1Dir), 'normative-example markers not directly followed by a ```json fence').toEqual([]);
182
+ });
183
+
184
+ it('every declared example parses as strict JSON — jsonc comments and ellipses are prose, not instances', () => {
185
+ const unparsable = declared.filter((b) => parseStrict(b.body) === undefined).map((b) => `spec/v1/${b.file}:${b.line} (${b.info}) declared ${String(b.declared)}`);
186
+ expect(unparsable, 'declared normative examples MUST be strict JSON').toEqual([]);
187
+ });
188
+
189
+ it('every declared example VALIDATES against its declared schema — the same schema file conformance uses', () => {
190
+ const failures: string[] = [];
191
+ for (const b of declared) {
192
+ const doc = parseStrict(b.body);
193
+ if (doc === undefined) continue; // reported by the leg above
194
+ const v = byFile.get(b.declared as string);
195
+ if (v === undefined) continue; // reported by the leg above
196
+ if (v(doc) !== true) {
197
+ const errs = (v.errors ?? []).slice(0, 3).map((e) => `${e.instancePath || '/'} ${e.message ?? ''}`).join('; ');
198
+ failures.push(`spec/v1/${b.file}:${b.line} ✗ ${String(b.declared)}: ${errs}`);
199
+ }
200
+ }
201
+ expect(failures, 'RFC 0149 §D: a normative example that does not validate teaches a non-conformant document').toEqual([]);
202
+ });
203
+
204
+ it('every fenced JSON block that IS a whole instance of a (non-permissive) schema is declared — no undeclared normative example', () => {
205
+ const undeclared: string[] = [];
206
+ for (const b of blocks) {
207
+ if (b.declared !== null) continue;
208
+ const doc = parseStrict(b.body);
209
+ if (doc === undefined) continue;
210
+ const hits = [...byFile.entries()].filter(([f, v]) => !permissive.has(f) && v(doc) === true).map(([f]) => f);
211
+ if (hits.length > 0) undeclared.push(`spec/v1/${b.file}:${b.line} validates as ${hits.join(' | ')} — add <!-- normative-example: <one of these> --> above the fence`);
212
+ }
213
+ expect(undeclared, 'whole-document examples MUST be declared so they are validated on every run').toEqual([]);
214
+ });
215
+
216
+ it('every discovery-shaped example is declared against capabilities.schema.json and validates (RFC 0149: "all normative discovery examples ... validate")', () => {
217
+ const discovery = blocks.filter((b) => isDiscoveryShaped(parseStrict(b.body)));
218
+ expect(discovery.length, 'spec/v1 MUST carry at least one whole discovery example').toBeGreaterThan(0);
219
+ const wrong = discovery.filter((b) => b.declared !== 'capabilities.schema.json').map((b) => `spec/v1/${b.file}:${b.line} declared ${String(b.declared)}`);
220
+ expect(wrong, 'discovery-shaped examples MUST be declared normative-example: capabilities.schema.json').toEqual([]);
221
+ const v = byFile.get('capabilities.schema.json');
222
+ expect(v).toBeDefined();
223
+ for (const b of discovery) expect((v as ValidateFunction)(parseStrict(b.body)), `spec/v1/${b.file}:${b.line}`).toBe(true);
224
+ });
225
+
226
+ it('§E: no declared discovery example carries credential- or tenant-shaped keys', () => {
227
+ const offenders: string[] = [];
228
+ for (const b of declared.filter((x) => x.declared === 'capabilities.schema.json')) {
229
+ for (const p of credentialShapedKeys(parseStrict(b.body))) offenders.push(`spec/v1/${b.file}:${b.line} ${p}`);
230
+ }
231
+ expect(offenders, 'RFC 0149 §E: no discovery example may contain credentials or tenant data').toEqual([]);
232
+ });
233
+
234
+ it('sabotage: a declared example with a wrong enum value is caught by the same validator path', () => {
235
+ // Guard against a validator that compiles to `true` (strict:false + a
236
+ // schema that failed to register would do that silently).
237
+ const v = byFile.get('error-envelope.schema.json');
238
+ expect(v).toBeDefined();
239
+ expect((v as ValidateFunction)({ error: 'not_found', message: 'gone', retriable: 'yes' })).toBe(false);
240
+ expect((v as ValidateFunction)({ error: 'not_found', message: 'gone' })).toBe(true);
241
+ });
242
+ });
@@ -35,6 +35,7 @@
35
35
  */
36
36
 
37
37
  import { describe, it, expect } from 'vitest';
38
+ import { softSkip } from '../lib/soft-skip.js';
38
39
  import { driver } from '../lib/driver.js';
39
40
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
40
41
 
@@ -74,8 +75,8 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
74
75
  describe('oauth-authorization-code-roundtrip: the grant dance (RFC 0047 §C)', () => {
75
76
  it('acquires a token via authorization_code and returns a reference, never the token', async () => {
76
77
  const oauth = await readOAuth();
77
- if (!oauth?.supported) return; // capability-gated
78
- if (!Array.isArray(oauth.grants) || !oauth.grants.includes('authorization_code')) return; // grant-gated
78
+ if (!oauth?.supported) return softSkip('inapplicable', 'capability-gated');
79
+ if (!Array.isArray(oauth.grants) || !oauth.grants.includes('authorization_code')) return softSkip('inapplicable', 'grant-gated (!Array.isArray(oauth.grants) || !oauth.grants.includes(\'authorization_code\'))');
79
80
 
80
81
  // Seam contract: the host performs the full authorization-code roundtrip
81
82
  // against the synthetic provider's authUrl/tokenUrl, persists the acquired
@@ -95,7 +96,7 @@ describe('oauth-authorization-code-roundtrip: the grant dance (RFC 0047 §C)', (
95
96
  refreshTokenCanary: SYNTHETIC.refreshTokenCanary,
96
97
  });
97
98
  // A host that hasn't wired the seam soft-skips (Tier-2, host-pending).
98
- if (res.status === 404) return;
99
+ if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
99
100
 
100
101
  expect(
101
102
  res.status,
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
+ import { softSkip } from '../lib/soft-skip.js';
20
21
  import { driver } from '../lib/driver.js';
21
22
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
22
23
 
@@ -54,7 +55,7 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
54
55
  describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
55
56
  it('capabilities.oauth is either absent or well-formed', async () => {
56
57
  const oauth = await readOAuth();
57
- if (oauth === null) return; // host doesn't advertise host.oauth at all
58
+ if (oauth === null) return softSkip('inapplicable', 'host doesn\'t advertise host.oauth at all');
58
59
  expect(
59
60
  typeof oauth.supported,
60
61
  driver.describe(
@@ -66,7 +67,7 @@ describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
66
67
 
67
68
  it('grants is a subset of the canonical grant set when supported', async () => {
68
69
  const oauth = await readOAuth();
69
- if (!oauth?.supported || oauth.grants === undefined) return;
70
+ if (!oauth?.supported || oauth.grants === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!oauth?.supported || oauth.grants === undefined` returned early');
70
71
  expect(
71
72
  Array.isArray(oauth.grants),
72
73
  driver.describe('RFC 0047 §A', 'capabilities.oauth.grants MUST be an array'),
@@ -84,7 +85,7 @@ describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
84
85
 
85
86
  it('every advertised provider has a non-empty id when supported', async () => {
86
87
  const oauth = await readOAuth();
87
- if (!oauth?.supported || oauth.providers === undefined) return;
88
+ if (!oauth?.supported || oauth.providers === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!oauth?.supported || oauth.providers === undefined` returned early');
88
89
  for (const provider of oauth.providers) {
89
90
  expect(
90
91
  typeof provider.id === 'string' && provider.id.length > 0,
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
+ import { softSkip } from '../lib/soft-skip.js';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
31
 
@@ -49,7 +50,7 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
49
50
  describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () => {
50
51
  it('capabilities.oauth.supported is a boolean when advertised', async () => {
51
52
  const oauth = await readOAuth();
52
- if (oauth === null) return;
53
+ if (oauth === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `oauth === null` returned early');
53
54
  expect(
54
55
  typeof oauth.supported,
55
56
  driver.describe(
@@ -63,14 +64,14 @@ describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () =>
63
64
  describe('oauth-connector-redaction: token material MUST NOT cross the wire (RFC 0047 §C.2)', () => {
64
65
  it('canary token is absent from every observable run surface', async () => {
65
66
  const oauth = await readOAuth();
66
- if (!oauth?.supported) return; // capability-gated
67
+ if (!oauth?.supported) return softSkip('inapplicable', 'capability-gated');
67
68
 
68
69
  // Seam contract: a synthetic provider issues a token whose value is
69
70
  // TOKEN_CANARY, a connector node runs, and the run's observable surfaces
70
71
  // (events incl. connector.authorized + snapshot + debug bundle) are returned.
71
72
  const res = await driver.post('/v1/host/sample/oauth/connector-echo', { canary: TOKEN_CANARY });
72
73
  // 404 from a host that hasn't wired the test seam is a soft-skip.
73
- if (res.status === 404) return;
74
+ if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
74
75
 
75
76
  expect(
76
77
  res.status,