@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,63 @@
1
+ /**
2
+ * The `provider.usage` emission witness (`events.md` §`providerUsage`): "A
3
+ * host advertising `providerUsage` MUST emit exactly one `provider.usage` per
4
+ * LLM provider invocation, before that node's `node.completed`."
5
+ *
6
+ * Driven on `conformance-context-budget-live` — a supervisor on the host's LIVE
7
+ * model (the fixture's catalog entry forbids a mock), so a completed run made
8
+ * at least one provider invocation. From outside, the number of invocations
9
+ * is unknowable, so "exactly one per invocation" is NOT asserted; what is:
10
+ * emitted at least one `provider.usage` is on the log;
11
+ * ordering each one precedes a `node.completed` of the node it names (or,
12
+ * when it names none, some `node.completed`);
13
+ * payload each payload validates against the v2 `providerUsage` def (whose
14
+ * closed shape also keeps credential refs and prompt text out).
15
+ *
16
+ * {@link driveUsage} observes and asserts nothing; {@link judgeUsage} is pure,
17
+ * proven in `provider-usage-witness.test.ts`.
18
+ */
19
+
20
+ import { isFixtureAdvertised } from './fixtures.js';
21
+ import type { MajorProfile } from './major-profile.js';
22
+ import { finding, observed, observeRun, skip, type ObservedRun, type ObserveOpts, type Outcome, type Skip, type Validate } from './fixture-run-observer.js';
23
+
24
+ export const LIVE_FIXTURE = 'conformance-context-budget-live';
25
+ const DOC = 'events.md §providerUsage';
26
+
27
+ export interface UsageObservation { readonly run: ObservedRun; readonly usageType: string }
28
+
29
+ /** The `providerUsage` record, the live fixture, and this major's event name. */
30
+ export function usageGate(profile: MajorProfile, doc: unknown): Skip | { usageType: string } {
31
+ if (profile.family(doc, 'providerUsage') === null) return skip('inapplicable', 'the host does not advertise providerUsage');
32
+ if (!isFixtureAdvertised(LIVE_FIXTURE)) return skip('inapplicable', `fixture ${LIVE_FIXTURE} is not advertised — the fixture is the opt-in (a host whose supervisor has no live model must not advertise it)`);
33
+ const usageType = profile.eventType('provider.usage');
34
+ if (usageType === undefined) return skip('blocked', 'the event codemap is not on disk in this layout — the suite will not guess an event name');
35
+ return { usageType };
36
+ }
37
+
38
+ /** Run the live fixture. Asserts nothing. */
39
+ export async function driveUsage(profile: MajorProfile, doc: unknown, opts: ObserveOpts = {}): Promise<Skip | UsageObservation> {
40
+ const g = usageGate(profile, doc);
41
+ if ('kind' in g) return g;
42
+ return { run: await observeRun(profile, LIVE_FIXTURE, { timeoutMs: 300_000, pollMs: 1000, ...opts }), ...g };
43
+ }
44
+
45
+ /** Emission, ordering and payload shape. Pure. */
46
+ export function judgeUsage(o: UsageObservation, validate: Validate): Outcome {
47
+ const usage = o.run.events.filter((e) => e.type === o.usageType);
48
+ const completions = o.run.events.filter((e) => e.type === 'node.completed');
49
+ if (o.run.createStatus !== 201 && o.run.createStatus !== 202) {
50
+ return observed([finding(false, 'runs.md createRun', `createRun of ${LIVE_FIXTURE} MUST be accepted (got ${o.run.createStatus})`)]);
51
+ }
52
+ if (usage.length === 0 && o.run.terminalStatus !== 'completed') {
53
+ return skip('blocked', `the live run ended ${String(o.run.terminalStatus)} with no ${o.usageType} — no provider invocation is shown to have happened`);
54
+ }
55
+ return observed([
56
+ finding(usage.length > 0, DOC, `a completed run of a live-model supervisor MUST carry at least one ${o.usageType} (got none)`),
57
+ ...usage.map((u) => {
58
+ const after = completions.some((c) => c.sequence > u.sequence && (u.nodeId === undefined || c.nodeId === u.nodeId));
59
+ return finding(after, DOC, `${o.usageType} (seq ${u.sequence}${u.nodeId === undefined ? '' : `, node ${u.nodeId}`}) MUST precede that node's node.completed`);
60
+ }),
61
+ ...usage.map((u) => { const v = validate(u.payload); return finding(v.ok, 'schemas run-event-payloads providerUsage', `every ${o.usageType} payload MUST validate (seq ${u.sequence}): ${v.errors}`); }),
62
+ ]);
63
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The run execution-bounds witness (`runs.md` §`run` section; RFC 0058), shared
3
+ * by every major that gives a run a wire surface for `runTimeoutMs`.
4
+ *
5
+ * Three legs:
6
+ * shape advertised `limits.maxRunDurationMs` is an integer ≥ 1000 and
7
+ * `limits.maxLoopIterations` an integer ≥ 1, when present;
8
+ * refused an out-of-range `runTimeoutMs` (0, below the schema minimum of 1)
9
+ * is refused `400 validation_error` at create — unaided;
10
+ * breach a run of the `conformance-run-duration-breach` fixture with a
11
+ * 1 s `runTimeoutMs` ends `failed` with `run_timeout` and emits
12
+ * `cap.breached { kind: 'run-duration' }` with `observed > limit`.
13
+ *
14
+ * Two halves: the drivers observe and assert nothing; {@link judgeBreach} is pure,
15
+ * proven in `run-bounds-witness.test.ts`.
16
+ */
17
+
18
+ import { driver } from './driver.js';
19
+ import { readErrorCode } from './error-envelope.js';
20
+ import { isFixtureAdvertised } from './fixtures.js';
21
+ import type { MajorProfile } from './major-profile.js';
22
+
23
+ export const TIMEOUT_FIXTURE = 'conformance-run-duration-breach';
24
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
25
+
26
+ export interface BoundsFinding { readonly ok: boolean; readonly doc: string; readonly message: string }
27
+ export type BoundsOutcome =
28
+ | { readonly kind: 'skip'; readonly disposition: 'inapplicable' | 'blocked'; readonly reason: string }
29
+ | { readonly kind: 'observed'; readonly findings: readonly BoundsFinding[] };
30
+
31
+ const DOC = 'runs.md §run section';
32
+ const f = (ok: boolean, message: string): BoundsFinding => ({ ok, doc: DOC, message });
33
+
34
+ /** The `run` section fragment that carries a `runTimeoutMs` at this major. */
35
+ function runTimeout(profile: MajorProfile, ms: number): Record<string, unknown> {
36
+ return profile.major >= 2 ? { configurable: { version: 1, run: { runTimeoutMs: ms } } } : { configurable: { runTimeoutMs: ms } };
37
+ }
38
+
39
+ /** Leg 1, shape: judged from the discovery record alone. */
40
+ export function shapeLeg(profile: MajorProfile, discovery: unknown): BoundsOutcome {
41
+ const limits = profile.family(discovery, 'limits');
42
+ if (limits === null) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host does not advertise limits' };
43
+ const findings: BoundsFinding[] = [];
44
+ const dur = limits['maxRunDurationMs'];
45
+ const loops = limits['maxLoopIterations'];
46
+ if (dur !== undefined) findings.push(f(Number.isInteger(dur) && (dur as number) >= 1000, `limits.maxRunDurationMs MUST be an integer >= 1000 when present (got ${JSON.stringify(dur)})`));
47
+ if (loops !== undefined) findings.push(f(Number.isInteger(loops) && (loops as number) >= 1, `limits.maxLoopIterations MUST be an integer >= 1 when present (got ${JSON.stringify(loops)})`));
48
+ if (findings.length === 0) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host advertises neither limits.maxRunDurationMs nor limits.maxLoopIterations' };
49
+ return { kind: 'observed', findings };
50
+ }
51
+
52
+ /** Leg 2, refused: an out-of-range runTimeoutMs is refused at create. Unaided: any advertised fixture names a valid workflow. */
53
+ export async function refusedLeg(profile: MajorProfile, workflowId: string | undefined): Promise<BoundsOutcome> {
54
+ if (workflowId === undefined) return { kind: 'skip', disposition: 'inapplicable', reason: 'the host advertises no fixture workflow to name in a create, so a refusal cannot be told from an unknown workflow' };
55
+ const res = await driver.post(profile.runsPath, { workflowId, ...runTimeout(profile, 0) });
56
+ if (res.status === 201 || res.status === 202) {
57
+ const runId = isRecord(res.json) ? res.json['runId'] : undefined;
58
+ if (typeof runId === 'string') await driver.post(`${profile.runsPath}/${profile.idSegment(runId)}/cancel`, {}).catch(() => undefined);
59
+ }
60
+ return { kind: 'observed', findings: [
61
+ f(res.status === 400, `runTimeoutMs: 0 (below the minimum of 1) MUST be refused 400 at create (got ${res.status})`),
62
+ f(readErrorCode(res.json) === 'validation_error', `the refusal MUST carry validation_error (got ${String(readErrorCode(res.json))})`),
63
+ ] };
64
+ }
65
+
66
+ export interface BreachObservation {
67
+ readonly createStatus: number;
68
+ readonly terminalStatus: string | undefined;
69
+ readonly errorCode: string | undefined;
70
+ readonly breach: Record<string, unknown> | undefined;
71
+ }
72
+
73
+ /** Leg 3, breach, observed: a fixture run with a 1 s runTimeoutMs. Asserts nothing. */
74
+ export async function driveBreach(profile: MajorProfile, opts: { timeoutMs?: number; pollMs?: number } = {}): Promise<Extract<BoundsOutcome, { kind: 'skip' }> | BreachObservation> {
75
+ if (!isFixtureAdvertised(TIMEOUT_FIXTURE)) return { kind: 'skip', disposition: 'inapplicable', reason: `fixture ${TIMEOUT_FIXTURE} is not advertised — the host does not claim a run long enough to breach a 1 s timeout` };
76
+ const created = await driver.post(profile.runsPath, { workflowId: TIMEOUT_FIXTURE, ...runTimeout(profile, 1000) });
77
+ const runId = isRecord(created.json) ? created.json['runId'] : undefined;
78
+ if ((created.status !== 201 && created.status !== 202) || typeof runId !== 'string') {
79
+ return { createStatus: created.status, terminalStatus: undefined, errorCode: readErrorCode(created.json), breach: undefined };
80
+ }
81
+ const deadline = Date.now() + (opts.timeoutMs ?? 20_000);
82
+ let snap: Record<string, unknown> = {};
83
+ for (;;) {
84
+ const r = await driver.get(`${profile.runsPath}/${profile.idSegment(runId)}`);
85
+ snap = isRecord(r.json) ? r.json : {};
86
+ if (['completed', 'failed', 'cancelled'].includes(String(snap['status'])) || Date.now() > deadline) break;
87
+ await new Promise((res) => setTimeout(res, opts.pollMs ?? 500));
88
+ }
89
+ const poll = await driver.get(`${profile.runsPath}/${profile.idSegment(runId)}/events/poll`);
90
+ const events = ((poll.json as { events?: unknown } | undefined)?.events as Array<Record<string, unknown>> | undefined) ?? [];
91
+ const capType = profile.eventType('cap.breached') ?? 'cap.breached';
92
+ const breach = events.find((e) => e['type'] === capType);
93
+ const err = isRecord(snap['error']) ? snap['error'] : undefined;
94
+ return { createStatus: created.status, terminalStatus: typeof snap['status'] === 'string' ? (snap['status'] as string) : undefined, errorCode: typeof err?.['code'] === 'string' ? (err['code'] as string) : undefined, breach: isRecord(breach?.['payload']) ? (breach!['payload'] as Record<string, unknown>) : undefined };
95
+ }
96
+
97
+ /** Leg 3, breach, judged. Pure. */
98
+ export function judgeBreach(o: BreachObservation): BoundsFinding[] {
99
+ const out = [f(o.createStatus === 201 || o.createStatus === 202, `a create with runTimeoutMs: 1000 MUST be accepted (got ${o.createStatus})`)];
100
+ if (!out[0]!.ok) return out;
101
+ const observed = o.breach?.['observed'];
102
+ const limit = o.breach?.['limit'];
103
+ out.push(
104
+ f(o.terminalStatus === 'failed', `a run exceeding runTimeoutMs MUST end failed (got ${String(o.terminalStatus)})`),
105
+ f(o.errorCode === 'run_timeout', `the run's error.code MUST be run_timeout (got ${String(o.errorCode)})`),
106
+ f(o.breach !== undefined, 'a cap.breached event MUST be emitted on the breach'),
107
+ f(o.breach?.['kind'] === 'run-duration', `cap.breached.kind MUST be run-duration (got ${String(o.breach?.['kind'])})`),
108
+ f(typeof observed === 'number' && typeof limit === 'number' && observed > limit, `cap.breached.observed MUST exceed limit (got observed=${String(observed)}, limit=${String(limit)})`),
109
+ );
110
+ return out;
111
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A run-graph double: the smallest HTTP server the wave-2 fixture witnesses
3
+ * (sub-workflow linkage and input mapping, dispatch input mapping, the
4
+ * dispatch loop, provider usage) and the artifact schema-URL witness can be
5
+ * proven against, in both directions.
6
+ *
7
+ * `ScratchHost` plays one run per create. These witnesses follow a parent run
8
+ * to the child it started, read the child's snapshot (`variables`, `inputs`)
9
+ * and its `getRunAncestry` answer, and fetch paths outside the run surface,
10
+ * so this double lets a case script a parent that spawns children, and answer
11
+ * any other GET.
12
+ *
13
+ * It is a test double, not a host: it executes nothing, and nothing it does is
14
+ * evidence about any implementation. Ids are tenant-bound (`<tenant>/<opaque>`,
15
+ * `ids.schema.json#/$defs/runId`) and accepted `~`-projected or percent-encoded.
16
+ */
17
+
18
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
19
+ import type { AddressInfo } from 'node:net';
20
+ import { unprojectBoundId } from './bound-id.js';
21
+
22
+ export interface DoubleEvent {
23
+ readonly type: string;
24
+ /** The envelope's top-level `nodeId`. */
25
+ readonly nodeId?: string | undefined;
26
+ readonly payload?: Record<string, unknown> | undefined;
27
+ }
28
+ export interface DoubleRun {
29
+ readonly workflowId?: string;
30
+ readonly status: string;
31
+ readonly variables?: Record<string, unknown> | undefined;
32
+ readonly inputs?: Record<string, unknown> | undefined;
33
+ readonly events?: readonly DoubleEvent[];
34
+ /** The `GET /runs/{runId}/ancestry` answer, given the run's own id. Unset: 404. */
35
+ readonly ancestry?: ((self: string) => { readonly status: number; readonly body: unknown }) | undefined;
36
+ }
37
+ export interface RawAnswer { readonly status: number; readonly headers?: Readonly<Record<string, string>>; readonly body: string }
38
+ export interface DoubleOptions {
39
+ readonly discovery: Readonly<Record<string, unknown>>;
40
+ /**
41
+ * The run a create makes. `spawn` stores a further run (a child) and returns
42
+ * its id; `self` is the id the created run will carry. Unset: every create is 404.
43
+ */
44
+ readonly onCreate?: ((workflowId: string, spawn: (run: DoubleRun) => string, self: string) => DoubleRun | undefined) | undefined;
45
+ /** Any other GET, by path. `undefined` is 404. */
46
+ readonly onGet?: ((path: string) => RawAnswer | undefined) | undefined;
47
+ }
48
+
49
+ export const DOUBLE_TENANT = 'tenant-a';
50
+
51
+ export class RunGraphDouble {
52
+ private server: Server | undefined;
53
+ private readonly runs = new Map<string, DoubleRun>();
54
+ private seq = 0;
55
+ url = '';
56
+
57
+ constructor(private opts: DoubleOptions) {}
58
+
59
+ /** Swap behaviour between cases. The address stays the same, since the driver caches its base URL per file. */
60
+ reconfigure(next: Partial<DoubleOptions>): void {
61
+ this.opts = { ...this.opts, ...next };
62
+ this.runs.clear();
63
+ }
64
+
65
+ get discovery(): Readonly<Record<string, unknown>> { return this.opts.discovery; }
66
+
67
+ async start(): Promise<string> {
68
+ this.server = createServer((req, res) => { void this.handle(req, res); });
69
+ await new Promise<void>((r) => this.server!.listen(0, '127.0.0.1', r));
70
+ this.url = `http://127.0.0.1:${(this.server.address() as AddressInfo).port}`;
71
+ return this.url;
72
+ }
73
+
74
+ async stop(): Promise<void> {
75
+ const s = this.server;
76
+ if (s === undefined) return;
77
+ s.closeAllConnections();
78
+ await new Promise<void>((r) => s.close(() => r()));
79
+ }
80
+
81
+ private mint(): string { return `${DOUBLE_TENANT}/run-${String(++this.seq).padStart(16, '0')}`; }
82
+
83
+ private send(res: ServerResponse, status: number, body: unknown, headers: Readonly<Record<string, string>> = {}): void {
84
+ res.writeHead(status, { 'content-type': 'application/json', ...headers });
85
+ res.end(typeof body === 'string' ? body : JSON.stringify(body));
86
+ }
87
+
88
+ private async body(req: IncomingMessage): Promise<Record<string, unknown>> {
89
+ const chunks: Buffer[] = [];
90
+ for await (const c of req) chunks.push(c as Buffer);
91
+ try {
92
+ const v: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
93
+ return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
94
+ } catch { return {}; }
95
+ }
96
+
97
+ private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
98
+ const path = (req.url ?? '/').split('?')[0] ?? '/';
99
+ if (req.method === 'GET' && path === '/.well-known/openwop') return this.send(res, 200, this.opts.discovery);
100
+
101
+ if (req.method === 'POST' && path === '/runs') {
102
+ const workflowId = String((await this.body(req))['workflowId'] ?? '');
103
+ const self = this.mint();
104
+ const run = this.opts.onCreate?.(workflowId, (child) => { const id = this.mint(); this.runs.set(id, child); return id; }, self);
105
+ if (run === undefined) return this.send(res, 404, { error: 'not_found', message: `no workflow ${workflowId}` });
106
+ this.runs.set(self, { workflowId, ...run });
107
+ return this.send(res, 201, { runId: self, status: 'running' });
108
+ }
109
+
110
+ const m = /^\/runs\/([^/]+)(\/events\/poll|\/ancestry)?$/.exec(path);
111
+ if (req.method === 'GET' && m) {
112
+ const seg = m[1] as string;
113
+ const id = seg.includes('~') ? unprojectBoundId(seg) : decodeURIComponent(seg);
114
+ const run = this.runs.get(id);
115
+ if (run === undefined) return this.send(res, 404, { error: 'not_found', message: 'no such run' });
116
+ if (m[2] === '/ancestry') {
117
+ const a = run.ancestry?.(id);
118
+ return a === undefined ? this.send(res, 404, { error: 'not_found', message: 'ancestry not served' }) : this.send(res, a.status, a.body);
119
+ }
120
+ if (m[2] === '/events/poll') {
121
+ const events = (run.events ?? []).map((e, i) => ({
122
+ eventId: `e${i + 1}`, runId: id, type: e.type, ...(e.nodeId === undefined ? {} : { nodeId: e.nodeId }),
123
+ payload: e.payload ?? {}, sequence: i + 1, schemaVersion: 2, timestamp: new Date(0).toISOString(),
124
+ }));
125
+ return this.send(res, 200, { events });
126
+ }
127
+ return this.send(res, 200, {
128
+ runId: id, workflowId: run.workflowId ?? 'double', status: run.status,
129
+ ...(run.variables === undefined ? {} : { variables: run.variables }),
130
+ ...(run.inputs === undefined ? {} : { inputs: run.inputs }),
131
+ });
132
+ }
133
+
134
+ if (req.method === 'GET') {
135
+ const raw = this.opts.onGet?.(path);
136
+ if (raw !== undefined) {
137
+ res.writeHead(raw.status, { ...(raw.headers ?? {}) });
138
+ res.end(raw.body);
139
+ return;
140
+ }
141
+ }
142
+ return this.send(res, 404, { error: 'not_found', message: 'no such route' });
143
+ }
144
+ }
@@ -16,6 +16,7 @@
16
16
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
17
17
  import type { AddressInfo } from 'node:net';
18
18
  import type { MajorProfile } from './major-profile.js';
19
+ import { unprojectBoundId } from './bound-id.js';
19
20
 
20
21
  export interface ScriptedEvent { readonly type: string; readonly payload?: Record<string, unknown> | undefined }
21
22
  export interface ScriptedRun {
@@ -112,7 +113,9 @@ export class ScratchHost {
112
113
  if (req.method === 'POST' && path === `${runs}:bulk-cancel`) return this.send(res, 200, { results: [] });
113
114
 
114
115
  const m = new RegExp(`^${runs}/([^/]+)(/events/poll|/events|/cancel)?$`).exec(path);
115
- const stored = m ? this.runs.get(decodeURIComponent(m[1] as string)) : undefined;
116
+ // A bound id arrives `~`-projected (identity.md §5) or percent-encoded; accept both.
117
+ const seg = m ? (m[1] as string) : '';
118
+ const stored = m ? this.runs.get(seg.includes('~') ? unprojectBoundId(seg) : decodeURIComponent(seg)) : undefined;
116
119
  if (!m || stored === undefined) return this.send(res, 404, { error: 'not_found', message: 'no such route or run' });
117
120
  const tail = m[2];
118
121
  if (tail === '/cancel') return this.send(res, 200, { status: 'cancelled' });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The `core.subWorkflow` witness (`execution.md` §`subWorkflow`), a fixture
3
+ * port through `POST /runs` and `events/poll`. No seam.
4
+ *
5
+ * Linkage, on `conformance-subworkflow-parent` → `conformance-subworkflow-child`:
6
+ * output the parent's `node.completed` for `subwf-call` carries
7
+ * `outputs.childRunId` and `outputs.childStatus`
8
+ * (`completed | failed | cancelled`), and the status names the
9
+ * child's real terminal status;
10
+ * outputMapping after the child completes, the mapped variable is copied
11
+ * into the parent (`childOutcome` ← child `childResult`);
12
+ * ancestry where `getRunAncestry` is served (advertised by
13
+ * `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`),
14
+ * the child's `parent` is the parent run with
15
+ * `cause: "core.subWorkflow"`.
16
+ * The v1 `parentRunId`/`parentNodeId` child-linkage leg is RETIRED at v2
17
+ * (`spec/v1/migrations.json` C4.18): `parentRunId` is fork lineage, not this link.
18
+ *
19
+ * Input mapping, on `conformance-subworkflow-input-mapping[-no-default]` →
20
+ * `conformance-subworkflow-input-mapping-child` (child default `receivedPrdId: "baked-in"`):
21
+ * seed the child's variable bag is seeded: `receivedPrdId` is present;
22
+ * override the mapping lands after and over the default: `"prd-1"`;
23
+ * unset an unset parent variable arrives undefined, never an error or
24
+ * `null` (the run completes; the child's value is absent or its
25
+ * own default).
26
+ *
27
+ * The drivers observe and assert nothing; the judges are pure, proven in
28
+ * `subworkflow-witness.test.ts`.
29
+ */
30
+
31
+ import { driver } from './driver.js';
32
+ import { isFixtureAdvertised } from './fixtures.js';
33
+ import type { MajorProfile } from './major-profile.js';
34
+ import {
35
+ completionsOf, finding, isRecord, observed, observeRun, readRun, runCompleted, skip,
36
+ type Finding, type ObservedRun, type ObserveOpts, type Outcome, type RunRead, type Skip, type Validate,
37
+ } from './fixture-run-observer.js';
38
+
39
+ export const PARENT_FIXTURE = 'conformance-subworkflow-parent';
40
+ export const CHILD_FIXTURE = 'conformance-subworkflow-child';
41
+ export const IM_PARENT_FIXTURE = 'conformance-subworkflow-input-mapping';
42
+ export const IM_NO_DEFAULT_FIXTURE = 'conformance-subworkflow-input-mapping-no-default';
43
+ export const IM_CHILD_FIXTURE = 'conformance-subworkflow-input-mapping-child';
44
+ export const SUBWF_NODE = 'subwf-call';
45
+
46
+ const DOC = 'execution.md §subWorkflow';
47
+ const CHILD_STATUSES = new Set(['completed', 'failed', 'cancelled']);
48
+
49
+ /** The child a `core.subWorkflow` node started, as its `node.completed` names it. */
50
+ export interface ChildLink {
51
+ readonly outputs: Record<string, unknown> | undefined;
52
+ readonly childRunId: string | undefined;
53
+ readonly child: RunRead | undefined;
54
+ }
55
+
56
+ export interface LinkageObservation extends ChildLink {
57
+ readonly parent: ObservedRun;
58
+ /** `undefined`: the host does not advertise `getRunAncestry`, so the leg does not bind. */
59
+ readonly ancestry: { readonly status: number; readonly json: unknown } | undefined;
60
+ }
61
+ export interface MappingObservation extends ChildLink { readonly parent: ObservedRun }
62
+
63
+ /** Whether the host advertises `getRunAncestry` (`runs.md` §Surface gate). */
64
+ export function ancestryAdvertised(profile: MajorProfile, doc: unknown): boolean {
65
+ const ma = profile.family(doc, 'multiAgent');
66
+ const em = isRecord(ma?.['executionModel']) ? ma['executionModel'] : undefined;
67
+ const chc = isRecord(em?.['crossHostCausation']) ? em['crossHostCausation'] : undefined;
68
+ return chc?.['ancestryEndpointSupported'] === true;
69
+ }
70
+
71
+ function fixturesGate(...ids: string[]): Skip | null {
72
+ for (const id of ids) if (!isFixtureAdvertised(id)) return skip('inapplicable', `fixture ${id} is not advertised — the fixture is the opt-in`);
73
+ return null;
74
+ }
75
+
76
+ async function followChild(profile: MajorProfile, parent: ObservedRun): Promise<ChildLink> {
77
+ const done = completionsOf(parent.events, SUBWF_NODE)[0];
78
+ const outputs = isRecord(done?.payload['outputs']) ? done.payload['outputs'] : undefined;
79
+ const childRunId = typeof outputs?.['childRunId'] === 'string' && outputs['childRunId'].length > 0 ? outputs['childRunId'] : undefined;
80
+ return { outputs, childRunId, child: childRunId === undefined ? undefined : await readRun(profile, childRunId) };
81
+ }
82
+
83
+ /** Run the linkage fixture and follow it to the child. Asserts nothing. */
84
+ export async function driveLinkage(profile: MajorProfile, doc: unknown, opts: ObserveOpts = {}): Promise<Skip | LinkageObservation> {
85
+ const g = fixturesGate(PARENT_FIXTURE, CHILD_FIXTURE);
86
+ if (g) return g;
87
+ const parent = await observeRun(profile, PARENT_FIXTURE, opts);
88
+ const link = await followChild(profile, parent);
89
+ let ancestry: LinkageObservation['ancestry'];
90
+ if (ancestryAdvertised(profile, doc) && link.childRunId !== undefined) {
91
+ const r = await driver.get(`${profile.runsPath}/${profile.idSegment(link.childRunId)}/ancestry`);
92
+ ancestry = { status: r.status, json: r.json };
93
+ }
94
+ return { parent, ...link, ancestry };
95
+ }
96
+
97
+ /** Run an input-mapping fixture and follow it to the child. Asserts nothing. */
98
+ export async function driveInputMapping(profile: MajorProfile, fixture: string, opts: ObserveOpts = {}): Promise<Skip | MappingObservation> {
99
+ const g = fixturesGate(fixture, IM_CHILD_FIXTURE);
100
+ if (g) return g;
101
+ const parent = await observeRun(profile, fixture, opts);
102
+ return { parent, ...(await followChild(profile, parent)) };
103
+ }
104
+
105
+ /** Leg: `node.completed` carries `outputs.childRunId` and `outputs.childStatus`. Pure. */
106
+ export function judgeOutput(o: LinkageObservation): Outcome {
107
+ const out: Finding[] = runCompleted(o.parent);
108
+ const status = o.outputs?.['childStatus'];
109
+ const childStatus = o.child?.snapshot['status'];
110
+ out.push(
111
+ finding(completionsOf(o.parent.events, SUBWF_NODE).length > 0, `${DOC} (Output)`, `the parent log MUST carry node.completed for the core.subWorkflow node ${SUBWF_NODE}`),
112
+ finding(o.childRunId !== undefined, `${DOC} (Output)`, `node.completed MUST carry outputs.childRunId (got ${JSON.stringify(o.outputs?.['childRunId'])})`),
113
+ finding(typeof status === 'string' && CHILD_STATUSES.has(status), `${DOC} (Output)`, `node.completed MUST carry outputs.childStatus, one of completed | failed | cancelled (got ${JSON.stringify(status)})`),
114
+ );
115
+ if (o.child !== undefined) {
116
+ out.push(
117
+ finding(o.child.status === 200, `${DOC} (Output)`, `outputs.childRunId MUST name a readable run (GET answered ${o.child.status})`),
118
+ finding(o.child.status !== 200 || status === childStatus, `${DOC} (Output)`, `outputs.childStatus MUST be the child's terminal status (got ${JSON.stringify(status)}, child is ${JSON.stringify(childStatus)})`),
119
+ );
120
+ }
121
+ return observed(out);
122
+ }
123
+
124
+ /** Leg: the mapped child variable is copied into the parent. Pure. */
125
+ export function judgeOutputMapping(o: LinkageObservation): Outcome {
126
+ const vars = isRecord(o.parent.snapshot['variables']) ? o.parent.snapshot['variables'] : {};
127
+ return observed([
128
+ ...runCompleted(o.parent),
129
+ finding(vars['childOutcome'] === 'child-completed', `${DOC} (outputMapping)`, `after the child completes, the host MUST copy childResult into the parent's childOutcome ("child-completed"; got ${JSON.stringify(vars['childOutcome'])})`),
130
+ ]);
131
+ }
132
+
133
+ /** Leg: the child's ancestry names the parent with `cause: "core.subWorkflow"`. Pure. */
134
+ export function judgeAncestry(o: LinkageObservation, validate: Validate): Outcome {
135
+ if (o.childRunId === undefined) return skip('blocked', 'the child run id is unobserved (node.completed carried no outputs.childRunId — the output leg convicts that), so its ancestry cannot be read');
136
+ if (o.ancestry === undefined) return skip('inapplicable', 'the host does not advertise multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported, so getRunAncestry is not served');
137
+ const body = o.ancestry.json;
138
+ const parent = isRecord(body) && isRecord(body['parent']) ? body['parent'] : undefined;
139
+ const v = validate(body);
140
+ return observed([
141
+ finding(o.ancestry.status === 200, `${DOC} (Parent link); runs.md §Diff and ancestry`, `getRunAncestry of the child MUST answer 200 (got ${o.ancestry.status})`),
142
+ finding(v.ok, 'schemas run-ancestry-response', `the ancestry body MUST validate: ${v.errors}`),
143
+ finding(parent !== undefined && parent['runId'] === o.parent.runId, `${DOC} (Parent link)`, `the child's parent MUST be the parent run ${String(o.parent.runId)} (got ${JSON.stringify(parent?.['runId'] ?? (isRecord(body) ? body['parent'] : undefined))})`),
144
+ finding(parent?.['cause'] === 'core.subWorkflow', `${DOC} (Parent link)`, `the child's parent.cause MUST be core.subWorkflow (got ${JSON.stringify(parent?.['cause'])})`),
145
+ ]);
146
+ }
147
+
148
+ const childVars = (o: MappingObservation): Record<string, unknown> | undefined =>
149
+ o.child !== undefined && o.child.status === 200 ? (isRecord(o.child.snapshot['variables']) ? o.child.snapshot['variables'] : {}) : undefined;
150
+
151
+ function childReached(o: MappingObservation): Finding[] {
152
+ return [
153
+ ...runCompleted(o.parent),
154
+ finding(o.childRunId !== undefined, `${DOC} (Output)`, `the parent's node.completed for ${SUBWF_NODE} MUST carry outputs.childRunId`),
155
+ finding(o.child?.status === 200, `${DOC} (Output)`, `outputs.childRunId MUST name a readable run (GET answered ${String(o.child?.status)})`),
156
+ finding(o.child?.snapshot['status'] === 'completed', `fixtures.md ${IM_CHILD_FIXTURE}`, `the child run MUST end completed (got ${JSON.stringify(o.child?.snapshot['status'])})`),
157
+ ];
158
+ }
159
+
160
+ /** Leg: the child's variable bag is seeded at creation — `receivedPrdId` is present. Pure. */
161
+ export function judgeSeed(o: MappingObservation): Outcome {
162
+ const v = childVars(o)?.['receivedPrdId'];
163
+ return observed([
164
+ ...childReached(o),
165
+ finding(v !== undefined && v !== null, `${DOC} (inputMapping: defaultValue MUST seed first)`, `the child's receivedPrdId MUST be seeded at creation (got ${JSON.stringify(v)})`),
166
+ ]);
167
+ }
168
+
169
+ /** Leg: the mapping lands after and over the child's default. Pure. */
170
+ export function judgeOverride(o: MappingObservation): Outcome {
171
+ const vars = childVars(o);
172
+ if (vars === undefined) return skip('blocked', 'the child run is unread (no outputs.childRunId, or GET failed — the seed leg convicts that)');
173
+ const v = vars['receivedPrdId'];
174
+ return observed([
175
+ finding(v !== 'baked-in', `${DOC} (inputMapping: after and over defaultValue)`, 'the child\'s defaultValue "baked-in" MUST NOT win over the mapped parent value'),
176
+ finding(v === 'prd-1', `${DOC} (inputMapping)`, `the child's receivedPrdId MUST be the parent's currentPrdId projection "prd-1" (got ${JSON.stringify(v)})`),
177
+ ]);
178
+ }
179
+
180
+ /** Leg: an unset parent variable arrives undefined — no error, no `null`. Pure. */
181
+ export function judgeUnset(o: MappingObservation): Outcome {
182
+ const vars = childVars(o);
183
+ const out = childReached(o);
184
+ if (vars !== undefined) {
185
+ const v = vars['receivedPrdId'];
186
+ out.push(
187
+ finding(v !== null, `${DOC} (inputMapping: never null)`, 'an unset parent variable MUST NOT arrive as null'),
188
+ finding(!('receivedPrdId' in vars) || v === 'baked-in', `${DOC} (inputMapping: arrives undefined)`, `an unset parent variable MUST arrive undefined: the child's value is absent or its own default (got ${JSON.stringify(v)})`),
189
+ );
190
+ }
191
+ return observed(out);
192
+ }
@@ -18,6 +18,7 @@
18
18
  * nothing; {@link judge} is pure.
19
19
  */
20
20
 
21
+ import { projectBoundId } from './bound-id.js';
21
22
  import { randomUUID } from 'node:crypto';
22
23
  import { driver, type OpenWOPResponse } from './driver.js';
23
24
  import { readErrorCode } from './error-envelope.js';
@@ -52,7 +53,8 @@ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typ
52
53
  async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
53
54
  try { return await fn(); } catch { return null; }
54
55
  }
55
- const enc = (id: string): string => encodeURIComponent(id);
56
+ /** A v2 bound id as one path segment: the `~` projection (identity.md §5). */
57
+ const enc = (id: string): string => projectBoundId(id);
56
58
 
57
59
  async function probe(action: ProbeAction, timeoutMs: number): Promise<ProbeOutcome | string> {
58
60
  const table = `conformance_schema_${randomUUID().replace(/-/g, '').slice(0, 16)}`;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * v2 — the `agentRuntime` advertisement (`spec/v2/core/host-services.md` §`agentRuntime`): the agent runtime claim. It had no v1 witness;
3
+ * the legs live in `lib/family-advert-witness.ts` (witness wave 3).
4
+ *
5
+ * record the record validates against its capabilities-schema seat;
6
+ * implies-manifest-runtime advertising agentRuntime also advertises agents.manifestRuntime;
7
+ *
8
+ * Not here: the `ctx` surface (`spawn`, `delegate`, `consensus`, `messageSend`) cannot be observed from outside a node.
9
+ *
10
+ * Dispositions: discovery unreadable ⇒ `blocked`; `agentRuntime` absent, or the
11
+ * facet a leg reads absent ⇒ `inapplicable`.
12
+ *
13
+ * @see spec/v2/core/host-services.md §`agentRuntime`
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
18
+ import { softSkip } from '../lib/soft-skip.js';
19
+ import { req } from '../lib/requirement-ids.js';
20
+ import { majorProfile } from '../lib/major-profile.js';
21
+ import { agentRuntimeImpliesManifestRuntimeLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
22
+
23
+ const PROFILE = majorProfile(2);
24
+ const DOC = 'host-services.md §agentRuntime';
25
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
26
+ const ID_RECORD = 'openwop.requirement.agent-runtime.advert-record-schema';
27
+ const ID_IMPLIES_MANIFEST_RUNTIME = 'openwop.requirement.agent-runtime.implies-manifest-runtime';
28
+
29
+ async function discovery(): Promise<Record<string, unknown> | null> {
30
+ try { return await v2Discovery(); } catch { return null; }
31
+ }
32
+
33
+ describe('v2 agentRuntime advertisement (host-services.md §agentRuntime)', () => {
34
+ it('the agentRuntime record validates against its capabilities-schema seat', async () => {
35
+ const doc = await discovery();
36
+ if (!doc) return softSkip('blocked', UNREADABLE);
37
+ if (!(await familyAdvertised('agentRuntime'))) return softSkip('inapplicable', 'the host does not advertise agentRuntime');
38
+ const out = recordSchemaLeg(PROFILE, doc, 'agentRuntime', DOC);
39
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
40
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
41
+ });
42
+
43
+ it('advertising agentRuntime also advertises agents.manifestRuntime', async () => {
44
+ const doc = await discovery();
45
+ if (!doc) return softSkip('blocked', UNREADABLE);
46
+ if (!(await familyAdvertised('agentRuntime'))) return softSkip('inapplicable', 'the host does not advertise agentRuntime');
47
+ const out = agentRuntimeImpliesManifestRuntimeLeg(PROFILE, doc);
48
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
49
+ for (const x of out.findings) expect(x.ok, req(ID_IMPLIES_MANIFEST_RUNTIME, x.doc, x.message)).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * v2 — the `aiEnvelope` advertisement (`spec/v2/core/host-services.md`
3
+ * §aiEnvelope; RFC 0144). The v1 twin is the record shape `aiEnvelope` carried
4
+ * at v1; the legs live in `lib/family-advert-witness.ts`.
5
+ *
6
+ * record the record (with its optional `await` boolean) validates against
7
+ * its capabilities-schema seat.
8
+ *
9
+ * Not here: exposing `ctx.aiEnvelope.generate` / `await` to pack code is a
10
+ * pack-runtime surface with no wire witness.
11
+ *
12
+ * Dispositions: discovery unreadable ⇒ `blocked`; `aiEnvelope` absent, or the
13
+ * facet a leg reads absent ⇒ `inapplicable`.
14
+ *
15
+ * @see spec/v2/core/host-services.md §aiEnvelope
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
20
+ import { softSkip } from '../lib/soft-skip.js';
21
+ import { req } from '../lib/requirement-ids.js';
22
+ import { majorProfile } from '../lib/major-profile.js';
23
+ import { recordSchemaLeg } from '../lib/family-advert-witness.js';
24
+
25
+ const PROFILE = majorProfile(2);
26
+ const DOC = 'host-services.md §aiEnvelope';
27
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
28
+ const ID_RECORD = 'openwop.requirement.ai-envelope.advert-record-schema';
29
+
30
+ async function discovery(): Promise<Record<string, unknown> | null> {
31
+ try { return await v2Discovery(); } catch { return null; }
32
+ }
33
+
34
+ describe('v2 aiEnvelope advertisement (host-services.md §aiEnvelope)', () => {
35
+ it('the aiEnvelope record validates against its capabilities-schema seat', async () => {
36
+ const doc = await discovery();
37
+ if (!doc) return softSkip('blocked', UNREADABLE);
38
+ if (!(await familyAdvertised('aiEnvelope'))) return softSkip('inapplicable', 'the host does not advertise aiEnvelope');
39
+ const out = recordSchemaLeg(PROFILE, doc, 'aiEnvelope', DOC);
40
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
41
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
42
+ });
43
+ });