@openwop/openwop-conformance 2.45.14 → 2.45.16

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 (82) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -2
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +2668 -4
  6. package/scenario-majors.json +164 -2
  7. package/schemas/CORPUS-STAMP.json +13 -13
  8. package/src/lib/artifact-schema-url-witness.ts +83 -0
  9. package/src/lib/backpressure-witness.ts +2 -2
  10. package/src/lib/budget-witness.ts +3 -3
  11. package/src/lib/dispatch-witness.ts +212 -0
  12. package/src/lib/envelope-schema-static.ts +166 -0
  13. package/src/lib/exhaustion-facet-witness.ts +1 -1
  14. package/src/lib/family-advert-witness.ts +411 -0
  15. package/src/lib/fixture-run-observer.ts +97 -0
  16. package/src/lib/i18n-double.ts +99 -0
  17. package/src/lib/i18n-negotiation-witness.ts +173 -0
  18. package/src/lib/localized-content-witness.ts +49 -0
  19. package/src/lib/model-capability-insufficient-witness.ts +121 -0
  20. package/src/lib/prompt-double.ts +217 -0
  21. package/src/lib/prompt-events-witness.ts +211 -0
  22. package/src/lib/prompt-library-witness.ts +406 -0
  23. package/src/lib/provider-usage-witness.ts +63 -0
  24. package/src/lib/run-bounds-witness.ts +111 -0
  25. package/src/lib/run-graph-double.ts +144 -0
  26. package/src/lib/scratch-host.ts +4 -1
  27. package/src/lib/subworkflow-witness.ts +192 -0
  28. package/src/lib/table-schema-witness.ts +3 -1
  29. package/src/scenarios/v2-agent-runtime-advertisement.test.ts +51 -0
  30. package/src/scenarios/v2-ai-envelope-advertisement.test.ts +43 -0
  31. package/src/scenarios/v2-ai-envelope-shape-static.test.ts +93 -0
  32. package/src/scenarios/v2-ai-providers-advertisement.test.ts +94 -0
  33. package/src/scenarios/v2-artifact-created-static.test.ts +92 -0
  34. package/src/scenarios/v2-artifact-type-schema-url.test.ts +44 -0
  35. package/src/scenarios/v2-artifact-types-advertisement.test.ts +64 -0
  36. package/src/scenarios/v2-authorization-advertisement.test.ts +66 -0
  37. package/src/scenarios/v2-channel-presence-advertisement.test.ts +41 -0
  38. package/src/scenarios/v2-conversation-turn-model-provenance-advertisement.test.ts +41 -0
  39. package/src/scenarios/v2-conversation-turn-model-provenance.test.ts +110 -0
  40. package/src/scenarios/v2-credentials-advertisement.test.ts +66 -0
  41. package/src/scenarios/v2-data-residency-admission.test.ts +98 -0
  42. package/src/scenarios/v2-data-residency-advertisement.test.ts +52 -0
  43. package/src/scenarios/v2-dead-letter-advertisement.test.ts +54 -0
  44. package/src/scenarios/v2-dispatch-input-mapping.test.ts +70 -0
  45. package/src/scenarios/v2-dispatch-loop.test.ts +80 -0
  46. package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +40 -0
  47. package/src/scenarios/v2-envelope-reasoning-shape.test.ts +81 -0
  48. package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +77 -0
  49. package/src/scenarios/v2-envelope-variant-discriminator-static.test.ts +57 -0
  50. package/src/scenarios/v2-envelopes-advertisement.test.ts +85 -0
  51. package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
  52. package/src/scenarios/v2-i18n-negotiation.test.ts +91 -0
  53. package/src/scenarios/v2-limits-envelope-caps-advertisement.test.ts +61 -0
  54. package/src/scenarios/v2-localized-content-delivery.test.ts +136 -0
  55. package/src/scenarios/v2-media-inline-cap-static.test.ts +59 -0
  56. package/src/scenarios/v2-model-capabilities-advertisement.test.ts +61 -0
  57. package/src/scenarios/v2-model-capability-insufficient.test.ts +63 -0
  58. package/src/scenarios/v2-multi-party-conversation-advertisement.test.ts +52 -0
  59. package/src/scenarios/v2-node-pack-runtimes-advertisement.test.ts +52 -0
  60. package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +41 -0
  61. package/src/scenarios/v2-portability-advertisement.test.ts +63 -0
  62. package/src/scenarios/v2-prompt-all-four-kinds-events.test.ts +69 -0
  63. package/src/scenarios/v2-prompt-end-to-end-events.test.ts +71 -0
  64. package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
  65. package/src/scenarios/v2-prompt-list-and-fetch.test.ts +103 -0
  66. package/src/scenarios/v2-prompt-mutable-lifecycle.test.ts +130 -0
  67. package/src/scenarios/v2-prompt-pack-install.test.ts +78 -0
  68. package/src/scenarios/v2-prompt-render-deterministic.test.ts +87 -0
  69. package/src/scenarios/v2-prompt-resolution-chain-event.test.ts +46 -0
  70. package/src/scenarios/v2-prompt-template-shape.test.ts +112 -0
  71. package/src/scenarios/v2-provider-usage-advertisement.test.ts +61 -0
  72. package/src/scenarios/v2-provider-usage-emission.test.ts +50 -0
  73. package/src/scenarios/v2-provider-usage-payload-static.test.ts +92 -0
  74. package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +52 -0
  75. package/src/scenarios/v2-run-execution-bounds.test.ts +67 -0
  76. package/src/scenarios/v2-scheduling-advertisement.test.ts +55 -0
  77. package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +52 -0
  78. package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +121 -0
  79. package/src/scenarios/v2-sub-workflow-advertisement.test.ts +53 -0
  80. package/src/scenarios/v2-subworkflow-input-mapping.test.ts +74 -0
  81. package/src/scenarios/v2-subworkflow-linkage.test.ts +76 -0
  82. package/src/scenarios/v2-ui-plugins-advertisement.test.ts +85 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The `core.dispatch` witnesses, fixture ports through `POST /runs` and
3
+ * `events/poll`. No seam.
4
+ *
5
+ * Input mapping (`spec/v2/facets/agents.schema.json` `dispatchMapping`;
6
+ * `schemas/v2/dispatch-config.schema.json` `inputMapping`), on
7
+ * `conformance-dispatch-input-mapping[-no-default]` →
8
+ * `conformance-dispatch-input-mapping-child`. A host advertising
9
+ * `agents.dispatchMapping: true` honors `inputMapping`, "building child inputs
10
+ * from parent variables before dispatch": the child receives
11
+ * `inputs[childKey] = parentVariables[parentKey]`.
12
+ * projection the child's `inputs.childGreeting` is the parent's
13
+ * `parentName` ("Alice");
14
+ * unset an unset `parentName` projects to undefined — the child's
15
+ * `inputs.childGreeting` is absent, never `null` or a placeholder.
16
+ *
17
+ * Supervisor loop (`execution.md` §`multiAgent`, level 1 and level 5), on
18
+ * `conformance-dispatch-loop`:
19
+ * terminates the run completes; each turn records a decision as
20
+ * `runOrchestrator.decided` (`orchestrator.decided` at major 2),
21
+ * and the last is `terminate` — `terminate` completes the run,
22
+ * so no decision follows it;
23
+ * decisions every decision payload is well-formed (the decision checked
24
+ * branch by branch — see {@link decisionBranchValidator}), and
25
+ * its `agentId` is the same for the run's lifetime;
26
+ * iteration at `executionModel.version >= 5`, every decision carries
27
+ * `iteration`, 1-based, incremented by exactly 1 per turn.
28
+ *
29
+ * The drivers observe and assert nothing; the judges are pure, proven in
30
+ * `dispatch-witness.test.ts`.
31
+ */
32
+
33
+ import { isFixtureAdvertised } from './fixtures.js';
34
+ import type { MajorProfile } from './major-profile.js';
35
+ import {
36
+ completionsOf, finding, isRecord, observed, observeRun, readRun, runCompleted, skip,
37
+ type Ev, type Finding, type ObservedRun, type ObserveOpts, type Outcome, type RunRead, type Skip, type Validate,
38
+ } from './fixture-run-observer.js';
39
+
40
+ export const DISPATCH_PARENT_FIXTURE = 'conformance-dispatch-input-mapping';
41
+ export const DISPATCH_NO_DEFAULT_FIXTURE = 'conformance-dispatch-input-mapping-no-default';
42
+ export const DISPATCH_CHILD_FIXTURE = 'conformance-dispatch-input-mapping-child';
43
+ export const DISPATCH_NODE = 'dispatch';
44
+ export const LOOP_FIXTURE = 'conformance-dispatch-loop';
45
+
46
+ const DOC_MAP = 'agents.schema.json dispatchMapping; dispatch-config.schema.json inputMapping';
47
+ const DOC_LOOP = 'execution.md §multiAgent';
48
+
49
+ export interface DispatchObservation {
50
+ readonly parent: ObservedRun;
51
+ /** How the child was found: the parent's `node.dispatched`, or the dispatch node's `node.completed` outputs. */
52
+ readonly childRunId: string | undefined;
53
+ readonly child: RunRead | undefined;
54
+ }
55
+
56
+ /** `agents.dispatchMapping: true`, the parent fixture and the child fixture. */
57
+ export function dispatchMappingGate(profile: MajorProfile, doc: unknown, fixture: string): Skip | null {
58
+ const agents = profile.family(doc, 'agents');
59
+ if (agents === null) return skip('inapplicable', 'the host does not advertise agents');
60
+ if (agents['dispatchMapping'] !== true) return skip('inapplicable', 'the host does not advertise agents.dispatchMapping: true, so inputMapping does not bind it (it must refuse the field at registration instead)');
61
+ for (const id of [fixture, DISPATCH_CHILD_FIXTURE]) if (!isFixtureAdvertised(id)) return skip('inapplicable', `fixture ${id} is not advertised — the fixture is the opt-in`);
62
+ return null;
63
+ }
64
+
65
+ /** Find the dispatched child: `node.dispatched` naming the child workflow, else the dispatch node's outputs. */
66
+ export function locateChild(profile: MajorProfile, events: readonly Ev[], childWorkflowId: string): string | undefined {
67
+ const dispatchedType = profile.eventType('node.dispatched') ?? 'node.dispatched';
68
+ const d = events.find((e) => e.type === dispatchedType && e.payload['childWorkflowId'] === childWorkflowId && typeof e.payload['childRunId'] === 'string');
69
+ if (d !== undefined) return d.payload['childRunId'] as string;
70
+ for (const c of completionsOf(events, DISPATCH_NODE)) {
71
+ const out = c.payload['outputs'];
72
+ if (isRecord(out) && typeof out['childRunId'] === 'string' && out['childRunId'].length > 0) return out['childRunId'];
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /** Run a dispatch input-mapping fixture and follow it to the child. Asserts nothing. */
78
+ export async function driveDispatchMapping(profile: MajorProfile, doc: unknown, fixture: string, opts: ObserveOpts = {}): Promise<Skip | DispatchObservation> {
79
+ const g = dispatchMappingGate(profile, doc, fixture);
80
+ if (g) return g;
81
+ const parent = await observeRun(profile, fixture, opts);
82
+ const childRunId = locateChild(profile, parent.events, DISPATCH_CHILD_FIXTURE);
83
+ return { parent, childRunId, child: childRunId === undefined ? undefined : await readRun(profile, childRunId) };
84
+ }
85
+
86
+ function childInputs(o: DispatchObservation): Skip | { inputs: Record<string, unknown>; reached: Finding[] } {
87
+ if (o.parent.createStatus !== 201 && o.parent.createStatus !== 202) return { inputs: {}, reached: runCompleted(o.parent) };
88
+ if (o.childRunId === undefined) return skip('inapplicable', `no v2 rule requires a host to expose the dispatched child, and this one does not: the parent log names no ${DISPATCH_CHILD_FIXTURE} run (no node.dispatched, no dispatch-node outputs.childRunId), so its inputs cannot be read`);
89
+ const c = o.child;
90
+ return {
91
+ inputs: c !== undefined && isRecord(c.snapshot['inputs']) ? c.snapshot['inputs'] : {},
92
+ reached: [
93
+ ...runCompleted(o.parent),
94
+ finding(c?.status === 200, 'runs.md getRun', `the dispatched child run MUST be readable (GET answered ${String(c?.status)})`),
95
+ finding(c?.snapshot['status'] === 'completed', `fixtures.md ${DISPATCH_CHILD_FIXTURE}`, `the child run MUST end completed (got ${JSON.stringify(c?.snapshot['status'])})`),
96
+ ],
97
+ };
98
+ }
99
+
100
+ /** Leg: the child's inputs carry the parent's projected variable. Pure. */
101
+ export function judgeProjection(o: DispatchObservation): Outcome {
102
+ const c = childInputs(o);
103
+ if ('kind' in c) return c;
104
+ return observed([
105
+ ...c.reached,
106
+ finding(c.inputs['childGreeting'] === 'Alice', DOC_MAP, `the child MUST receive inputs.childGreeting = parentVariables.parentName ("Alice"; got ${JSON.stringify(c.inputs['childGreeting'])})`),
107
+ ]);
108
+ }
109
+
110
+ /** Leg: an unset parent variable projects to undefined — absent, never null. Pure. */
111
+ export function judgeUnsetProjection(o: DispatchObservation): Outcome {
112
+ const c = childInputs(o);
113
+ if ('kind' in c) return c;
114
+ return observed([
115
+ ...c.reached,
116
+ finding(c.inputs['childGreeting'] !== null, `${DOC_MAP} (mirrors execution.md §subWorkflow inputMapping: never null)`, 'an unset parent variable MUST NOT project as null'),
117
+ finding(!('childGreeting' in c.inputs), DOC_MAP, `inputs[childKey] = parentVariables[parentKey] is undefined for an unset parent variable: inputs.childGreeting MUST be absent (got ${JSON.stringify(c.inputs['childGreeting'])})`),
118
+ ]);
119
+ }
120
+
121
+ export interface LoopObservation { readonly run: ObservedRun; readonly decidedType: string; readonly version: number }
122
+
123
+ /** `multiAgent.executionModel.version >= 1`, the fixture, and this major's event name. */
124
+ export function loopGate(profile: MajorProfile, doc: unknown): Skip | { decidedType: string; version: number } {
125
+ const ma = profile.family(doc, 'multiAgent');
126
+ const em = isRecord(ma?.['executionModel']) ? ma['executionModel'] : undefined;
127
+ const version = em?.['version'];
128
+ if (typeof version !== 'number' || version < 1) return skip('inapplicable', 'the host does not advertise multiAgent.executionModel (version >= 1), so the supervisor loop does not bind it');
129
+ if (!isFixtureAdvertised(LOOP_FIXTURE)) return skip('inapplicable', `fixture ${LOOP_FIXTURE} is not advertised — the fixture is the opt-in`);
130
+ const decidedType = profile.eventType('runOrchestrator.decided');
131
+ if (decidedType === undefined) return skip('blocked', 'the event codemap is not on disk in this layout — the suite will not guess an event name');
132
+ return { decidedType, version };
133
+ }
134
+
135
+ /** Run the loop fixture. Asserts nothing. */
136
+ export async function driveLoop(profile: MajorProfile, doc: unknown, opts: ObserveOpts = {}): Promise<Skip | LoopObservation> {
137
+ const g = loopGate(profile, doc);
138
+ if ('kind' in g) return g;
139
+ return { run: await observeRun(profile, LOOP_FIXTURE, opts), ...g };
140
+ }
141
+
142
+ const decisions = (o: LoopObservation): Ev[] => o.run.events.filter((e) => e.type === o.decidedType);
143
+ const kindOf = (e: Ev): unknown => (isRecord(e.payload['decision']) ? e.payload['decision']['kind'] : undefined);
144
+
145
+ /** Leg: decisions are recorded, the last is `terminate`, and it completes the run. Pure. */
146
+ export function judgeLoopTerminates(o: LoopObservation): Outcome {
147
+ const ds = decisions(o);
148
+ const firstTerminate = ds.findIndex((e) => kindOf(e) === 'terminate');
149
+ return observed([
150
+ ...runCompleted(o.run),
151
+ finding(ds.length > 0, `${DOC_LOOP} (level 1)`, `each turn MUST record its decision as ${o.decidedType} (got none)`),
152
+ finding(ds.length > 0 && kindOf(ds[ds.length - 1]!) === 'terminate', `${DOC_LOOP} (level 1: terminate completes the run)`, `the completed loop's last decision MUST be terminate (got ${JSON.stringify(ds.map(kindOf))})`),
153
+ finding(firstTerminate === -1 || firstTerminate === ds.length - 1, `${DOC_LOOP} (level 1: terminate completes the run)`, `no decision may follow a terminate (got ${JSON.stringify(ds.map(kindOf))})`),
154
+ ]);
155
+ }
156
+
157
+ /**
158
+ * A validator for one `OrchestratorDecision`, by its `oneOf` branches.
159
+ *
160
+ * `schemas/v2/orchestrator-decision.schema.json` closes its ROOT with
161
+ * `additionalProperties: false` and declares no root `properties`, so the
162
+ * schema as a whole rejects every decision, `kind` included (the v1 root is
163
+ * open). Validating against it would convict every host. Until the corpus
164
+ * fixes it, a decision is valid when exactly one branch accepts it.
165
+ */
166
+ export function decisionBranchValidator(branch: (index: number) => Validate, branches = 3): Validate {
167
+ const vs = Array.from({ length: branches }, (_, i) => branch(i));
168
+ return (doc) => {
169
+ const results = vs.map((v) => v(doc));
170
+ const ok = results.filter((r) => r.ok).length === 1;
171
+ return { ok, errors: ok ? '' : results.map((r, i) => `branch ${i}: ${r.errors || 'accepted'}`).join(' | ') };
172
+ };
173
+ }
174
+
175
+ const PAYLOAD_KEYS = new Set(['agentId', 'decision', 'iteration', 'causationHostId']);
176
+ const AGENT_ID = /^[A-Za-z0-9._~:-]{1,128}$/;
177
+
178
+ /**
179
+ * Leg: every decision payload is well-formed (the `runOrchestratorDecided`
180
+ * def's closed key set and `agentId`; the decision by its branches), and the
181
+ * deciding agent never changes. Pure.
182
+ */
183
+ export function judgeDecisions(o: LoopObservation, validateDecision: Validate): Outcome {
184
+ const ds = decisions(o);
185
+ if (ds.length === 0) return skip('inapplicable', `the loop recorded no ${o.decidedType}, so there is nothing to judge here — the terminates leg convicts that`);
186
+ const agents = new Set(ds.map((e) => e.payload['agentId']));
187
+ const DEF = 'schemas run-event-payloads runOrchestratorDecided';
188
+ return observed([
189
+ ...ds.flatMap((e) => {
190
+ const extra = Object.keys(e.payload).filter((k) => !PAYLOAD_KEYS.has(k));
191
+ const agentId = e.payload['agentId'];
192
+ const v = validateDecision(e.payload['decision']);
193
+ return [
194
+ finding(extra.length === 0, DEF, `a ${o.decidedType} payload carries only agentId, decision, iteration, causationHostId (seq ${e.sequence}; extra ${JSON.stringify(extra)})`),
195
+ finding(typeof agentId === 'string' && AGENT_ID.test(agentId), `${DEF}.agentId`, `every ${o.decidedType} MUST carry agentId (seq ${e.sequence}; got ${JSON.stringify(agentId)})`),
196
+ finding(v.ok, 'schemas orchestrator-decision', `every decision MUST be one OrchestratorDecision (seq ${e.sequence}): ${v.errors}`),
197
+ ];
198
+ }),
199
+ finding(agents.size === 1, `${DEF}.agentId`, `orchestrator identity is set at the first decision and does not change (got ${JSON.stringify([...agents])})`),
200
+ ]);
201
+ }
202
+
203
+ /** Leg: at version >= 5, `iteration` is 1-based and +1 per turn. Pure. */
204
+ export function judgeIteration(o: LoopObservation): Outcome {
205
+ if (o.version < 5) return skip('inapplicable', `the host advertises multiAgent.executionModel.version ${o.version}; iteration binds from level 5`);
206
+ const ds = decisions(o);
207
+ if (ds.length === 0) return skip('inapplicable', `the loop recorded no ${o.decidedType}, so there is nothing to judge here — the terminates leg convicts that`);
208
+ const its = ds.map((e) => e.payload['iteration']);
209
+ return observed([
210
+ finding(its.every((n, i) => n === i + 1), `${DOC_LOOP} (level 5)`, `every ${o.decidedType} MUST carry iteration, 1-based and incremented by exactly 1 per turn (got ${JSON.stringify(its)})`),
211
+ ]);
212
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Static walkers over envelope payload schemas, shared by the v2 static twins
3
+ * `v2-envelope-tier-one-subset-static` and `v2-envelope-variant-discriminator-static`.
4
+ *
5
+ * The walk rules are the v1 rules unchanged (RFC 0030 §B, RFC 0031 §A); what
6
+ * moved at major 2 is where the schemas live (`schemas/v2/envelopes/`) and
7
+ * where a host's kind catalog is advertised (`supportedEnvelopes.kinds`, a
8
+ * family record, instead of a root `string[]`).
9
+ *
10
+ * Every function here is pure and is proven in both directions in
11
+ * `envelope-schema-static.test.ts`: a conforming schema yields no violation,
12
+ * and each defect yields the one it names.
13
+ */
14
+
15
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { SCHEMAS_DIR } from './paths.js';
18
+
19
+ export interface SchemaViolation { readonly path: string; readonly rule: string; readonly detail?: string }
20
+
21
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
22
+
23
+ /** The v2 envelope payload schema directory. */
24
+ export const V2_ENVELOPES_DIR = join(SCHEMAS_DIR, 'v2', 'envelopes');
25
+
26
+ /** The four kinds every engine recognizes (`events.md` §"AI envelopes"). */
27
+ export const UNIVERSAL_KINDS = ['clarification.request', 'schema.request', 'schema.response', 'error'] as const;
28
+
29
+ /** A v2 envelope payload schema by kind, or `null` when the corpus has none. */
30
+ export function loadV2EnvelopeSchema(kind: string): Record<string, unknown> | null {
31
+ const p = join(V2_ENVELOPES_DIR, `${kind}.schema.json`);
32
+ if (!existsSync(p)) return null;
33
+ const v: unknown = JSON.parse(readFileSync(p, 'utf8'));
34
+ return isRecord(v) ? v : null;
35
+ }
36
+
37
+ /** Every kind with a payload schema under `schemas/v2/envelopes/`. */
38
+ export function listV2EnvelopeKinds(): string[] {
39
+ if (!existsSync(V2_ENVELOPES_DIR)) return [];
40
+ return readdirSync(V2_ENVELOPES_DIR).filter((f) => f.endsWith('.schema.json')).map((f) => f.replace(/\.schema\.json$/, '')).sort();
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // RFC 0030 §B — the Tier-1 structured-output subset.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /** Keywords that fail across more than one Tier-1 vendor (Gemini drops `oneOf` silently). Checked in both modes. */
48
+ const LOAD_BEARING_KEYWORDS = ['oneOf', 'allOf', 'not', 'if', 'then', 'else', 'dependencies', 'prefixItems', 'propertyNames'] as const;
49
+ export const MAX_DEPTH = 5;
50
+ export const MAX_PROPERTIES = 100;
51
+
52
+ const hasType = (schema: Record<string, unknown>, t: string): boolean => schema['type'] === t || (Array.isArray(schema['type']) && (schema['type'] as unknown[]).includes(t));
53
+
54
+ /**
55
+ * Walk a payload schema for Tier-1 subset violations.
56
+ *
57
+ * `load-bearing`: only the rules that fail across several vendors — the floor
58
+ * every corpus schema is held to. `strict`: every rule of the OpenAI-strict ∩
59
+ * Anthropic-strict ∩ Gemini intersection, applied only when a host advertises
60
+ * `envelopes.tierOneSubsetCompliance: "strict"`.
61
+ */
62
+ export function tierOneViolations(schema: Record<string, unknown>, mode: 'load-bearing' | 'strict'): SchemaViolation[] {
63
+ const out: SchemaViolation[] = [];
64
+ const count = { n: 0 };
65
+ walkTierOne(schema, '#', 0, count, out, mode);
66
+ if (count.n > MAX_PROPERTIES) out.push({ path: '#', rule: 'max-property-count-100-exceeded', detail: `count=${count.n}` });
67
+ return out;
68
+ }
69
+
70
+ function walkTierOne(schema: Record<string, unknown>, path: string, depth: number, count: { n: number }, out: SchemaViolation[], mode: 'load-bearing' | 'strict'): void {
71
+ if (depth > MAX_DEPTH) { out.push({ path, rule: 'max-nesting-depth-5', detail: `depth=${depth}` }); return; }
72
+ for (const kw of LOAD_BEARING_KEYWORDS) if (kw in schema) out.push({ path, rule: 'forbidden-keyword', detail: kw });
73
+ if (Array.isArray(schema['anyOf'])) {
74
+ (schema['anyOf'] as unknown[]).forEach((b, i) => { if (isRecord(b)) walkTierOne(b, `${path}/anyOf/${i}`, depth + 1, count, out, mode); });
75
+ }
76
+ if (hasType(schema, 'object')) {
77
+ if (mode === 'strict' && schema['additionalProperties'] !== false) out.push({ path, rule: 'additionalProperties-must-be-false-on-object-strict-only' });
78
+ const props = isRecord(schema['properties']) ? schema['properties'] : {};
79
+ const required = Array.isArray(schema['required']) ? (schema['required'] as unknown[]) : [];
80
+ for (const [name, sub] of Object.entries(props)) {
81
+ count.n++;
82
+ if (mode === 'strict' && !required.includes(name)) out.push({ path: `${path}/properties/${name}`, rule: 'property-not-in-required-strict-mode-only' });
83
+ if (isRecord(sub)) walkTierOne(sub, `${path}/properties/${name}`, depth + 1, count, out, mode);
84
+ }
85
+ }
86
+ if (mode === 'strict') {
87
+ const bounded: Array<[boolean, readonly string[], string]> = [
88
+ [hasType(schema, 'string'), ['minLength', 'maxLength', 'pattern', 'format'], 'forbidden-string-constraint-strict-only'],
89
+ [hasType(schema, 'number') || hasType(schema, 'integer'), ['minimum', 'maximum', 'multipleOf'], 'forbidden-number-constraint-strict-only'],
90
+ [hasType(schema, 'array'), ['minItems', 'maxItems', 'uniqueItems'], 'forbidden-array-constraint-strict-only'],
91
+ ];
92
+ for (const [applies, kws, rule] of bounded) if (applies) for (const kw of kws) if (kw in schema) out.push({ path, rule, detail: kw });
93
+ }
94
+ if (hasType(schema, 'array') && isRecord(schema['items'])) walkTierOne(schema['items'], `${path}/items`, depth + 1, count, out, mode);
95
+ const defs = isRecord(schema['$defs']) ? schema['$defs'] : {};
96
+ for (const [name, sub] of Object.entries(defs)) if (isRecord(sub)) walkTierOne(sub, `${path}/$defs/${name}`, depth + 1, count, out, mode);
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // RFC 0031 §A — variant payloads: no `oneOf`; every `anyOf` branch discriminated.
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /** `oneOf` anywhere in a payload schema. */
104
+ export function oneOfViolations(schema: unknown, path = '#'): SchemaViolation[] {
105
+ const out: SchemaViolation[] = [];
106
+ const walk = (s: unknown, p: string): void => {
107
+ if (Array.isArray(s)) { s.forEach((x, i) => walk(x, `${p}/${i}`)); return; }
108
+ if (!isRecord(s)) return;
109
+ if ('oneOf' in s) out.push({ path: p, rule: 'oneOf-forbidden', detail: 'use anyOf with a single-string-enum discriminator (RFC 0031 §A)' });
110
+ for (const [k, v] of Object.entries(s)) walk(v, `${p}/${k}`);
111
+ };
112
+ walk(schema, path);
113
+ return out;
114
+ }
115
+
116
+ /**
117
+ * A branch is discriminated when one of its `required` properties is
118
+ * `type: string` with an `enum` of exactly one value. A `$ref` branch is
119
+ * accepted unresolved, as at v1: the referenced shape is walked where it is
120
+ * defined, and a version-selected union (one branch per `schemaVersion`, as
121
+ * `ui.a2ui-surface` declares) is not a payload-field discrimination at all.
122
+ */
123
+ export function branchDiscriminated(branch: unknown): boolean {
124
+ if (!isRecord(branch)) return false;
125
+ if ('$ref' in branch) return true;
126
+ const required = Array.isArray(branch['required']) ? (branch['required'] as unknown[]) : [];
127
+ const props = isRecord(branch['properties']) ? branch['properties'] : {};
128
+ return required.some((name) => {
129
+ const p = typeof name === 'string' ? props[name] : undefined;
130
+ return isRecord(p) && p['type'] === 'string' && Array.isArray(p['enum']) && (p['enum'] as unknown[]).length === 1;
131
+ });
132
+ }
133
+
134
+ /** Whether a branch can match a JSON object: an explicit object type, a `$ref`, or object keywords with no type. */
135
+ function admitsObject(branch: unknown): boolean {
136
+ if (!isRecord(branch)) return false;
137
+ if ('$ref' in branch) return true;
138
+ const t = branch['type'];
139
+ if (t === undefined) return 'properties' in branch || 'required' in branch;
140
+ return t === 'object' || (Array.isArray(t) && t.includes('object'));
141
+ }
142
+
143
+ /**
144
+ * Every `anyOf` branch, at any depth, that carries no single-string-enum
145
+ * discriminator — in a VARIANT union, one where two or more branches can be
146
+ * objects. A union with at most one object branch (`string | binding`,
147
+ * `string | number | boolean | array | binding`, as `ui.a2ui-surface`
148
+ * payload v2 declares) is a type union: JSON type alone selects the branch, no
149
+ * field is inspected, and a scalar branch has no property to discriminate on.
150
+ * RFC 0031 §A governs variant payloads; it is not read as forbidding these.
151
+ */
152
+ export function anyOfDiscriminatorViolations(schema: unknown, path = '#'): SchemaViolation[] {
153
+ const out: SchemaViolation[] = [];
154
+ const walk = (s: unknown, p: string): void => {
155
+ if (Array.isArray(s)) { s.forEach((x, i) => walk(x, `${p}/${i}`)); return; }
156
+ if (!isRecord(s)) return;
157
+ if (Array.isArray(s['anyOf']) && (s['anyOf'] as unknown[]).filter(admitsObject).length >= 2) {
158
+ (s['anyOf'] as unknown[]).forEach((b, i) => {
159
+ if (admitsObject(b) && !branchDiscriminated(b)) out.push({ path: `${p}/anyOf/${i}`, rule: 'anyOf-branch-missing-discriminator', detail: 'a branch MUST declare a single-string-enum discriminator property in required (RFC 0031 §A)' });
160
+ });
161
+ }
162
+ for (const [k, v] of Object.entries(s)) walk(v, `${p}/${k}`);
163
+ };
164
+ walk(schema, path);
165
+ return out;
166
+ }
@@ -75,7 +75,7 @@ export async function drive(profile: MajorProfile, doc: unknown): Promise<FacetR
75
75
 
76
76
  // A host that accepted the create started a run. Stop it, so a failing row leaves none behind.
77
77
  const runId = (res.json as { runId?: unknown } | undefined)?.runId;
78
- if (typeof runId === 'string') await http(() => driver.post(`${profile.runsPath}/${encodeURIComponent(runId)}/cancel`, {}));
78
+ if (typeof runId === 'string') await http(() => driver.post(`${profile.runsPath}/${profile.idSegment(runId)}/cancel`, {}));
79
79
 
80
80
  return { kind: 'observed', observation: { facet, create: { status: res.status, code: readErrorCode(res.json) ?? undefined } } };
81
81
  }