@junghanacs/entwurf 0.12.10 → 0.13.1

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 (86) hide show
  1. package/AGENTS.md +6 -3
  2. package/BASELINE.md +56 -163
  3. package/CHANGELOG.md +35 -0
  4. package/CONTRIBUTING.md +4 -2
  5. package/DELIVERY.md +117 -261
  6. package/README.md +74 -399
  7. package/VERIFY.md +59 -85
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +231 -0
  10. package/docs/external-mcp-host.md +132 -0
  11. package/docs/fresh-cut-policy.md +99 -0
  12. package/docs/setup-clean-host.md +123 -328
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +155 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -5
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
  21. package/mcp/entwurf-bridge/src/index.ts +21 -48
  22. package/package.json +14 -9
  23. package/pi/settings.reference.json +1 -1
  24. package/pi-extensions/acp-provider.ts +20 -10
  25. package/pi-extensions/entwurf-control.ts +20 -49
  26. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  27. package/pi-extensions/lib/acp/augment.ts +99 -2
  28. package/pi-extensions/lib/acp/backend-adapter.ts +190 -14
  29. package/pi-extensions/lib/acp/backend.ts +310 -37
  30. package/pi-extensions/lib/acp/config.ts +19 -5
  31. package/pi-extensions/lib/acp/engraving.ts +46 -2
  32. package/pi-extensions/lib/acp/event-mapper.ts +26 -16
  33. package/pi-extensions/lib/acp/models.ts +69 -7
  34. package/pi-extensions/lib/acp/overlay.ts +234 -5
  35. package/pi-extensions/lib/acp/tool-surface.ts +12 -4
  36. package/prompts/engraving.md +13 -5
  37. package/run.sh +386 -119
  38. package/scripts/check-acp-carrier-augment.ts +246 -10
  39. package/scripts/check-acp-cortex.ts +668 -0
  40. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  41. package/scripts/check-acp-provider-surface.ts +50 -6
  42. package/scripts/check-acp-sdk-surface.ts +60 -21
  43. package/scripts/check-acp-session-reuse.ts +64 -1
  44. package/scripts/check-acp-stop-reason.ts +342 -0
  45. package/scripts/check-entwurf-v2-surface.ts +41 -0
  46. package/scripts/check-gate-qualification.ts +7 -2
  47. package/scripts/check-probe-cli-shim.ts +879 -0
  48. package/scripts/check-probe-ordering.ts +2482 -0
  49. package/scripts/check-release-gate-outcomes.ts +332 -0
  50. package/scripts/check-shell-quote.ts +4 -4
  51. package/scripts/fixtures/probe-cli-shim +20 -0
  52. package/scripts/fixtures/probe-mcp-server.ts +168 -12
  53. package/scripts/lib/live-skip.ts +33 -0
  54. package/scripts/lib/probe-acp-turn.ts +228 -0
  55. package/scripts/lib/probe-cli-shim.ts +464 -0
  56. package/scripts/lib/probe-cli-target.ts +165 -0
  57. package/scripts/lib/probe-event-log.ts +383 -0
  58. package/scripts/lib/probe-verdict.ts +1213 -0
  59. package/scripts/lib/step-outcome.sh +88 -0
  60. package/scripts/mutants/acp-augment.json +106 -0
  61. package/scripts/mutants/acp-cortex.json +196 -0
  62. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  63. package/scripts/mutants/acp-stop-reason.json +80 -0
  64. package/scripts/mutants/probe-ordering.json +1054 -0
  65. package/scripts/mutants/release-gate.json +105 -0
  66. package/scripts/mutants/v2-surface.json +26 -15
  67. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  68. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  69. package/scripts/smoke-acp-cortex-live.ts +386 -0
  70. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  71. package/scripts/smoke-acp-mcp-live.ts +2 -2
  72. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  73. package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
  74. package/scripts/smoke-acp-overlay-live.ts +2 -2
  75. package/scripts/smoke-acp-provider-live.ts +2 -2
  76. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  77. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  78. package/scripts/smoke-acp-skill-live.ts +2 -2
  79. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  80. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  81. package/scripts/smoke-agy-native-push-live.ts +4 -3
  82. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  83. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  84. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  85. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  86. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -0,0 +1,2482 @@
1
+ // Deterministic gate for the §11-7 ordering probe (docs/acp-backend-rail.md).
2
+ //
3
+ // §11-7 allows the probe a raw client ONLY "bound by a gate asserting it issues
4
+ // the same calls, arguments, and order as the backend's real sequence" — THIS is
5
+ // that gate. Seven axes, no live API anywhere:
6
+ //
7
+ // 1) SAMENESS — driveProbeTurn over a recording fake connection must issue
8
+ // initialize → newSession → enforceModel → prompt with production-shaped
9
+ // arguments; the phase timeouts / call order / clientInfo / enforceModel
10
+ // wire shape / permission policy are pinned against backend.ts and
11
+ // backend-adapter.ts SOURCE, and the LIVE runner is pinned to inject the
12
+ // REAL emitted claudeAdapter + enrichMcpServersWithEnvelope — so
13
+ // production drift turns this gate red instead of silently unbinding the
14
+ // probe.
15
+ // 2) PHASE ATTRIBUTION — a failure names its wire phase (set-model included:
16
+ // §11-7 measured that dropping it misreads enforceModel stalls as C or D).
17
+ // 3) FIXTURE WIRE INSTRUMENTATION — the probe-mode fixture is spawned for
18
+ // real (child process, no API) and must stamp the §11-7 marker set,
19
+ // honor the injected startup delay, REQUIRE probeRunId, and stay
20
+ // byte-compatible with smoke-acp-mcp-live when the probe env is absent.
21
+ // 4) EVENT LOG INTEGRITY — the contract at the log's door, both layers: the
22
+ // writer may not have its own envelope rewritten by a payload key; a
23
+ // JSON-valid line carrying an unknown marker name or a broken sort axis is
24
+ // MALFORMED (→ the runner's INVALIDATED path), never a quiet event; and
25
+ // every PAYLOAD field the classifier judges on is typed there too, since a
26
+ // perfect envelope around `ok: "true"` reads as a phase failure and a wire
27
+ // marker naming no expected tool is not wire-availability at all.
28
+ // 4b) EVENT LOG STREAM INTEGRITY — the second door, one layer out: per writing
29
+ // pid, on the RAW APPEND ORDER, seq must strictly increase and the clock may
30
+ // not run backwards. Checked BEFORE the sort, because a post-sort check is
31
+ // circular — the comparator would have rewritten the order under examination.
32
+ // 5) VERDICT TRUTH TABLE — synthetic paired logs replay through the PURE
33
+ // classifier: P0/I0 outside the verdict space, phase-qualified D, the B
34
+ // promotion ladder (exact measured id only), C, and A's two-delay rule.
35
+ // 6) OBSERVATION WINDOW + RUNNER TOPOLOGY — what puts a run OUTSIDE the space:
36
+ // a window closed by child-exit with the marker unseen is CENSORED, never an
37
+ // MCP handshake/fixture/config attribution (the 2026-07-28 D2 misreading),
38
+ // while the SAME absence under a window held to its deadline is a real
39
+ // candidate. Runner-owned markers are exactly-once; repeatable ones are not
40
+ // swept in with them.
41
+ // 7) TWO AXES — (a) the server-wait observation and (b) the callability reading
42
+ // are reported separately, so a settled ordering fact is not buried by an
43
+ // unsettled failure axis. A keeps the newSession axis; B/C's causal window is
44
+ // promptStart, and the evidence carries both deltas plus the post-wire turn.
45
+ // 8) §11-7-c B-NAME-SNAPSHOT SEAM (consumer side) — the CLI-target
46
+ // precondition gate (ambient override refused, native-branch absolute
47
+ // target only, exact-allowlist scrub), the upstream launch-semantics
48
+ // inspector (synthetic-validated, applied to the installed dists), the
49
+ // shim event doors, and the snapshot verdict ladder (calibrated control,
50
+ // exactly-one ordinal binding, interval-ordered absence, roster-armed
51
+ // channel; B and B-name-snapshot never conflated). The PRODUCER (shim) is
52
+ // gated separately by check-probe-cli-shim, and the LIVE runner now ARMS the
53
+ // channel — 8d pins that wiring.
54
+ //
55
+ // Kill-proof, stated at its honest strength: scripts/mutants/probe-ordering.json
56
+ // qualifies 63 claims for THIS gate — each carries a [QK:...] signature appearing
57
+ // EXACTLY once below, and check-gate-qualification proves its mutant dies at that
58
+ // signature. (The lane also carries the §11-7-c PRODUCER claims, whose signatures
59
+ // live in check-probe-cli-shim.ts; one lane, two consuming gates.)
60
+ // [QK:*] tokens and qualified claims are 1:1 BY DESIGN: an assertion without a
61
+ // mutant carries a plain message, so "killed claim IDs, never assertion counts"
62
+ // stays readable. The remaining assertions are enforced-but-not-mutant-qualified.
63
+ // Properties that are review-pinned only (no deterministic mutant exists):
64
+ // - the write-CALLBACK timing of tools_list_response_forwarded (cross-process
65
+ // microsecond ordering; existence/attribution IS proven);
66
+ // - the fixture's errored-write path (EPIPE cannot be forced deterministically
67
+ // in this handshake; the callback rejects and never stamps — source-visible);
68
+ // - the writer's SINGLE clock read (a two-read millisecond straddle is a rare
69
+ // race, so it is pinned in source and made loud by the parser's ts↔tsMs
70
+ // equality rule, which IS mutant-qualified).
71
+
72
+ import { strict as assert } from "node:assert";
73
+ import { type ChildProcessByStdio, spawn } from "node:child_process";
74
+ import {
75
+ appendFileSync,
76
+ chmodSync,
77
+ existsSync,
78
+ mkdirSync,
79
+ mkdtempSync,
80
+ readFileSync,
81
+ rmSync,
82
+ statSync,
83
+ writeFileSync,
84
+ } from "node:fs";
85
+ import { createRequire } from "node:module";
86
+ import { tmpdir } from "node:os";
87
+ import { dirname, join, resolve } from "node:path";
88
+ import type { Readable, Writable } from "node:stream";
89
+ import { fileURLToPath } from "node:url";
90
+ import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
91
+ import type { AcpConnectionLike } from "../pi-extensions/lib/acp/acp-client.ts";
92
+ import { resolveProviderConfig } from "../pi-extensions/lib/acp/config.ts";
93
+ import { terminateChild } from "./lib/acp-child-cleanup.ts";
94
+ import {
95
+ driveProbeTurn,
96
+ PROBE_PHASE_TIMEOUTS,
97
+ PROBE_PROMPT_OBSERVATION_MS,
98
+ type ProbeAdapterSeam,
99
+ type ProbeMcpEnricher,
100
+ ProbePhaseError,
101
+ type ProbeTurnPhase,
102
+ } from "./lib/probe-acp-turn.ts";
103
+ import {
104
+ AMBIENT_OVERRIDE_ENV,
105
+ hashFileSha256,
106
+ PROBE_SHIM_ENV,
107
+ ProbeCliPreconditionError,
108
+ resolveProbeCliTarget,
109
+ SDK_SCRIPT_SUFFIXES,
110
+ SHIM_SCRUB_ENV_VARS,
111
+ } from "./lib/probe-cli-target.ts";
112
+ import {
113
+ appendProbeEvent,
114
+ PAYLOAD_CONTRACT_EVENTS,
115
+ PROBE_ENV,
116
+ PROBE_EVENTS,
117
+ PROBE_EXPECTED_TOOL,
118
+ type ProbeEvent,
119
+ type ProbeWindowReason,
120
+ RESERVED_EVENT_KEYS,
121
+ readProbeEvents,
122
+ } from "./lib/probe-event-log.ts";
123
+ import { classifyProbe, DELAY_WELL_BELOW_MS, type ProbeRunRecord, RUNNER_EXACTLY_ONCE } from "./lib/probe-verdict.ts";
124
+
125
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
126
+ const BACKEND_SRC = readFileSync(join(REPO_ROOT, "pi-extensions", "lib", "acp", "backend.ts"), "utf8");
127
+ const ADAPTER_SRC = readFileSync(join(REPO_ROOT, "pi-extensions", "lib", "acp", "backend-adapter.ts"), "utf8");
128
+ const RUNNER_SRC = readFileSync(join(REPO_ROOT, "scripts", "smoke-acp-ordering-probe-live.ts"), "utf8");
129
+ const EVENT_LOG_SRC = readFileSync(join(REPO_ROOT, "scripts", "lib", "probe-event-log.ts"), "utf8");
130
+ const VERDICT_SRC = readFileSync(join(REPO_ROOT, "scripts", "lib", "probe-verdict.ts"), "utf8");
131
+ const FIXTURE_PATH = join(REPO_ROOT, "scripts", "fixtures", "probe-mcp-server.ts");
132
+ const FIXTURE_SRC = readFileSync(FIXTURE_PATH, "utf8");
133
+ const tmp = mkdtempSync(join(tmpdir(), "check-probe-ordering-"));
134
+
135
+ // The REAL config resolver (config.ts is strip-types-clean) over a scratch
136
+ // operator settings file — the fixture entry must survive config→wire intact.
137
+ function makeConfigFixture(): ReturnType<typeof resolveProviderConfig> {
138
+ const cwd = join(tmp, "cfg");
139
+ mkdirSync(join(cwd, ".pi"), { recursive: true });
140
+ const projectSettingsPath = join(cwd, ".pi", "settings.json");
141
+ writeFileSync(
142
+ projectSettingsPath,
143
+ `${JSON.stringify({
144
+ entwurfProvider: {
145
+ mcpServers: { probe: { command: "node", args: ["probe.ts"], env: { X: "1" } } },
146
+ },
147
+ })}\n`,
148
+ );
149
+ return resolveProviderConfig({
150
+ cwd,
151
+ modelId: "claude-sonnet-5",
152
+ adapter: { resolveAdapterSettings: () => undefined } as never,
153
+ globalSettingsPath: join(cwd, ".pi", "no-global.json"),
154
+ projectSettingsPath,
155
+ });
156
+ }
157
+
158
+ interface RecordedCall {
159
+ method: string;
160
+ params: unknown;
161
+ }
162
+
163
+ function makeRecordingConnection(calls: RecordedCall[]): AcpConnectionLike {
164
+ return {
165
+ initialize: async (params) => {
166
+ calls.push({ method: "initialize", params });
167
+ return { protocolVersion: PROTOCOL_VERSION };
168
+ },
169
+ newSession: async (params) => {
170
+ calls.push({ method: "newSession", params });
171
+ return { sessionId: "sess-gate" };
172
+ },
173
+ setSessionConfigOption: async (params) => {
174
+ calls.push({ method: "setSessionConfigOption", params });
175
+ return {};
176
+ },
177
+ prompt: async (params) => {
178
+ calls.push({ method: "prompt", params });
179
+ return { stopReason: "end_turn" };
180
+ },
181
+ };
182
+ }
183
+
184
+ // ===========================================================================
185
+ // 1) SAMENESS — recorded sequence + production source pins
186
+ // ===========================================================================
187
+ {
188
+ const config = makeConfigFixture();
189
+ const calls: RecordedCall[] = [];
190
+ const logged: Array<{ event: string; payload: Record<string, unknown> }> = [];
191
+ const metaMarker = { modelId: "claude-sonnet-5", gateMeta: true };
192
+ const enrichedMarker = [{ name: "probe", command: "node", args: ["probe.ts"], env: [{ name: "X", value: "1" }] }];
193
+ let enrichArgs: { servers: unknown; envelope: unknown } | undefined;
194
+ let metaArgs: { params: unknown; carrier: unknown } | undefined;
195
+ const recordingAdapter: ProbeAdapterSeam = {
196
+ buildSessionMeta(params, carrier) {
197
+ metaArgs = { params, carrier };
198
+ return metaMarker;
199
+ },
200
+ async enforceModel(params) {
201
+ calls.push({ method: "enforceModel", params: { ...params, connection: "<connection>" } });
202
+ },
203
+ };
204
+
205
+ // PI_SESSION_ID would leak the host session into the envelope assertion —
206
+ // pin it to a deterministic value for the recording turn.
207
+ const prevPiSession = process.env.PI_SESSION_ID;
208
+ process.env.PI_SESSION_ID = "gate-pi-session";
209
+ try {
210
+ await driveProbeTurn(makeRecordingConnection(calls), {
211
+ cwd: "/scratch/gate",
212
+ modelId: "claude-sonnet-5",
213
+ nativeModelId: "claude-sonnet-5",
214
+ config,
215
+ carrier: "gate-carrier",
216
+ promptText: "gate prompt",
217
+ adapter: recordingAdapter,
218
+ enrichMcpServers: (servers, envelope) => {
219
+ enrichArgs = { servers, envelope };
220
+ return enrichedMarker as never;
221
+ },
222
+ log: (event, payload = {}) => logged.push({ event, payload }),
223
+ });
224
+ } finally {
225
+ if (prevPiSession === undefined) delete process.env.PI_SESSION_ID;
226
+ else process.env.PI_SESSION_ID = prevPiSession;
227
+ }
228
+
229
+ assert.deepEqual(
230
+ calls.map((c) => c.method),
231
+ ["initialize", "newSession", "enforceModel", "prompt"],
232
+ "probe issues the production wire sequence in order",
233
+ );
234
+ assert.deepEqual(
235
+ calls[0].params,
236
+ {
237
+ protocolVersion: PROTOCOL_VERSION,
238
+ clientCapabilities: {},
239
+ clientInfo: { name: "entwurf", version: "s2d" },
240
+ },
241
+ "initialize params match backend.ts byte-shape [QK:PROBE-SEQ-ORDER]",
242
+ );
243
+ const newSessionParams = calls[1].params as { cwd: string; mcpServers: unknown; _meta?: unknown };
244
+ assert.equal(newSessionParams.cwd, "/scratch/gate", "newSession carries the run cwd");
245
+ assert.equal(newSessionParams.mcpServers, enrichedMarker, "newSession sends EXACTLY the envelope-enriched servers");
246
+ assert.deepEqual(
247
+ enrichArgs,
248
+ {
249
+ servers: config.mcpServers,
250
+ envelope: { modelId: "claude-sonnet-5", piSessionId: "gate-pi-session" },
251
+ },
252
+ "enrichment sees the resolved servers + the production envelope",
253
+ );
254
+ assert.equal(config.mcpServers[0]?.name, "probe", "the fixture entry survives config resolution");
255
+ assert.equal(newSessionParams._meta, metaMarker, "newSession _meta is buildSessionMeta's result");
256
+ assert.deepEqual(
257
+ metaArgs,
258
+ {
259
+ params: { modelId: "claude-sonnet-5", nativeModelId: "claude-sonnet-5", config },
260
+ carrier: "gate-carrier",
261
+ },
262
+ "buildSessionMeta receives the production inputs incl. the carrier",
263
+ );
264
+ assert.deepEqual(
265
+ calls[2].params,
266
+ {
267
+ connection: "<connection>",
268
+ acpSessionId: "sess-gate",
269
+ modelId: "claude-sonnet-5",
270
+ nativeModelId: "claude-sonnet-5",
271
+ },
272
+ "enforceModel receives the production inputs (adapter owns the wire shape)",
273
+ );
274
+ const promptParams = calls[3].params as { sessionId: string; prompt: Array<{ type: string; text: string }> };
275
+ assert.equal(promptParams.sessionId, "sess-gate", "prompt binds the newSession sessionId");
276
+ assert.deepEqual(
277
+ promptParams.prompt,
278
+ [{ type: "text", text: "gate prompt" }],
279
+ "prompt block shape matches production",
280
+ );
281
+
282
+ // Set-model markers exist BETWEEN newSession end and prompt start — §11-7:
283
+ // dropping them misreads an enforceModel stall as C or D.
284
+ const eventOrder = logged.map((l) => l.event);
285
+ const nsEnd = eventOrder.indexOf(PROBE_EVENTS.newSessionEnd);
286
+ const smStart = eventOrder.indexOf(PROBE_EVENTS.setModelStart);
287
+ const smEnd = eventOrder.indexOf(PROBE_EVENTS.setModelEnd);
288
+ const pStart = eventOrder.indexOf(PROBE_EVENTS.promptStart);
289
+ assert.ok(
290
+ nsEnd !== -1 && smStart > nsEnd && smEnd > smStart && pStart > smEnd,
291
+ `set-model start/end are marked between newSession end and prompt start (got ${eventOrder.join(",")})`,
292
+ );
293
+
294
+ // Carrier-less shape: an undefined session meta omits the `_meta` KEY entirely.
295
+ const calls2: RecordedCall[] = [];
296
+ await driveProbeTurn(makeRecordingConnection(calls2), {
297
+ cwd: "/scratch/gate2",
298
+ modelId: "claude-sonnet-5",
299
+ nativeModelId: "claude-sonnet-5",
300
+ config,
301
+ carrier: null,
302
+ promptText: "x",
303
+ adapter: { buildSessionMeta: () => undefined, enforceModel: async () => {} },
304
+ enrichMcpServers: (servers) => servers,
305
+ log: () => {},
306
+ });
307
+ assert.ok(
308
+ !Object.hasOwn(calls2.find((c) => c.method === "newSession")?.params as object, "_meta"),
309
+ "undefined session meta omits the _meta KEY entirely (carrier-less shape)",
310
+ );
311
+
312
+ // --- production source pins ---------------------------------------------
313
+ const timeoutOf = (name: string): number => {
314
+ const m = new RegExp(`const ${name} = ([\\d_]+);`).exec(BACKEND_SRC);
315
+ assert.ok(m, `backend.ts declares ${name}`);
316
+ return Number(m[1].replaceAll("_", ""));
317
+ };
318
+ assert.deepEqual(
319
+ {
320
+ initializeMs: PROBE_PHASE_TIMEOUTS.initializeMs,
321
+ newSessionMs: PROBE_PHASE_TIMEOUTS.newSessionMs,
322
+ setModelMs: PROBE_PHASE_TIMEOUTS.setModelMs,
323
+ },
324
+ {
325
+ initializeMs: timeoutOf("INITIALIZE_TIMEOUT_MS"),
326
+ newSessionMs: timeoutOf("NEW_SESSION_TIMEOUT_MS"),
327
+ setModelMs: timeoutOf("SET_MODEL_TIMEOUT_MS"),
328
+ },
329
+ "probe BOOTSTRAP timeouts EQUAL backend.ts's — a bootstrap-phase D is only readable against production " +
330
+ "boundaries [QK:PROBE-BOOTSTRAP-TIMEOUTS-MATCH-PRODUCTION]",
331
+ );
332
+
333
+ // --- the prompt phase has NO production boundary to match --------------
334
+ //
335
+ // backend.ts ends a prompt on lifecycle events only. The probe still needs a
336
+ // bounded observation horizon so a measurement RUN cannot hang forever, but
337
+ // that horizon is the harness's own: pinning it to a production number (or
338
+ // letting production grow one back) would make the probe report a
339
+ // harness-invented cutoff as if it were the turn contract.
340
+ const promptCutoffTraces = [/\bPROMPT_TIMEOUT_MS\b/, /withTimeout\(\s*\n?\s*"prompt"/].filter((re) =>
341
+ re.test(BACKEND_SRC),
342
+ );
343
+ assert.equal(
344
+ promptCutoffTraces.length,
345
+ 0,
346
+ "[QK:NO-PRODUCTION-PROMPT-CUTOFF] backend.ts carries NO prompt wall-clock — neither a PROMPT_TIMEOUT_MS constant " +
347
+ 'nor a withTimeout("prompt", …) race. A running turn is not a failed turn for being long, and the old 600s ' +
348
+ `cutoff also fed pi's transient-retry dictionary. Found: ${promptCutoffTraces.join(", ")}`,
349
+ );
350
+ assert.ok(
351
+ PROBE_PROMPT_OBSERVATION_MS > 0 && !("promptMs" in PROBE_PHASE_TIMEOUTS),
352
+ "[QK:PROMPT-HORIZON-NOT-PRODUCTION] the prompt horizon lives OUTSIDE the production-pinned bootstrap set — it is " +
353
+ "the harness's own observation bound, and folding it back in would report a harness number as the turn contract",
354
+ );
355
+
356
+ const idx = (needle: string): number => {
357
+ const i = BACKEND_SRC.indexOf(needle);
358
+ assert.ok(i !== -1, `backend.ts contains ${JSON.stringify(needle)}`);
359
+ return i;
360
+ };
361
+ const iInit = idx("connection.initialize({");
362
+ const iNew = idx("connection.newSession(newSessionArgs)");
363
+ const iEnforce = idx("adapter.enforceModel({");
364
+ // The prompt no longer sits inline: runNewTurn hands the wire call to
365
+ // awaitAcpPromptTurn (the lifecycle-bounded driver), so the ordering pin
366
+ // follows the dispatch, and the driver's own `session.connection.prompt(...)`
367
+ // is the single place the wire call lives.
368
+ const iPrompt = BACKEND_SRC.indexOf("await awaitAcpPromptTurn(", iEnforce);
369
+ assert.ok(
370
+ iInit < iNew && iNew < iEnforce && iEnforce < iPrompt && iPrompt !== -1,
371
+ "backend.ts runNewTurn keeps initialize → newSession → enforceModel → prompt; the probe mirrors THIS sequence",
372
+ );
373
+ assert.ok(
374
+ BACKEND_SRC.includes("await Promise.race([session.connection.prompt(promptArgs), lifecycle])"),
375
+ "the prompt driver races the wire call against LIFECYCLE endings only — the probe mirrors that same wire call",
376
+ );
377
+ assert.ok(
378
+ BACKEND_SRC.includes('clientInfo: { name: "entwurf", version: "s2d" }'),
379
+ "backend.ts still sends the clientInfo the probe mirrors",
380
+ );
381
+ // The claude adapter's enforceModel wire shape — the LIVE probe executes THIS
382
+ // method (emitted twin), so its shape is pinned where it lives.
383
+ assert.ok(
384
+ ADAPTER_SRC.includes(
385
+ 'await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });',
386
+ ),
387
+ 'claudeAdapter.enforceModel still sends setSessionConfigOption({configId:"model"})',
388
+ );
389
+
390
+ // Permission policy: the runner's copy must mirror backend.ts's private
391
+ // resolvePermissionResponse (approve-first-allow, empty → cancelled).
392
+ for (const [label, src] of [
393
+ ["backend.ts", BACKEND_SRC],
394
+ ["smoke-acp-ordering-probe-live.ts", RUNNER_SRC],
395
+ ] as const) {
396
+ assert.ok(
397
+ src.includes('o.kind === "allow_once" || o.kind === "allow_always"') &&
398
+ src.includes('if (options.length === 0) return { outcome: { outcome: "cancelled" } };'),
399
+ `${label} carries the approve-first-allow permission policy`,
400
+ );
401
+ }
402
+
403
+ // The runner must drive THE gated sequence with the REAL emitted production
404
+ // modules — never a bare wire call or a re-implementation of its own.
405
+ assert.ok(
406
+ RUNNER_SRC.includes("driveProbeTurn(") &&
407
+ RUNNER_SRC.includes("adapterMod.claudeAdapter") &&
408
+ RUNNER_SRC.includes("configMod.enrichMcpServersWithEnvelope") &&
409
+ RUNNER_SRC.includes("adapter: claudeAdapter") &&
410
+ RUNNER_SRC.includes("enrichMcpServers: enrichMcpServersWithEnvelope") &&
411
+ !RUNNER_SRC.includes("connection.initialize(") &&
412
+ !RUNNER_SRC.includes("connection.newSession(") &&
413
+ !RUNNER_SRC.includes("connection.prompt("),
414
+ "the LIVE runner routes every wire call through driveProbeTurn with the emitted production adapter/config",
415
+ );
416
+
417
+ // The runtime No-such-tool marker may ride ONLY structured tool frames —
418
+ // scanning agent prose would let the model MINT the marker by saying the
419
+ // sentence, silently promoting prose to runtime evidence. Exactly one scan
420
+ // call site (plus its definition), and never inside the agent-chunk branch.
421
+ const scanSites = RUNNER_SRC.split("scanStructuredNoSuchTool").length - 1;
422
+ assert.ok(
423
+ scanSites === 2 && !/collectedText \+= t;[^}]*scanStructured/s.test(RUNNER_SRC),
424
+ "No-such-tool is scanned ONLY off structured tool frames, never agent prose [QK:PROBE-NO-PROSE-ERROR-SCAN]",
425
+ );
426
+
427
+ // A broken event log is run-invalidating on BOTH doors: a malformed line could
428
+ // be the very wire marker whose absence the classifier would then read as
429
+ // evidence, and a stream whose per-writer order cannot be trusted cannot carry
430
+ // an ordering verdict at all. The preserved classification.json must say
431
+ // INVALIDATED on its face, never a judgeable-looking thin-log verdict.
432
+ //
433
+ // Pinned as the WHOLE guard, not the substring `if (malformed.length > 0)`:
434
+ // that shorter form also matches the artifact-writing line right below it, so
435
+ // a mutant could disable the guard and still satisfy the pin. It did — this
436
+ // assertion SURVIVED its own mutant until the pin was tightened (2026-07-29).
437
+ // The post-delay slack is what lets a missing wire marker be read as evidence:
438
+ // an operator-shrinkable window could close early and then be called
439
+ // "deadline-sufficient". It is a constant, and no env may reach it.
440
+ assert.ok(
441
+ RUNNER_SRC.includes("const POST_DELAY_SLACK_MS = 5_000;") && !RUNNER_SRC.includes("PROBE_POST_DELAY_SLACK_MS"),
442
+ "the observation window's post-delay slack is a CONSTANT with no env override [QK:PROBE-WINDOW-SLACK-IS-CONSTANT]",
443
+ );
444
+
445
+ // The exit contract asks three separate questions. Failing the run on the
446
+ // composite verdict alone reported a pair that MEASURED its ordering axis as a
447
+ // failed run, purely because the callability axis had no marker.
448
+ assert.ok(
449
+ RUNNER_SRC.includes('classification.status.validity !== "valid" && classification.status.validity !== "partial"') &&
450
+ RUNNER_SRC.includes("do not read it as a complete series") &&
451
+ RUNNER_SRC.includes('classification.status.orderingMeasurement !== "measured"') &&
452
+ RUNNER_SRC.includes("NOT a claim about server wait behavior"),
453
+ "the runner's exit contract separates fatal validity from the two axes, and claims no server-wait conclusion [QK:RUNNER-EXIT-CONTRACT-SPLIT]",
454
+ );
455
+
456
+ assert.ok(
457
+ RUNNER_SRC.includes("if (malformed.length > 0 || sequenceViolations.length > 0) {") &&
458
+ RUNNER_SRC.includes("run INVALIDATED") &&
459
+ RUNNER_SRC.includes('verdict: "INVALIDATED"'),
460
+ "the runner refuses to judge a log with malformed lines OR per-writer order violations, and writes an INVALIDATED classification [QK:PROBE-MALFORMED-INVALIDATES]",
461
+ );
462
+ }
463
+
464
+ // ===========================================================================
465
+ // 2) PHASE ATTRIBUTION — a failure names its wire phase; timeouts are flagged
466
+ // ===========================================================================
467
+ {
468
+ const config = makeConfigFixture();
469
+ const passthroughAdapter: ProbeAdapterSeam = {
470
+ buildSessionMeta: () => undefined,
471
+ enforceModel: async () => {},
472
+ };
473
+ const identityEnrich: ProbeMcpEnricher = (servers) => [...servers];
474
+ const base = {
475
+ cwd: "/scratch/phase",
476
+ modelId: "claude-sonnet-5",
477
+ nativeModelId: "claude-sonnet-5",
478
+ config,
479
+ carrier: null as string | null,
480
+ promptText: "x",
481
+ adapter: passthroughAdapter,
482
+ enrichMcpServers: identityEnrich,
483
+ log: () => {},
484
+ };
485
+ const failAt = async (broken: Partial<AcpConnectionLike>, adapter?: ProbeAdapterSeam): Promise<ProbePhaseError> => {
486
+ const calls: RecordedCall[] = [];
487
+ const conn = { ...makeRecordingConnection(calls), ...broken };
488
+ try {
489
+ await driveProbeTurn(conn, { ...base, adapter: adapter ?? passthroughAdapter });
490
+ } catch (err) {
491
+ assert.ok(err instanceof ProbePhaseError, "turn failures are ProbePhaseError");
492
+ return err;
493
+ }
494
+ assert.fail("expected the broken connection to fail the turn");
495
+ };
496
+
497
+ const expectPhase = async (
498
+ phase: ProbeTurnPhase,
499
+ broken: Partial<AcpConnectionLike>,
500
+ adapter?: ProbeAdapterSeam,
501
+ ): Promise<void> => {
502
+ const err = await failAt(broken, adapter);
503
+ assert.equal(err.phase, phase, `failure attributes to ${phase}`);
504
+ };
505
+ await expectPhase("initialize", { initialize: async () => Promise.reject(new Error("boom")) });
506
+ await expectPhase("newSession", { newSession: async () => Promise.reject(new Error("boom")) });
507
+ await expectPhase("newSession", { newSession: async () => ({}) }); // no sessionId
508
+ await expectPhase(
509
+ "enforceModel",
510
+ {},
511
+ {
512
+ buildSessionMeta: () => undefined,
513
+ enforceModel: async () => Promise.reject(new Error("boom")),
514
+ },
515
+ );
516
+ await expectPhase("prompt", { prompt: async () => Promise.reject(new Error("boom")) });
517
+
518
+ // Timeout path — a hung set-model must attribute to enforceModel AND flag timedOut.
519
+ const calls: RecordedCall[] = [];
520
+ try {
521
+ await driveProbeTurn(makeRecordingConnection(calls), {
522
+ ...base,
523
+ adapter: { buildSessionMeta: () => undefined, enforceModel: () => new Promise<never>(() => {}) },
524
+ timeouts: { initializeMs: 1000, newSessionMs: 1000, setModelMs: 50 },
525
+ promptObservationMs: 1000,
526
+ });
527
+ assert.fail("hung set-model must time the turn out");
528
+ } catch (err) {
529
+ assert.ok(err instanceof ProbePhaseError, "timeout is a ProbePhaseError");
530
+ assert.equal(err.phase, "enforceModel", "timeout attributes to enforceModel");
531
+ assert.equal(err.timedOut, true, "timeout is flagged timedOut");
532
+ }
533
+ }
534
+
535
+ // ===========================================================================
536
+ // 3) FIXTURE WIRE INSTRUMENTATION — real child, raw JSON-RPC, no API
537
+ // ===========================================================================
538
+
539
+ interface FixtureChild {
540
+ child: ChildProcessByStdio<Writable, Readable, Readable>;
541
+ request(msg: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
542
+ notify(msg: Record<string, unknown>): void;
543
+ }
544
+
545
+ function spawnFixture(env: Record<string, string>): FixtureChild {
546
+ const child = spawn(process.execPath, ["--experimental-strip-types", FIXTURE_PATH], {
547
+ cwd: tmp,
548
+ env: { ...process.env, ...env },
549
+ stdio: ["pipe", "pipe", "pipe"],
550
+ }) as ChildProcessByStdio<Writable, Readable, Readable>;
551
+ let buffer = "";
552
+ const pending: Array<{ id: number; resolve: (msg: Record<string, unknown>) => void }> = [];
553
+ child.stdout.on("data", (chunk) => {
554
+ buffer += chunk.toString();
555
+ let nl = buffer.indexOf("\n");
556
+ while (nl !== -1) {
557
+ const line = buffer.slice(0, nl).trim();
558
+ buffer = buffer.slice(nl + 1);
559
+ if (line.length > 0) {
560
+ const msg = JSON.parse(line) as Record<string, unknown>;
561
+ const i = pending.findIndex((p) => p.id === msg.id);
562
+ if (i !== -1) pending.splice(i, 1)[0].resolve(msg);
563
+ }
564
+ nl = buffer.indexOf("\n");
565
+ }
566
+ });
567
+ return {
568
+ child,
569
+ request(msg, timeoutMs = 10_000) {
570
+ return new Promise((resolvePromise, reject) => {
571
+ const timer = setTimeout(
572
+ () => reject(new Error(`fixture did not answer ${msg.method} within ${timeoutMs}ms`)),
573
+ timeoutMs,
574
+ );
575
+ pending.push({
576
+ id: msg.id as number,
577
+ resolve: (m) => {
578
+ clearTimeout(timer);
579
+ resolvePromise(m);
580
+ },
581
+ });
582
+ child.stdin.write(`${JSON.stringify(msg)}\n`);
583
+ });
584
+ },
585
+ notify(msg) {
586
+ child.stdin.write(`${JSON.stringify(msg)}\n`);
587
+ },
588
+ };
589
+ }
590
+
591
+ const INIT_PARAMS = {
592
+ protocolVersion: "2024-11-05",
593
+ capabilities: {},
594
+ clientInfo: { name: "check-probe-ordering", version: "0" },
595
+ };
596
+
597
+ // --- probe mode: delay honored, marker set stamped, probeRunId required -----
598
+ {
599
+ const logPath = join(tmp, "probe-mode.ndjson");
600
+ const delayMs = 400;
601
+ const fx = spawnFixture({
602
+ [PROBE_ENV.eventLog]: logPath,
603
+ [PROBE_ENV.startupDelayMs]: String(delayMs),
604
+ [PROBE_ENV.runId]: "gate-run",
605
+ [PROBE_ENV.nonce]: "GATE_NONCE",
606
+ });
607
+ try {
608
+ // The injected delay sits before serving: this first request only answers
609
+ // after ≥ delayMs (generous wire timeout keeps slow machines green).
610
+ await fx.request({ jsonrpc: "2.0", id: 1, method: "initialize", params: INIT_PARAMS });
611
+ fx.notify({ jsonrpc: "2.0", method: "notifications/initialized" });
612
+ const list = await fx.request({ jsonrpc: "2.0", id: 2, method: "tools/list" });
613
+ const tools = (list.result as { tools: Array<{ name: string; inputSchema?: { required?: string[] } }> }).tools;
614
+ assert.equal(tools.length, 1, "fixture serves exactly one tool");
615
+ assert.equal(tools[0].name, "probe_nonce", "the tool is probe_nonce");
616
+ assert.ok(
617
+ tools[0].inputSchema?.required?.includes("probeRunId"),
618
+ "probe mode REQUIRES probeRunId [QK:PROBE-FIXTURE-RUNID-REQUIRED] — the §11-7 cross-layer join key",
619
+ );
620
+
621
+ const good = await fx.request({
622
+ jsonrpc: "2.0",
623
+ id: 3,
624
+ method: "tools/call",
625
+ params: { name: "probe_nonce", arguments: { probeRunId: "prb-gate" } },
626
+ });
627
+ const goodText = JSON.stringify(good.result ?? {});
628
+ assert.ok(goodText.includes("GATE_NONCE"), "a correlated call returns the nonce");
629
+
630
+ const bad = await fx.request({
631
+ jsonrpc: "2.0",
632
+ id: 4,
633
+ method: "tools/call",
634
+ params: { name: "probe_nonce", arguments: {} },
635
+ });
636
+ const badIsError = bad.error !== undefined || (bad.result as { isError?: boolean } | undefined)?.isError === true;
637
+ assert.ok(badIsError, "a call WITHOUT probeRunId is refused");
638
+
639
+ // Marker set + ordering + honored delay, read off the one shared log.
640
+ // (appendFileSync in the fixture is synchronous, but poll briefly anyway so
641
+ // a slow fs never turns a green wire into a red gate.)
642
+ let events: ProbeEvent[] = [];
643
+ for (let i = 0; i < 20; i++) {
644
+ ({ events } = readProbeEvents(logPath));
645
+ if (events.some((e) => e.event === PROBE_EVENTS.fixtureToolsCallReplied)) break;
646
+ await new Promise((r) => setTimeout(r, 100));
647
+ }
648
+ const names = events.map((e) => e.event);
649
+ for (const expected of [
650
+ PROBE_EVENTS.fixtureProcessStart,
651
+ PROBE_EVENTS.fixtureDelayStart,
652
+ PROBE_EVENTS.fixtureDelayEnd,
653
+ PROBE_EVENTS.fixtureTransportConnected,
654
+ PROBE_EVENTS.fixtureInitializeReceived,
655
+ PROBE_EVENTS.fixtureToolsListReceived,
656
+ PROBE_EVENTS.toolsListResponseForwarded,
657
+ PROBE_EVENTS.fixtureToolsCallReceived,
658
+ PROBE_EVENTS.fixtureToolsCallReplied,
659
+ ]) {
660
+ assert.ok(names.includes(expected), `fixture stamps ${expected} [QK:PROBE-FIXTURE-WIRE-MARKER]`);
661
+ }
662
+ const delayStart = events.find((e) => e.event === PROBE_EVENTS.fixtureDelayStart);
663
+ const delayEnd = events.find((e) => e.event === PROBE_EVENTS.fixtureDelayEnd);
664
+ assert.ok(
665
+ delayStart && delayEnd && delayEnd.tsMs - delayStart.tsMs >= delayMs - 5,
666
+ `the startup delay is actually honored (${delayEnd?.tsMs}-${delayStart?.tsMs} vs ${delayMs})`,
667
+ );
668
+ const forwarded = events.find((e) => e.event === PROBE_EVENTS.toolsListResponseForwarded);
669
+ assert.ok(
670
+ Array.isArray(forwarded?.tools) && (forwarded.tools as string[]).includes("probe_nonce"),
671
+ "the forwarded marker names the expected tool",
672
+ );
673
+ const callReceived = events.find((e) => e.event === PROBE_EVENTS.fixtureToolsCallReceived);
674
+ assert.equal(callReceived?.probeRunId, "prb-gate", "the callability marker carries the correlation argument");
675
+ assert.ok(
676
+ (forwarded?.tsMs ?? 0) >= (delayEnd?.tsMs ?? Number.MAX_SAFE_INTEGER),
677
+ "nothing is forwarded before the injected delay elapses",
678
+ );
679
+ assert.ok(
680
+ events.every((e) => e.runId === "gate-run"),
681
+ "every fixture event carries the runId",
682
+ );
683
+ } finally {
684
+ await terminateChild(fx.child);
685
+ }
686
+ }
687
+
688
+ // --- legacy mode: byte-compatible surface for smoke-acp-mcp-live ------------
689
+ {
690
+ const legacyLog = join(tmp, "legacy-should-not-exist.ndjson");
691
+ const fx = spawnFixture({ [PROBE_ENV.nonce]: "LEGACY_NONCE" });
692
+ try {
693
+ await fx.request({ jsonrpc: "2.0", id: 1, method: "initialize", params: INIT_PARAMS });
694
+ fx.notify({ jsonrpc: "2.0", method: "notifications/initialized" });
695
+ const list = await fx.request({ jsonrpc: "2.0", id: 2, method: "tools/list" });
696
+ const tools = (list.result as { tools: Array<{ name: string; inputSchema?: { required?: string[] } }> }).tools;
697
+ assert.equal(tools[0]?.name, "probe_nonce", "legacy mode still serves probe_nonce");
698
+ assert.ok(
699
+ !tools[0].inputSchema?.required?.includes("probeRunId"),
700
+ "legacy mode does NOT require probeRunId (smoke-acp-mcp-live compat)",
701
+ );
702
+ const call = await fx.request({
703
+ jsonrpc: "2.0",
704
+ id: 3,
705
+ method: "tools/call",
706
+ params: { name: "probe_nonce", arguments: {} },
707
+ });
708
+ assert.ok(
709
+ JSON.stringify(call.result ?? {}).includes("LEGACY_NONCE"),
710
+ "legacy argument-less call still returns the nonce",
711
+ );
712
+ assert.ok(!existsSync(legacyLog), "legacy mode writes no event log");
713
+ } finally {
714
+ await terminateChild(fx.child);
715
+ }
716
+ }
717
+
718
+ // ===========================================================================
719
+ // 4) EVENT LOG INTEGRITY — the envelope contract at the door
720
+ // ===========================================================================
721
+ //
722
+ // The shared NDJSON log is the record the verdict is read off, so a line that
723
+ // merely parses as JSON is not yet an event. §11-7's two decisive reads are
724
+ // ABSENCE (a marker that never appears) and ORDERING (one tsMs axis across
725
+ // processes) — exactly what a typo'd marker name or a broken stamp corrupts
726
+ // while still looking healthy. Such a line must leave through the SAME door as
727
+ // a truncated one (malformed → the runner's INVALIDATED path above), and a
728
+ // caller's payload must never be able to rewrite the envelope it rides in.
729
+ {
730
+ // Writer round-trip: its own line clears the door, payload rides alongside,
731
+ // and the readable stamp agrees with the sort axis EXACTLY.
732
+ const okPath = join(tmp, "envelope-ok.ndjson");
733
+ appendProbeEvent(okPath, "env-run", PROBE_EVENTS.runStart, { note: "hello" });
734
+ const roundTrip = readProbeEvents(okPath);
735
+ assert.equal(roundTrip.malformed.length, 0, "the writer's own line clears the envelope contract");
736
+ assert.equal(roundTrip.events.length, 1, "…and lands as exactly one event");
737
+ const written = roundTrip.events[0];
738
+ assert.equal(Date.parse(written.ts), written.tsMs, "ts is DERIVED from tsMs — no millisecond straddle");
739
+ assert.equal(written.note, "hello", "payload rides alongside the envelope");
740
+ assert.ok(
741
+ EVENT_LOG_SRC.includes("const tsMs = Date.now();") && !EVENT_LOG_SRC.includes("ts: new Date().toISOString()"),
742
+ "the writer reads the clock ONCE and derives ts from it (a second read could straddle a millisecond)",
743
+ );
744
+
745
+ // Envelope authority: a payload key may never rewrite the run id, the marker
746
+ // name, or the sort axis of its own evidence line. The write REFUSES — a
747
+ // silent merge (either key order) would leave a lying line on disk.
748
+ const authPath = join(tmp, "envelope-authority.ndjson");
749
+ const refusedEveryKey = RESERVED_EVENT_KEYS.every((key) => {
750
+ try {
751
+ appendProbeEvent(authPath, "env-run", PROBE_EVENTS.runStart, { [key]: "hijacked" });
752
+ return false;
753
+ } catch {
754
+ return true;
755
+ }
756
+ });
757
+ assert.ok(
758
+ refusedEveryKey && !existsSync(authPath),
759
+ "a payload carrying a reserved envelope key is REFUSED and nothing is written [QK:PROBE-LOG-ENVELOPE-AUTHORITY]",
760
+ );
761
+
762
+ const STAMP_MS = 1_700_000_000_000;
763
+ const stamp = { seq: 0, pid: 1234, ts: new Date(STAMP_MS).toISOString(), tsMs: STAMP_MS, runId: "env-run" };
764
+
765
+ // Vocabulary: a name neither writer nor classifier knows is not a new event —
766
+ // it is a marker that went MISSING, and §11-7 reads absence as evidence.
767
+ const vocabPath = join(tmp, "envelope-vocab.ndjson");
768
+ appendFileSync(vocabPath, `${JSON.stringify({ ...stamp, event: "new_sesion_end" })}\n`, "utf8");
769
+ const vocab = readProbeEvents(vocabPath);
770
+ assert.ok(
771
+ vocab.events.length === 0 && vocab.malformed.length === 1,
772
+ "an unknown event name is a MISSING marker, never a quiet extra line [QK:PROBE-LOG-EVENT-VOCABULARY]",
773
+ );
774
+
775
+ // Structural envelope: the shared sort axis and its readable twin. A tsMs
776
+ // that is absent or non-numeric makes the comparator NaN and un-orders the
777
+ // whole log, so ran-ahead / ordering-kept would be read off file order.
778
+ // These lines deliberately carry an event with NO payload rule (run_start is
779
+ // forensic): the envelope checks must be what rejects them, or this claim
780
+ // would be silently proven by the payload layer instead of its own code.
781
+ const brokenPath = join(tmp, "envelope-broken.ndjson");
782
+ const brokenLines = [
783
+ { ...stamp, event: PROBE_EVENTS.runStart, tsMs: undefined }, // no sort axis at all
784
+ { ...stamp, event: PROBE_EVENTS.runStart, tsMs: "1700000000000" }, // string axis → NaN comparator
785
+ { ...stamp, event: PROBE_EVENTS.runStart, ts: "not-a-timestamp" }, // readable twin unparseable
786
+ { ...stamp, event: PROBE_EVENTS.runStart, tsMs: STAMP_MS + 1 }, // twin disagrees by 1ms
787
+ { ...stamp, event: PROBE_EVENTS.runStart, seq: -1 }, // per-process counter
788
+ { ...stamp, event: PROBE_EVENTS.runStart, pid: 0 }, // writer identity
789
+ { ...stamp, event: PROBE_EVENTS.runStart, runId: "" }, // unattributable line
790
+ ];
791
+ for (const line of brokenLines) appendFileSync(brokenPath, `${JSON.stringify(line)}\n`, "utf8");
792
+ const broken = readProbeEvents(brokenPath);
793
+ assert.ok(
794
+ broken.events.length === 0 && broken.malformed.length === brokenLines.length,
795
+ "a JSON-valid line with a broken envelope is MALFORMED, never an event [QK:PROBE-LOG-ENVELOPE-SCHEMA]",
796
+ );
797
+
798
+ // Payload contract: the envelope can be perfect and the line still a lie,
799
+ // because the classifier judges on payload. `ok === true` is FALSE for the
800
+ // string "true", so a corrupted phase end silently reads as a phase FAILURE
801
+ // (D / P0) instead of invalidating the run; an uncorrelatable id does the
802
+ // same toward absence. Every field the classifier reads is typed at the door.
803
+ const payloadPath = join(tmp, "payload-broken.ndjson");
804
+ const badPayloads = [
805
+ { ...stamp, event: PROBE_EVENTS.newSessionEnd }, // no ok at all → reads as "failed"
806
+ { ...stamp, event: PROBE_EVENTS.promptEnd, ok: "true" }, // string, not boolean
807
+ { ...stamp, event: PROBE_EVENTS.initializeEnd, ok: true, timedOut: "yes" },
808
+ { ...stamp, event: PROBE_EVENTS.setModelEnd, ok: false, error: 42 },
809
+ { ...stamp, event: PROBE_EVENTS.acpNoSuchTool, toolId: 7 }, // the absence id
810
+ { ...stamp, event: PROBE_EVENTS.acpToolCallObserved, providerToolId: "x" }, // no join key
811
+ { ...stamp, event: PROBE_EVENTS.acpToolCallObserved, probeRunId: 9 },
812
+ { ...stamp, event: PROBE_EVENTS.promptReply, carriesNonce: "yes" }, // the nonce echo
813
+ { ...stamp, event: PROBE_EVENTS.fixtureToolsCallReceived, tool: 1, probeRunId: "p" },
814
+ ];
815
+ for (const line of badPayloads) appendFileSync(payloadPath, `${JSON.stringify(line)}\n`, "utf8");
816
+ const payloadBroken = readProbeEvents(payloadPath);
817
+ assert.ok(
818
+ payloadBroken.events.length === 0 && payloadBroken.malformed.length === badPayloads.length,
819
+ "a valid envelope with a payload the classifier cannot judge on is MALFORMED [QK:PROBE-LOG-PAYLOAD-SCHEMA]",
820
+ );
821
+
822
+ // …and the readings the writer legitimately omits are NOT malformed: the
823
+ // fixture stamps an inbound tools/call before validating it, and an ACP frame
824
+ // may carry no name/title. Refusing those would manufacture INVALIDATED runs
825
+ // out of real model behavior (a call with no join key IS the absence reading).
826
+ const observedPath = join(tmp, "payload-observed.ndjson");
827
+ const observedLines = [
828
+ { ...stamp, event: PROBE_EVENTS.fixtureToolsCallReceived, tool: PROBE_EXPECTED_TOOL }, // model called with no join key
829
+ { ...stamp, event: PROBE_EVENTS.acpToolCallObserved, probeRunId: "prb-x" }, // no extractable provider id
830
+ { ...stamp, event: PROBE_EVENTS.newSessionEnd, ok: false, timedOut: true, error: "boom" },
831
+ ];
832
+ for (const line of observedLines) appendFileSync(observedPath, `${JSON.stringify(line)}\n`, "utf8");
833
+ const observed = readProbeEvents(observedPath);
834
+ assert.ok(
835
+ observed.malformed.length === 0 && observed.events.length === observedLines.length,
836
+ "a legitimately absent optional field is an OBSERVATION, not a corrupt line",
837
+ );
838
+
839
+ // The wire-availability proxy must NAME the expected tool. §11-7 defines the
840
+ // marker as the expected-tool tools/list frame reaching the pipe, so a marker
841
+ // carrying another list is not that event — and it drives ran-ahead, C, and
842
+ // the B branch. (The fixture only stamps it when the tool is present; this is
843
+ // the same check at the READING door, where the verdict is actually taken.)
844
+ const wirePath = join(tmp, "payload-wire.ndjson");
845
+ const wireLines = [
846
+ { ...stamp, event: PROBE_EVENTS.toolsListResponseForwarded }, // no tools at all
847
+ { ...stamp, event: PROBE_EVENTS.toolsListResponseForwarded, tools: [] }, // served nothing
848
+ { ...stamp, event: PROBE_EVENTS.toolsListResponseForwarded, tools: ["other_tool"] },
849
+ { ...stamp, event: PROBE_EVENTS.toolsListResponseForwarded, tools: [7] },
850
+ ];
851
+ for (const line of wireLines) appendFileSync(wirePath, `${JSON.stringify(line)}\n`, "utf8");
852
+ const wire = readProbeEvents(wirePath);
853
+ assert.ok(
854
+ wire.events.length === 0 && wire.malformed.length === wireLines.length,
855
+ "a wire marker that does not name the expected tool is not wire-availability [QK:PROBE-LOG-WIRE-MARKER-NAMES-TOOL]",
856
+ );
857
+ const goodWirePath = join(tmp, "payload-wire-ok.ndjson");
858
+ appendFileSync(
859
+ goodWirePath,
860
+ `${JSON.stringify({ ...stamp, event: PROBE_EVENTS.toolsListResponseForwarded, tools: [PROBE_EXPECTED_TOOL, "x"] })}\n`,
861
+ "utf8",
862
+ );
863
+ assert.equal(readProbeEvents(goodWirePath).events.length, 1, "a marker naming the expected tool still passes");
864
+
865
+ // Coverage, stated as a hand-written literal (never read off the SUT): these
866
+ // are exactly the events the classifier judges payload on. A new classifier
867
+ // read without a door rule must turn this red rather than pass unnoticed.
868
+ assert.deepEqual(
869
+ [...PAYLOAD_CONTRACT_EVENTS].sort(),
870
+ [
871
+ PROBE_EVENTS.acpNoSuchTool,
872
+ PROBE_EVENTS.acpToolCallObserved,
873
+ PROBE_EVENTS.fixtureToolsCallReceived,
874
+ PROBE_EVENTS.initializeEnd,
875
+ PROBE_EVENTS.newSessionEnd,
876
+ PROBE_EVENTS.observationWindowEnd,
877
+ PROBE_EVENTS.promptEnd,
878
+ PROBE_EVENTS.promptReply,
879
+ PROBE_EVENTS.setModelEnd,
880
+ PROBE_EVENTS.toolsListResponseForwarded,
881
+ PROBE_EVENTS.shimBoot,
882
+ PROBE_EVENTS.shimPromptForwarded,
883
+ PROBE_EVENTS.shimInitSnapshot,
884
+ ].sort(),
885
+ "the payload contract covers exactly the events the classifier judges on",
886
+ );
887
+
888
+ // One SSOT for the expected tool: fixture, parser, and classifier must not
889
+ // each keep a private literal that can drift apart in silence.
890
+ assert.ok(
891
+ EVENT_LOG_SRC.split('"probe_nonce"').length - 1 === 1 &&
892
+ !FIXTURE_SRC.includes('= "probe_nonce"') &&
893
+ !VERDICT_SRC.includes('= "probe_nonce"'),
894
+ "the expected tool name is defined ONCE (probe-event-log.ts) and imported by fixture and classifier",
895
+ );
896
+
897
+ // Truncation still behaves — the widened door did not lose the original one.
898
+ const mixedPath = join(tmp, "envelope-mixed.ndjson");
899
+ appendProbeEvent(mixedPath, "env-run", PROBE_EVENTS.runStart);
900
+ appendFileSync(mixedPath, '{"event":"run_end","runId":"env-run"\n', "utf8");
901
+ const mixed = readProbeEvents(mixedPath);
902
+ assert.ok(
903
+ mixed.events.length === 1 && mixed.malformed.length === 1,
904
+ "an unparseable line is still malformed and the good line still parses",
905
+ );
906
+
907
+ // The window close is judged payload: `reason` decides whether a missing wire
908
+ // marker is a reading or our own teardown, so an unknown reason must not fall
909
+ // through to whatever branch consumes it.
910
+ const winPath = join(tmp, "payload-window.ndjson");
911
+ const winStamp = { seq: 0, pid: 7, ts: new Date(2_000).toISOString(), tsMs: 2_000, runId: "w" };
912
+ appendFileSync(
913
+ winPath,
914
+ `${JSON.stringify({ ...winStamp, event: PROBE_EVENTS.observationWindowEnd, reason: "whenever", markerSeen: false })}\n`,
915
+ "utf8",
916
+ );
917
+ appendFileSync(
918
+ winPath,
919
+ `${JSON.stringify({ ...winStamp, seq: 1, tsMs: 2_001, ts: new Date(2_001).toISOString(), event: PROBE_EVENTS.observationWindowEnd, reason: "deadline", markerSeen: "no" })}\n`,
920
+ "utf8",
921
+ );
922
+ appendFileSync(
923
+ winPath,
924
+ `${JSON.stringify({ ...winStamp, seq: 2, tsMs: 2_002, ts: new Date(2_002).toISOString(), event: PROBE_EVENTS.observationWindowEnd, reason: "deadline", markerSeen: false })}\n`,
925
+ "utf8",
926
+ );
927
+ const win = readProbeEvents(winPath);
928
+ assert.ok(
929
+ win.malformed.length === 2 && win.events.length === 1,
930
+ "an unknown window reason and a non-boolean markerSeen are MALFORMED; the closed vocabulary passes [QK:PROBE-LOG-WINDOW-REASON-VOCAB]",
931
+ );
932
+ }
933
+
934
+ // ===========================================================================
935
+ // 4b) EVENT LOG STREAM INTEGRITY — per-writer order, checked on RAW APPEND ORDER
936
+ // ===========================================================================
937
+ {
938
+ const stream = (lines: Array<Record<string, unknown>>): ReturnType<typeof readProbeEvents> => {
939
+ const p = join(tmp, `stream-${Math.random().toString(36).slice(2)}.ndjson`);
940
+ for (const l of lines) appendFileSync(p, `${JSON.stringify(l)}\n`, "utf8");
941
+ return readProbeEvents(p);
942
+ };
943
+ const line = (pid: number, seq: number, tsMs: number, event = PROBE_EVENTS.acpToolCallRaw) => ({
944
+ seq,
945
+ pid,
946
+ ts: new Date(tsMs).toISOString(),
947
+ tsMs,
948
+ runId: "s",
949
+ event,
950
+ });
951
+
952
+ // Clean stream: two writers interleaved, each strictly increasing.
953
+ const clean = stream([line(1, 0, 100), line(2, 0, 101), line(1, 1, 102), line(2, 1, 103)]);
954
+ assert.equal(clean.sequenceViolations.length, 0, "interleaved writers, each monotonic, is a clean stream");
955
+
956
+ // GAPS are fine — the counter is process-wide, not file-wide, so a process
957
+ // writing to more than one log skips numbers here by construction.
958
+ const gapped = stream([line(1, 0, 100), line(1, 7, 101), line(1, 90, 102)]);
959
+ assert.equal(
960
+ gapped.sequenceViolations.length,
961
+ 0,
962
+ "a per-pid seq GAP is not a violation — the counter is process-wide",
963
+ );
964
+
965
+ // A repeat is two lines claiming one slot: within a millisecond they cannot be
966
+ // ordered against each other at all.
967
+ const dupSeq = stream([line(1, 5, 100), line(1, 5, 100)]);
968
+ assert.ok(
969
+ dupSeq.sequenceViolations.length === 1 && /seq 5 does not exceed/.test(dupSeq.sequenceViolations[0]),
970
+ "a repeated per-pid seq is a stream violation [QK:PROBE-LOG-SEQ-STRICTLY-INCREASING]",
971
+ );
972
+ const backSeq = stream([line(1, 5, 100), line(1, 4, 101)]);
973
+ assert.equal(backSeq.sequenceViolations.length, 1, "a per-pid seq going backwards is a stream violation");
974
+
975
+ // THE claim, asserted BEFORE the clock rule below on purpose: validation runs
976
+ // on the RAW append order. This log sorts into perfect order — a post-sort
977
+ // check would see nothing — yet the file itself has pid 1 writing seq 9 before
978
+ // seq 2. (Ordering matters here: a check moved after the sort also perturbs the
979
+ // clock-regression case, so this assertion must be the one that fires.)
980
+ const outOfOrderInFile = stream([line(1, 9, 900), line(1, 2, 200)]);
981
+ assert.ok(
982
+ outOfOrderInFile.sequenceViolations.length > 0,
983
+ "per-writer order is judged on the RAW file order, not after the sort has rewritten it [QK:PROBE-LOG-RAW-ORDER-BEFORE-SORT]",
984
+ );
985
+ assert.deepEqual(
986
+ outOfOrderInFile.events.map((e) => e.seq),
987
+ [2, 9],
988
+ "the returned events are still sorted — the violation is reported, not repaired",
989
+ );
990
+
991
+ // One process reads one clock. A regression means the stamps were rewritten or
992
+ // the clock stepped — and those stamps ARE the ordering evidence.
993
+ const backTs = stream([line(1, 0, 500), line(1, 1, 499)]);
994
+ assert.ok(
995
+ backTs.sequenceViolations.length === 1 && /runs BACKWARDS/.test(backTs.sequenceViolations[0]),
996
+ "a per-pid tsMs regression is a stream violation [QK:PROBE-LOG-TS-NO-REGRESSION]",
997
+ );
998
+ // Different pids are independent: cross-process stamps are not comparable this
999
+ // way, and demanding it would flag every normal interleaving.
1000
+ assert.equal(
1001
+ stream([line(1, 0, 500), line(2, 0, 499)]).sequenceViolations.length,
1002
+ 0,
1003
+ "a LOWER stamp from a DIFFERENT pid is not a regression",
1004
+ );
1005
+
1006
+ // WRITER KEY is (runId, pid). The fixture is a fresh child per run and the OS
1007
+ // reuses pids, so a later run's fixture can legitimately hold the same pid and
1008
+ // restart its counter at 0. Keying on pid alone would call that healthy log
1009
+ // corrupt — and cross-run ordering is not something any verdict reads.
1010
+ const pidReuse = stream([
1011
+ { ...line(1, 0, 100), runId: "r1" },
1012
+ { ...line(1, 1, 101), runId: "r1" },
1013
+ { ...line(1, 0, 200), runId: "r2" },
1014
+ { ...line(1, 1, 201), runId: "r2" },
1015
+ ]);
1016
+ assert.equal(
1017
+ pidReuse.sequenceViolations.length,
1018
+ 0,
1019
+ "a reused pid restarting its counter in a LATER run is not a violation — the writer key is (runId, pid) [QK:PROBE-LOG-WRITER-KEY-PER-RUN]",
1020
+ );
1021
+ // …but within one run the same pid is still held to the rule.
1022
+ assert.equal(
1023
+ stream([
1024
+ { ...line(1, 5, 100), runId: "r1" },
1025
+ { ...line(1, 5, 101), runId: "r1" },
1026
+ ]).sequenceViolations.length,
1027
+ 1,
1028
+ "the same pid inside ONE run is still strictly increasing",
1029
+ );
1030
+
1031
+ // Malformed lines never participate: they are not events, so they cannot
1032
+ // manufacture a sequence violation on top of their own refusal.
1033
+ const withMalformed = stream([line(1, 0, 100), { junk: true }, line(1, 1, 101)]);
1034
+ assert.ok(
1035
+ withMalformed.malformed.length === 1 && withMalformed.sequenceViolations.length === 0,
1036
+ "a malformed line is refused at the line door and does not also break the stream door",
1037
+ );
1038
+ }
1039
+
1040
+ // ===========================================================================
1041
+ // 5) VERDICT TRUTH TABLE — synthetic paired logs through the PURE classifier
1042
+ // ===========================================================================
1043
+
1044
+ let syntheticSeq = 0;
1045
+ function ev(runId: string, event: string, tsMs: number, payload: Record<string, unknown> = {}): ProbeEvent {
1046
+ return {
1047
+ seq: syntheticSeq++,
1048
+ pid: 1,
1049
+ ts: new Date(tsMs).toISOString(),
1050
+ tsMs,
1051
+ runId,
1052
+ event,
1053
+ ...payload,
1054
+ } as ProbeEvent;
1055
+ }
1056
+
1057
+ interface SyntheticRunSpec {
1058
+ runId: string;
1059
+ probeRunId: string;
1060
+ base: number;
1061
+ /** newSession latency; wire marker fires at newSessionStart + wireAt. */
1062
+ nsLatency?: number;
1063
+ wireAt?: number | null; // null → no wire marker at all
1064
+ failPhase?: "initialize" | "newSession" | "enforceModel" | "prompt";
1065
+ fixtureCall?: boolean;
1066
+ fixtureCallAt?: number; // offset from newSessionStart
1067
+ providerToolId?: string;
1068
+ noSuchToolId?: string;
1069
+ nonceEchoed?: boolean;
1070
+ /** How the observation window closed. Default: inferred — the marker was seen
1071
+ * iff a wire marker exists, and the reason follows from that. `"omit"` drops
1072
+ * the marker entirely, which is a TOPOLOGY violation, not a soft default:
1073
+ * without it a missing wire marker cannot be told from our own teardown. */
1074
+ window?: { reason?: ProbeWindowReason; markerSeen?: boolean } | "omit";
1075
+ /** Extra runner-owned lines appended verbatim — used to build duplicate /
1076
+ * end-without-start topology corruptions. */
1077
+ extra?: (runId: string, base: number) => ProbeEvent[];
1078
+ }
1079
+
1080
+ /** Close a synthetic run the way the runner does: the observation window is
1081
+ * stamped BEFORE run_end, on the success and the failure path alike. */
1082
+ function closeRun(out: ProbeEvent[], spec: SyntheticRunSpec, at: number, failed: boolean): ProbeEvent[] {
1083
+ const { runId } = spec;
1084
+ if (spec.window !== "omit") {
1085
+ // Read markerSeen off what the run ACTUALLY emitted, never off the spec: the
1086
+ // classifier now checks the self-reported flag against the log, so a helper
1087
+ // that guesses would manufacture incoherent fixtures (a run that failed at
1088
+ // initialize never reaches the wire marker, whatever `wireAt` says).
1089
+ const emittedWire = out.some((e) => e.event === PROBE_EVENTS.toolsListResponseForwarded);
1090
+ const markerSeen = spec.window?.markerSeen ?? emittedWire;
1091
+ const reason: ProbeWindowReason =
1092
+ spec.window?.reason ?? (failed ? "run-failed" : markerSeen ? "wire-marker" : "deadline");
1093
+ out.push(ev(runId, PROBE_EVENTS.observationWindowEnd, at, { reason, markerSeen }));
1094
+ }
1095
+ out.push(ev(runId, PROBE_EVENTS.runEnd, at + 1, { ok: !failed }));
1096
+ if (spec.extra) out.push(...spec.extra(runId, spec.base));
1097
+ return out;
1098
+ }
1099
+
1100
+ function syntheticRun(spec: SyntheticRunSpec): ProbeEvent[] {
1101
+ const { runId, probeRunId, base } = spec;
1102
+ const nsStart = base + 100;
1103
+ const nsLatency = spec.nsLatency ?? 300;
1104
+ const out: ProbeEvent[] = [ev(runId, PROBE_EVENTS.runStart, base), ev(runId, PROBE_EVENTS.initializeStart, base)];
1105
+ if (spec.failPhase === "initialize") {
1106
+ out.push(ev(runId, PROBE_EVENTS.initializeEnd, base + 50, { ok: false, error: "init boom" }));
1107
+ return closeRun(out, spec, base + 60, true);
1108
+ }
1109
+ out.push(ev(runId, PROBE_EVENTS.initializeEnd, base + 50, { ok: true }));
1110
+ out.push(ev(runId, PROBE_EVENTS.newSessionStart, nsStart));
1111
+ if (spec.wireAt !== null)
1112
+ out.push(ev(runId, PROBE_EVENTS.toolsListResponseForwarded, nsStart + (spec.wireAt ?? 100)));
1113
+ if (spec.failPhase === "newSession") {
1114
+ out.push(
1115
+ ev(runId, PROBE_EVENTS.newSessionEnd, nsStart + nsLatency, { ok: false, timedOut: true, error: "ns boom" }),
1116
+ );
1117
+ return closeRun(out, spec, nsStart + nsLatency + 10, true);
1118
+ }
1119
+ out.push(ev(runId, PROBE_EVENTS.newSessionEnd, nsStart + nsLatency, { ok: true }));
1120
+ const smStart = nsStart + nsLatency + 10;
1121
+ out.push(ev(runId, PROBE_EVENTS.setModelStart, smStart));
1122
+ if (spec.failPhase === "enforceModel") {
1123
+ out.push(ev(runId, PROBE_EVENTS.setModelEnd, smStart + 30, { ok: false, error: "sm boom" }));
1124
+ return closeRun(out, spec, smStart + 40, true);
1125
+ }
1126
+ out.push(ev(runId, PROBE_EVENTS.setModelEnd, smStart + 30, { ok: true }));
1127
+ const pStart = smStart + 50;
1128
+ out.push(ev(runId, PROBE_EVENTS.promptStart, pStart));
1129
+ if (spec.providerToolId !== undefined) {
1130
+ out.push(
1131
+ ev(runId, PROBE_EVENTS.acpToolCallObserved, pStart + 100, { providerToolId: spec.providerToolId, probeRunId }),
1132
+ );
1133
+ }
1134
+ if (spec.fixtureCall) {
1135
+ out.push(
1136
+ ev(runId, PROBE_EVENTS.fixtureToolsCallReceived, nsStart + (spec.fixtureCallAt ?? 500), {
1137
+ tool: "probe_nonce",
1138
+ probeRunId,
1139
+ }),
1140
+ );
1141
+ }
1142
+ if (spec.noSuchToolId !== undefined) {
1143
+ out.push(ev(runId, PROBE_EVENTS.acpNoSuchTool, pStart + 150, { toolId: spec.noSuchToolId }));
1144
+ }
1145
+ if (spec.failPhase === "prompt") {
1146
+ out.push(ev(runId, PROBE_EVENTS.promptEnd, pStart + 200, { ok: false, error: "p boom" }));
1147
+ return closeRun(out, spec, pStart + 210, true);
1148
+ }
1149
+ // REAL writer order: driveProbeTurn stamps prompt_end and returns, THEN the
1150
+ // runner stamps prompt_reply. The synthetic corpus had these reversed until
1151
+ // the topology rule caught it (GPT review round 2, 2026-07-29).
1152
+ out.push(ev(runId, PROBE_EVENTS.promptEnd, pStart + 300, { ok: true }));
1153
+ out.push(ev(runId, PROBE_EVENTS.promptReply, pStart + 310, { carriesNonce: spec.nonceEchoed ?? true }));
1154
+ return closeRun(out, spec, pStart + 310, false);
1155
+ }
1156
+
1157
+ const PROVIDER_ID_MEASURED = "mcp__probe__probe_nonce";
1158
+
1159
+ function passingControl(base = 0): { record: ProbeRunRecord; events: ProbeEvent[] } {
1160
+ return {
1161
+ record: { runId: "ctl", role: "control", delayMs: 0, probeRunId: "prb-ctl", snapshotInstrumented: false },
1162
+ events: syntheticRun({
1163
+ runId: "ctl",
1164
+ probeRunId: "prb-ctl",
1165
+ base,
1166
+ wireAt: 50,
1167
+ nsLatency: 300,
1168
+ fixtureCall: true,
1169
+ providerToolId: PROVIDER_ID_MEASURED,
1170
+ nonceEchoed: true,
1171
+ }),
1172
+ };
1173
+ }
1174
+
1175
+ function intervention(
1176
+ runId: string,
1177
+ delayMs: number,
1178
+ base: number,
1179
+ spec: Partial<SyntheticRunSpec>,
1180
+ ): { record: ProbeRunRecord; events: ProbeEvent[] } {
1181
+ const probeRunId = `prb-${runId}`;
1182
+ return {
1183
+ record: { runId, role: "intervention", delayMs, probeRunId, snapshotInstrumented: false },
1184
+ events: syntheticRun({ runId, probeRunId, base, ...spec }),
1185
+ };
1186
+ }
1187
+
1188
+ // --- P0: control failures invalidate the whole experiment -------------------
1189
+ {
1190
+ const ctl = {
1191
+ record: {
1192
+ runId: "ctl",
1193
+ role: "control",
1194
+ delayMs: 0,
1195
+ probeRunId: "prb-ctl",
1196
+ snapshotInstrumented: false,
1197
+ } as ProbeRunRecord,
1198
+ events: syntheticRun({ runId: "ctl", probeRunId: "prb-ctl", base: 0, failPhase: "initialize" }),
1199
+ };
1200
+ const d1 = intervention("d1", 2000, 10_000, { wireAt: 2100, nsLatency: 2400, fixtureCall: true });
1201
+ const res = classifyProbe([ctl.record, d1.record], [...ctl.events, ...d1.events]);
1202
+ assert.equal(res.verdict, "P0", "control initialize failure → P0");
1203
+ assert.equal(res.control.p0Reason, "initialize", "P0 carries reason=initialize");
1204
+ assert.equal(res.interventions.length, 0, "no intervention is judged under P0");
1205
+
1206
+ const ctl2 = {
1207
+ record: {
1208
+ runId: "ctl",
1209
+ role: "control",
1210
+ delayMs: 0,
1211
+ probeRunId: "prb-ctl",
1212
+ snapshotInstrumented: false,
1213
+ } as ProbeRunRecord,
1214
+ events: syntheticRun({
1215
+ runId: "ctl",
1216
+ probeRunId: "prb-ctl",
1217
+ base: 0,
1218
+ wireAt: 50,
1219
+ fixtureCall: false, // visible? unproven — never callable
1220
+ providerToolId: undefined,
1221
+ nonceEchoed: false,
1222
+ }),
1223
+ };
1224
+ const res2 = classifyProbe([ctl2.record, d1.record], [...ctl2.events, ...d1.events]);
1225
+ assert.equal(res2.verdict, "P0", "control without the callability marker → P0 [QK:VERDICT-P0-CONTROL-FAIL]");
1226
+ assert.equal(res2.control.p0Reason, "tool-unavailable", "P0 names tool-unavailable");
1227
+ }
1228
+
1229
+ // --- I0: intervention initialize failure is drift, never a D ----------------
1230
+ {
1231
+ const ctl = passingControl();
1232
+ const d1 = intervention("d1", 2000, 10_000, { failPhase: "initialize" });
1233
+ const res = classifyProbe([ctl.record, d1.record], [...ctl.events, ...d1.events]);
1234
+ assert.equal(res.verdict, "I0", "intervention initialize failure → I0, never D [QK:VERDICT-I0-NEVER-D]");
1235
+ }
1236
+
1237
+ // --- D: phase-qualified fail-loud readings ----------------------------------
1238
+ {
1239
+ for (const [phase, expected] of [
1240
+ ["newSession", "D-newSession"],
1241
+ ["enforceModel", "D-enforceModel"],
1242
+ ["prompt", "D-prompt"],
1243
+ ] as const) {
1244
+ const ctl = passingControl();
1245
+ const d1 = intervention("d1", 2000, 10_000, { failPhase: phase, wireAt: null });
1246
+ const res = classifyProbe([ctl.record, d1.record], [...ctl.events, ...d1.events]);
1247
+ assert.equal(res.verdict, expected, `${phase} failure → ${expected}`);
1248
+ }
1249
+ }
1250
+
1251
+ // --- B promotion ladder — B is ONLY the marker-grade combination ------------
1252
+ {
1253
+ // (a) no wire marker at all → NOT B: the §11-7 promotion ladder files it as an
1254
+ // MCP handshake / fixture / config CANDIDATE and keeps unlisted combinations
1255
+ // inconclusive. Reading it as B would let a fixture that never served
1256
+ // manufacture a sufficiency verdict (GPT review 2026-07-28).
1257
+ const ctl = passingControl();
1258
+ const noWire = intervention("d1", 2000, 10_000, { wireAt: null, fixtureCall: false, nonceEchoed: false });
1259
+ const resA = classifyProbe([ctl.record, noWire.record], [...ctl.events, ...noWire.events]);
1260
+ assert.equal(
1261
+ resA.verdict,
1262
+ "inconclusive",
1263
+ "absence without the wire marker is a handshake/fixture/config candidate, never B [QK:VERDICT-NOWIRE-CANDIDATE]",
1264
+ );
1265
+ assert.equal(resA.promotable, false, "…and never promotes");
1266
+
1267
+ // (b) wire forwarded + no fixture call + No-such-tool naming the MEASURED id → promotable B.
1268
+ const exact = intervention("d1", 2000, 10_000, {
1269
+ wireAt: 2600,
1270
+ nsLatency: 300,
1271
+ fixtureCall: false,
1272
+ noSuchToolId: PROVIDER_ID_MEASURED,
1273
+ nonceEchoed: false,
1274
+ });
1275
+ const resB = classifyProbe([ctl.record, exact.record], [...ctl.events, ...exact.events]);
1276
+ assert.equal(
1277
+ resB.verdict,
1278
+ "B",
1279
+ "marker-complete absence reads B — the runtime No-such-tool ladder OWNS runtime-error runs; no other channel may stand in for it [QK:VERDICT-RUNTIME-B-LADDER-OWNS]",
1280
+ );
1281
+ assert.equal(resB.promotable, true, "exact measured-id No-such-tool promotes");
1282
+
1283
+ // (b2) same markers but the turn did NOT run ahead (wire forwarded BEFORE
1284
+ // newSession end) → NOT B: the delta-table B is "the delayed run puts the
1285
+ // turn ahead of wire-availability AND yields absence" — an exact-id absence
1286
+ // with ordering kept is a different, unlisted finding.
1287
+ const keptAbsence = intervention("k1", 2000, 250_000, {
1288
+ wireAt: 100,
1289
+ nsLatency: 2300,
1290
+ fixtureCall: false,
1291
+ noSuchToolId: PROVIDER_ID_MEASURED,
1292
+ nonceEchoed: false,
1293
+ });
1294
+ const resB2 = classifyProbe([ctl.record, keptAbsence.record], [...ctl.events, ...keptAbsence.events]);
1295
+ assert.equal(
1296
+ resB2.verdict,
1297
+ "inconclusive",
1298
+ "exact-id absence WITHOUT running ahead of wire-availability is not delta-B [QK:VERDICT-B-REQUIRES-RANAHEAD]",
1299
+ );
1300
+ assert.equal(resB2.promotable, false, "…and never promotes");
1301
+
1302
+ // (c) alias/bare-name No-such-tool → model/alias mismatch, NOT absence
1303
+ // evidence (§11-7 ladder: the real provider-bound id may have been in schema).
1304
+ const alias = intervention("d1", 2000, 10_000, {
1305
+ wireAt: 2600,
1306
+ nsLatency: 300,
1307
+ fixtureCall: false,
1308
+ noSuchToolId: "probe_nonce",
1309
+ nonceEchoed: false,
1310
+ });
1311
+ const resC = classifyProbe([ctl.record, alias.record], [...ctl.events, ...alias.events]);
1312
+ assert.equal(resC.verdict, "inconclusive", "alias-mismatch absence is not B [QK:VERDICT-B-PROMOTION-RULES]");
1313
+ assert.equal(resC.promotable, false, "alias mismatch never promotes");
1314
+ }
1315
+
1316
+ // --- model prose alone never reads as evidence ------------------------------
1317
+ {
1318
+ const ctl = passingControl();
1319
+ const prose = intervention("d1", 2000, 10_000, {
1320
+ wireAt: 2600,
1321
+ nsLatency: 300,
1322
+ fixtureCall: false,
1323
+ nonceEchoed: false, // model SAID the tool is missing; no marker, no error
1324
+ });
1325
+ const res = classifyProbe([ctl.record, prose.record], [...ctl.events, ...prose.events]);
1326
+ assert.equal(res.verdict, "inconclusive", "prose-only absence stays inconclusive");
1327
+ assert.equal(res.promotable, false, "prose never promotes");
1328
+ }
1329
+
1330
+ // --- C: ran ahead of wire-availability, later call succeeded ----------------
1331
+ {
1332
+ const ctl = passingControl();
1333
+ const late = intervention("d1", 2000, 10_000, {
1334
+ wireAt: 2600, // after newSession end (nsLatency 300) → ran ahead
1335
+ nsLatency: 300,
1336
+ fixtureCall: true,
1337
+ fixtureCallAt: 3000,
1338
+ nonceEchoed: true,
1339
+ });
1340
+ const res = classifyProbe([ctl.record, late.record], [...ctl.events, ...late.events]);
1341
+ assert.equal(res.verdict, "C", "ahead-of-wire + late success → C");
1342
+ }
1343
+
1344
+ // --- A: needs TWO distinct delays whose newSession excess tracks D ----------
1345
+ {
1346
+ const ctl = passingControl();
1347
+ // ordering kept: wire fires before newSession end; latency ≈ control + D.
1348
+ const d1 = intervention("d1", 2000, 10_000, {
1349
+ wireAt: 2050,
1350
+ nsLatency: 2300,
1351
+ fixtureCall: true,
1352
+ nonceEchoed: true,
1353
+ });
1354
+ const d2 = intervention("d2", 8000, 40_000, {
1355
+ wireAt: 8050,
1356
+ nsLatency: 8300,
1357
+ fixtureCall: true,
1358
+ nonceEchoed: true,
1359
+ });
1360
+ const one = classifyProbe([ctl.record, d1.record], [...ctl.events, ...d1.events]);
1361
+ assert.equal(one.verdict, "A-withheld", "one nonzero delay → wait verdict WITHHELD [QK:VERDICT-A-NEEDS-TWO-DELAYS]");
1362
+ const two = classifyProbe([ctl.record, d1.record, d2.record], [...ctl.events, ...d1.events, ...d2.events]);
1363
+ assert.equal(two.verdict, "A", "two tracking delays → A");
1364
+
1365
+ // ordering kept but latency does NOT track D → withheld (contradictory data).
1366
+ const flat1 = intervention("f1", 2000, 70_000, { wireAt: 100, nsLatency: 320, fixtureCall: true, nonceEchoed: true });
1367
+ const flat2 = intervention("f2", 8000, 90_000, { wireAt: 100, nsLatency: 340, fixtureCall: true, nonceEchoed: true });
1368
+ const flat = classifyProbe(
1369
+ [ctl.record, flat1.record, flat2.record],
1370
+ [...ctl.events, ...flat1.events, ...flat2.events],
1371
+ );
1372
+ assert.equal(flat.verdict, "A-withheld", "ordering without latency tracking stays withheld");
1373
+
1374
+ // Overshoot: excess grows with D but far EXCEEDS it (10s/16s for 2s/8s). A
1375
+ // floor-only check would call this "tracking"; the band must refuse — an
1376
+ // overshoot is some other stall, not wait-for-delay evidence.
1377
+ const over1 = intervention("o1", 2000, 110_000, {
1378
+ wireAt: 9950,
1379
+ nsLatency: 10_300,
1380
+ fixtureCall: true,
1381
+ nonceEchoed: true,
1382
+ });
1383
+ const over2 = intervention("o2", 8000, 140_000, {
1384
+ wireAt: 15_950,
1385
+ nsLatency: 16_300,
1386
+ fixtureCall: true,
1387
+ nonceEchoed: true,
1388
+ });
1389
+ const over = classifyProbe(
1390
+ [ctl.record, over1.record, over2.record],
1391
+ [...ctl.events, ...over1.events, ...over2.events],
1392
+ );
1393
+ assert.equal(
1394
+ over.verdict,
1395
+ "A-withheld",
1396
+ "excess overshooting the [0.8·D, D+slack] band is not tracking — A withheld [QK:VERDICT-A-TRACKING-BAND]",
1397
+ );
1398
+
1399
+ // In-band per point but NOT growing with D (4.0s → 6.4s excess for 2s → 8s
1400
+ // delays: ΔE=2.4s < 0.6·ΔD=3.6s) → withheld.
1401
+ const nog1 = intervention("g1", 2000, 170_000, {
1402
+ wireAt: 3950,
1403
+ nsLatency: 4_300,
1404
+ fixtureCall: true,
1405
+ nonceEchoed: true,
1406
+ });
1407
+ const nog2 = intervention("g2", 8000, 200_000, {
1408
+ wireAt: 6650,
1409
+ nsLatency: 6_700,
1410
+ fixtureCall: true,
1411
+ nonceEchoed: true,
1412
+ });
1413
+ const nog = classifyProbe([ctl.record, nog1.record, nog2.record], [...ctl.events, ...nog1.events, ...nog2.events]);
1414
+ assert.equal(nog.verdict, "A-withheld", "in-band but non-growing excess is not tracking — A withheld");
1415
+ }
1416
+
1417
+ // --- same-ms cross-process tie is unordered, never ordering evidence --------
1418
+ {
1419
+ // wire marker stamped in the SAME millisecond as newSession end (different
1420
+ // pids): the shared axis cannot order them — reading ordering-kept off the
1421
+ // tie would manufacture A-side evidence out of clock resolution.
1422
+ const ctl = passingControl();
1423
+ const tie = intervention("t1", 2000, 230_000, {
1424
+ wireAt: 2300,
1425
+ nsLatency: 2300,
1426
+ fixtureCall: true,
1427
+ nonceEchoed: true,
1428
+ });
1429
+ const res = classifyProbe([ctl.record, tie.record], [...ctl.events, ...tie.events]);
1430
+ assert.equal(
1431
+ res.verdict,
1432
+ "inconclusive",
1433
+ "a same-ms wire/newSession-end tie is unordered — neither kept nor ahead [QK:VERDICT-SAMEMS-AMBIGUOUS]",
1434
+ );
1435
+
1436
+ // A tie combined with an exact-id No-such-tool must not read B either —
1437
+ // unordered means ranAhead is unestablished, and B requires it.
1438
+ const tieAbsence = intervention("t2", 2000, 260_000, {
1439
+ wireAt: 2300,
1440
+ nsLatency: 2300,
1441
+ fixtureCall: false,
1442
+ noSuchToolId: PROVIDER_ID_MEASURED,
1443
+ nonceEchoed: false,
1444
+ });
1445
+ const res2 = classifyProbe([ctl.record, tieAbsence.record], [...ctl.events, ...tieAbsence.events]);
1446
+ assert.equal(res2.verdict, "inconclusive", "a same-ms tie plus exact-id absence still is not B");
1447
+ assert.equal(res2.promotable, false, "…and never promotes");
1448
+ }
1449
+
1450
+ // --- delay outside the §11-7 window can never read as D ---------------------
1451
+ {
1452
+ const ctl = passingControl();
1453
+ const wide = intervention("d1", DELAY_WELL_BELOW_MS, 10_000, { failPhase: "newSession", wireAt: null });
1454
+ const res = classifyProbe([ctl.record, wide.record], [...ctl.events, ...wide.events]);
1455
+ assert.equal(
1456
+ res.verdict,
1457
+ "inconclusive",
1458
+ `delay ≥ ${DELAY_WELL_BELOW_MS}ms is outside the experiment window — not a D`,
1459
+ );
1460
+ }
1461
+
1462
+ // ===========================================================================
1463
+ // 6) OBSERVATION WINDOW + RUNNER TOPOLOGY — what puts a run OUTSIDE the space
1464
+ // ===========================================================================
1465
+ {
1466
+ const ctl = passingControl();
1467
+
1468
+ // Without the window marker, a missing wire marker cannot be told apart from
1469
+ // our own teardown — so the run is not judged at all rather than judged
1470
+ // permissively. (Re-parsing any artifact written before the window protocol
1471
+ // lands here, which is exactly right: those logs cannot answer the question.)
1472
+ const noWindow = intervention("w0", 2000, 10_000, { wireAt: null, fixtureCall: false, window: "omit" });
1473
+ const resNoWindow = classifyProbe([ctl.record, noWindow.record], [...ctl.events, ...noWindow.events]);
1474
+ // Two independent nets cover this — the exactly-once inventory below AND the
1475
+ // explicit windowReason guard in the classifier — so no SINGLE mutation can
1476
+ // kill it and it carries no [QK:] token. The inventory itself is qualified.
1477
+ assert.ok(
1478
+ resNoWindow.verdict === "INVALIDATED" && resNoWindow.interventions[0].invalidReason === "topology",
1479
+ "a run with no observation-window marker is INVALIDATED for TOPOLOGY — absence cannot be told from our own teardown",
1480
+ );
1481
+ // The exactly-once inventory, as a HAND-WRITTEN literal (never read off the
1482
+ // module under test): dropping a member would retire a topology rule silently.
1483
+ assert.deepEqual(
1484
+ [...RUNNER_EXACTLY_ONCE].sort(),
1485
+ [PROBE_EVENTS.observationWindowEnd, PROBE_EVENTS.runEnd, PROBE_EVENTS.runStart].sort(),
1486
+ "the runner-owned exactly-once marker set is exactly run_start, the window close, and run_end [QK:VERDICT-RUNNER-EXACTLY-ONCE-INVENTORY]",
1487
+ );
1488
+
1489
+ // THE regression the first LIVE pair produced: D2's child was torn down while
1490
+ // the fixture was still inside its injected delay, so the wire marker could
1491
+ // never land — and a wire-marker-less run was filed as an MCP handshake /
1492
+ // fixture / config candidate. That is an attribution about the SERVER derived
1493
+ // from a fact about OUR teardown.
1494
+ const censored = intervention("w1", 8000, 40_000, {
1495
+ wireAt: null,
1496
+ fixtureCall: false,
1497
+ nonceEchoed: false,
1498
+ window: { reason: "child-exit", markerSeen: false },
1499
+ });
1500
+ const resCensored = classifyProbe([ctl.record, censored.record], [...ctl.events, ...censored.events]);
1501
+ assert.equal(
1502
+ resCensored.interventions[0].invalidReason,
1503
+ "observation-window-closed",
1504
+ "a window closed by child-exit with the marker unseen is CENSORED, not a handshake/fixture/config candidate [QK:VERDICT-CENSORED-NOT-CANDIDATE]",
1505
+ );
1506
+ assert.equal(
1507
+ resCensored.interventions[0].ordering,
1508
+ "censored",
1509
+ "…its (a) axis reads censored — a fact about the probe, not an ordering comparison",
1510
+ );
1511
+ assert.notEqual(
1512
+ resCensored.interventions[0].failure,
1513
+ "candidate-handshake",
1514
+ "…and its (b) axis refuses the attribution the first LIVE pair made",
1515
+ );
1516
+ assert.equal(resCensored.verdict, "INVALIDATED", "the pair's only intervention being censored invalidates the pair");
1517
+ // A fatal status still has to NAME what it discarded — `invalidRuns` is a
1518
+ // common field of the status contract, so leaving it empty on the fatal paths
1519
+ // would quietly lose the only record of which runs were thrown out and why.
1520
+ assert.deepEqual(
1521
+ resCensored.status.invalidRuns,
1522
+ [{ runId: "w1", reason: "observation-window-closed" }],
1523
+ "a fatal status still names the discarded run and its reason [QK:VERDICT-STATUS-NAMES-INVALID-RUNS]",
1524
+ );
1525
+
1526
+ // The SAME absence under a window we kept open to its deadline IS a reading:
1527
+ // the difference is entirely whether we looked long enough.
1528
+ const sufficient = intervention("w2", 8000, 70_000, {
1529
+ wireAt: null,
1530
+ fixtureCall: false,
1531
+ nonceEchoed: false,
1532
+ window: { reason: "deadline", markerSeen: false },
1533
+ });
1534
+ const resSufficient = classifyProbe([ctl.record, sufficient.record], [...ctl.events, ...sufficient.events]);
1535
+ assert.equal(
1536
+ resSufficient.interventions[0].failure,
1537
+ "candidate-handshake",
1538
+ "the same absence under a SUFFICIENT window is a handshake/fixture/config candidate (the mutant for this condition is VERDICT-CENSORED-NOT-CANDIDATE — one condition, one kill)",
1539
+ );
1540
+ assert.equal(resSufficient.verdict, "inconclusive", "…still not promotable, and still not B");
1541
+
1542
+ // Runner-owned markers are ours and exactly-once by construction: a duplicate
1543
+ // means the log describing the run is not the run.
1544
+ const dupEnd = intervention("w3", 2000, 100_000, {
1545
+ fixtureCall: true,
1546
+ extra: (runId, base) => [ev(runId, PROBE_EVENTS.runEnd, base + 5_000, { ok: true })],
1547
+ });
1548
+ const resDup = classifyProbe([ctl.record, dupEnd.record], [...ctl.events, ...dupEnd.events]);
1549
+ assert.equal(
1550
+ resDup.verdict,
1551
+ "INVALIDATED",
1552
+ "a duplicated runner-owned marker INVALIDATES the run [QK:VERDICT-RUNNER-TOPOLOGY-EXACTLY-ONCE]",
1553
+ );
1554
+ assert.match(resDup.interventions[0].evidence, /run_end appears 2 times/, "…and the evidence names the duplicate");
1555
+
1556
+ // An end with no start is the same defect seen from the other side.
1557
+ const orphanEnd = intervention("w4", 2000, 130_000, {
1558
+ failPhase: "initialize",
1559
+ extra: (runId, base) => [ev(runId, PROBE_EVENTS.promptEnd, base + 70, { ok: true })],
1560
+ });
1561
+ const resOrphan = classifyProbe([ctl.record, orphanEnd.record], [...ctl.events, ...orphanEnd.events]);
1562
+ assert.equal(
1563
+ resOrphan.verdict,
1564
+ "INVALIDATED",
1565
+ "a phase end with no start INVALIDATES the run — topology precedes I0",
1566
+ );
1567
+ assert.match(resOrphan.interventions[0].evidence, /prompt_end without prompt_start/, "…named exactly");
1568
+
1569
+ // Repeatable markers are NOT swept into the exactly-once rule: the model may
1570
+ // produce several tool-call frames and the client may re-request tools/list.
1571
+ const repeats = intervention("w5", 2000, 160_000, {
1572
+ fixtureCall: true,
1573
+ nsLatency: 2400,
1574
+ wireAt: 2100,
1575
+ extra: (runId, base) => [
1576
+ ev(runId, PROBE_EVENTS.acpToolCallRaw, base + 900, { kind: "tool_call_update", raw: "{}" }),
1577
+ ev(runId, PROBE_EVENTS.acpToolCallRaw, base + 901, { kind: "tool_call_update", raw: "{}" }),
1578
+ ],
1579
+ });
1580
+ const resRepeats = classifyProbe([ctl.record, repeats.record], [...ctl.events, ...repeats.events]);
1581
+ assert.notEqual(
1582
+ resRepeats.verdict,
1583
+ "INVALIDATED",
1584
+ "repeatable forensic markers repeating is not a topology violation (enforced, not mutant-qualified: widening the exactly-once set fails every run at once, so no isolated mutant exists)",
1585
+ );
1586
+ }
1587
+
1588
+ // --- window marker coherence + phase production order -----------------------
1589
+ {
1590
+ const ctl = passingControl();
1591
+
1592
+ // The window marker is SELF-REPORTED. A close claiming the marker was seen,
1593
+ // in a run whose log has no wire marker, would walk a censored run straight
1594
+ // into the candidate branch — so the flag is checked against the log.
1595
+ const lying = intervention("c1", 2000, 10_000, {
1596
+ wireAt: null,
1597
+ fixtureCall: false,
1598
+ nonceEchoed: false,
1599
+ window: { reason: "wire-marker", markerSeen: true },
1600
+ });
1601
+ const resLying = classifyProbe([ctl.record, lying.record], [...ctl.events, ...lying.events]);
1602
+ assert.equal(
1603
+ resLying.verdict,
1604
+ "INVALIDATED",
1605
+ "a window close claiming markerSeen=true with no wire marker in the log is INVALIDATED, never a candidate [QK:VERDICT-WINDOW-MARKER-COHERENCE]",
1606
+ );
1607
+ assert.match(resLying.interventions[0].evidence, /contradicts its own evidence/, "…named as a self-contradiction");
1608
+
1609
+ // The SAME bar applies to the CONTROL. Coherence lives in the run's shared
1610
+ // validity list precisely so the baseline cannot claim a wire marker it never
1611
+ // logged — a control free to lie about its own window is not a baseline, and
1612
+ // every intervention is read as a delta against it.
1613
+ const lyingControl = {
1614
+ record: {
1615
+ runId: "ctl-lie",
1616
+ role: "control",
1617
+ delayMs: 0,
1618
+ probeRunId: "prb-ctl-lie",
1619
+ snapshotInstrumented: false,
1620
+ } as ProbeRunRecord,
1621
+ events: syntheticRun({
1622
+ runId: "ctl-lie",
1623
+ probeRunId: "prb-ctl-lie",
1624
+ base: 500_000,
1625
+ wireAt: null, // no wire marker in the log …
1626
+ fixtureCall: true,
1627
+ providerToolId: PROVIDER_ID_MEASURED,
1628
+ nonceEchoed: true,
1629
+ window: { reason: "wire-marker", markerSeen: true }, // … but the close claims one
1630
+ }),
1631
+ };
1632
+ const d1ok = intervention("d1ok", 2000, 540_000, { wireAt: 2100, nsLatency: 2400, fixtureCall: true });
1633
+ const resLyingCtl = classifyProbe([lyingControl.record, d1ok.record], [...lyingControl.events, ...d1ok.events]);
1634
+ assert.ok(
1635
+ resLyingCtl.verdict === "INVALIDATED" && resLyingCtl.control.pass === false,
1636
+ "a CONTROL whose window close contradicts its own log is INVALIDATED before P0 is even considered — the baseline is held to the same bar [QK:VERDICT-CONTROL-HELD-TO-COHERENCE]",
1637
+ );
1638
+ assert.equal(resLyingCtl.interventions.length, 0, "…and no intervention is judged against a baseline that lied");
1639
+
1640
+ // The reason must agree with the flag too: `deadline` and `child-exit` both
1641
+ // mean the marker did not arrive.
1642
+ const wrongReason = intervention("c2", 2000, 40_000, {
1643
+ wireAt: 2200,
1644
+ fixtureCall: true,
1645
+ window: { reason: "deadline", markerSeen: true },
1646
+ });
1647
+ const resWrong = classifyProbe([ctl.record, wrongReason.record], [...ctl.events, ...wrongReason.events]);
1648
+ assert.equal(resWrong.verdict, "INVALIDATED", "reason=deadline with markerSeen=true is an incoherent close");
1649
+
1650
+ // PHASE-TO-PHASE order, not just start<end inside each phase: a log whose
1651
+ // phases are transposed has every pair intact and still does not describe the
1652
+ // driver's sequence. Both fixtures below are otherwise WELL FORMED — they
1653
+ // differ from a healthy run in exactly one way, so the violation they trip is
1654
+ // unambiguous and their mutants cannot die on someone else's assertion.
1655
+ const transposed: ProbeEvent[] = [
1656
+ ev("x1", PROBE_EVENTS.runStart, 300_000),
1657
+ ev("x1", PROBE_EVENTS.initializeStart, 300_000),
1658
+ ev("x1", PROBE_EVENTS.initializeEnd, 300_050, { ok: true }),
1659
+ // prompt BEFORE newSession — each pair is well formed on its own
1660
+ ev("x1", PROBE_EVENTS.promptStart, 300_100),
1661
+ ev("x1", PROBE_EVENTS.promptEnd, 300_200, { ok: true }),
1662
+ ev("x1", PROBE_EVENTS.promptReply, 300_210, { carriesNonce: false }),
1663
+ ev("x1", PROBE_EVENTS.newSessionStart, 300_300),
1664
+ ev("x1", PROBE_EVENTS.newSessionEnd, 300_400, { ok: true }),
1665
+ ev("x1", PROBE_EVENTS.setModelStart, 300_500),
1666
+ ev("x1", PROBE_EVENTS.setModelEnd, 300_530, { ok: true }),
1667
+ ev("x1", PROBE_EVENTS.observationWindowEnd, 300_600, { reason: "deadline", markerSeen: false }),
1668
+ ev("x1", PROBE_EVENTS.runEnd, 300_601, { ok: true }),
1669
+ ];
1670
+ const transposedRec: ProbeRunRecord = {
1671
+ runId: "x1",
1672
+ role: "intervention",
1673
+ delayMs: 2000,
1674
+ probeRunId: "prb-x1",
1675
+ snapshotInstrumented: false,
1676
+ };
1677
+ const resTrans = classifyProbe([ctl.record, transposedRec], [...ctl.events, ...transposed]);
1678
+ assert.equal(
1679
+ resTrans.verdict,
1680
+ "INVALIDATED",
1681
+ "transposed phases are a topology violation even though every start/end pair is intact [QK:VERDICT-PHASE-SEQUENTIAL]",
1682
+ );
1683
+ assert.match(
1684
+ resTrans.interventions[0].evidence,
1685
+ /phases are sequential/,
1686
+ "…named as a phase-sequencing violation (all four phases are present, so it is the ORDER that is wrong)",
1687
+ );
1688
+
1689
+ // The prefix rule catches the other shape: a phase that ran without the phases
1690
+ // before it. A failed run is a PREFIX of the production order, never a hole.
1691
+ const hole: ProbeEvent[] = [
1692
+ ev("x2", PROBE_EVENTS.runStart, 400_000),
1693
+ ev("x2", PROBE_EVENTS.initializeStart, 400_000),
1694
+ ev("x2", PROBE_EVENTS.initializeEnd, 400_050, { ok: true }),
1695
+ // newSession skipped entirely — enforceModel and prompt still ran
1696
+ ev("x2", PROBE_EVENTS.setModelStart, 400_100),
1697
+ ev("x2", PROBE_EVENTS.setModelEnd, 400_130, { ok: true }),
1698
+ ev("x2", PROBE_EVENTS.promptStart, 400_200),
1699
+ ev("x2", PROBE_EVENTS.promptEnd, 400_300, { ok: true }),
1700
+ ev("x2", PROBE_EVENTS.promptReply, 400_310, { carriesNonce: false }),
1701
+ ev("x2", PROBE_EVENTS.observationWindowEnd, 400_400, { reason: "deadline", markerSeen: false }),
1702
+ ev("x2", PROBE_EVENTS.runEnd, 400_401, { ok: true }),
1703
+ ];
1704
+ const holeRec: ProbeRunRecord = {
1705
+ runId: "x2",
1706
+ role: "intervention",
1707
+ delayMs: 2000,
1708
+ probeRunId: "prb-x2",
1709
+ snapshotInstrumented: false,
1710
+ };
1711
+ const resHole = classifyProbe([ctl.record, holeRec], [...ctl.events, ...hole]);
1712
+ assert.equal(
1713
+ resHole.verdict,
1714
+ "INVALIDATED",
1715
+ "a skipped phase is a topology violation — a failed run is a PREFIX of the production order, never a hole [QK:VERDICT-PHASE-PRODUCTION-ORDER]",
1716
+ );
1717
+ assert.match(resHole.interventions[0].evidence, /prefix of the production order/, "…named as a prefix violation");
1718
+ }
1719
+
1720
+ // ===========================================================================
1721
+ // 7) TWO AXES — an ordering observation is not hidden by a missing (b) marker
1722
+ // ===========================================================================
1723
+ {
1724
+ const ctl = passingControl();
1725
+
1726
+ // The D1 shape, exactly as measured 2026-07-28: the turn is opened BEFORE the
1727
+ // tools reach the wire, the wire marker lands mid-turn, and the model never
1728
+ // attempts the call. (b) has no marker — model silence is not evidence — but
1729
+ // (a) is settled: this server did NOT wait. Reporting one verdict let the
1730
+ // missing (b) marker bury the (a) fact.
1731
+ const d1 = intervention("a1", 2000, 10_000, {
1732
+ nsLatency: 2000,
1733
+ wireAt: 2200, // promptStart is nsStart+2060, promptEnd nsStart+2360
1734
+ fixtureCall: false,
1735
+ nonceEchoed: false,
1736
+ });
1737
+ const res = classifyProbe([ctl.record, d1.record], [...ctl.events, ...d1.events]);
1738
+ const r = res.interventions[0];
1739
+ assert.equal(r.deltas.promptRanAhead, true, "promptStart precedes the wire marker");
1740
+ assert.equal(
1741
+ r.ordering,
1742
+ "prompt-request-ahead-of-wire",
1743
+ "(a) is a settled COMPARISON: we issued the prompt request before the wire marker landed — named for the comparison, never for a server-wait conclusion",
1744
+ );
1745
+ assert.equal(r.failure, "inconclusive", "(b) has no marker — model silence never promotes");
1746
+ assert.equal(res.verdict, "inconclusive", "the composite verdict stays inconclusive because (b) is unsettled");
1747
+ assert.equal(
1748
+ res.ordering.summary,
1749
+ "prompt-request-ahead-of-wire",
1750
+ "…and the (a) axis is reported on its OWN terms rather than being folded into that verdict [QK:VERDICT-ORDERING-AXIS-REPORTED]",
1751
+ );
1752
+ // Diagnosability: §11-7-b's first artifact classified D1 correctly and still
1753
+ // left a reader unable to SEE the ran-ahead or the turn time left after it.
1754
+ assert.match(r.evidence, /promptStart \d+ms BEFORE wire/, "the evidence exposes the prompt↔wire delta");
1755
+ assert.match(
1756
+ r.evidence,
1757
+ /\d+ms of turn remained after wire/,
1758
+ "the evidence exposes how much turn was left after the wire marker [QK:VERDICT-EVIDENCE-EXPOSES-DELTAS]",
1759
+ );
1760
+
1761
+ // The axis split is load-bearing for B/C, not cosmetic. Here the wire lands
1762
+ // AFTER newSession end but BEFORE the prompt is issued: the newSession axis
1763
+ // says "ran ahead", the causal window says the turn was opened against a wire
1764
+ // that was already available. Only the second one may decide B.
1765
+ const between = intervention("a2", 2000, 40_000, {
1766
+ nsLatency: 2000,
1767
+ wireAt: 2030, // newSessionEnd + 30, promptStart is +60
1768
+ fixtureCall: false,
1769
+ noSuchToolId: PROVIDER_ID_MEASURED,
1770
+ nonceEchoed: false,
1771
+ });
1772
+ const resBetween = classifyProbe([ctl.record, between.record], [...ctl.events, ...between.events]);
1773
+ const rb = resBetween.interventions[0];
1774
+ assert.equal(rb.deltas.newSessionRanAhead, true, "the newSession axis alone would call this ran-ahead");
1775
+ assert.equal(rb.deltas.promptRanAhead, false, "…but the turn was opened AFTER the wire was available");
1776
+ assert.equal(
1777
+ resBetween.verdict,
1778
+ "inconclusive",
1779
+ "exact-id absence with the wire available before the prompt is NOT delta-B — B's window is promptStart, not newSession end [QK:VERDICT-B-WINDOW-IS-PROMPT]",
1780
+ );
1781
+ assert.equal(
1782
+ rb.ordering,
1783
+ "wire-before-prompt-request",
1784
+ "…and (a) records the comparison: the wire marker landed before we issued the prompt request",
1785
+ );
1786
+
1787
+ // A keeps its own axis: `wire < newSessionEnd` plus latency scaling. Replacing
1788
+ // the single ran-ahead flag with the prompt axis would have silently broken it.
1789
+ const k1 = intervention("k1", 2000, 70_000, { wireAt: 100, nsLatency: 2200, fixtureCall: true });
1790
+ const k2 = intervention("k2", 8000, 100_000, { wireAt: 100, nsLatency: 8200, fixtureCall: true });
1791
+ const resA = classifyProbe([ctl.record, k1.record, k2.record], [...ctl.events, ...k1.events, ...k2.events]);
1792
+ assert.equal(resA.verdict, "A", "A still reads off the newSession axis with latency tracking");
1793
+ assert.equal(
1794
+ resA.ordering.summary,
1795
+ "wire-before-newSession-end",
1796
+ "…and the (a) summary is named for the comparison, deliberately NOT 'wait' — the wait verdict needs the scaling A adds",
1797
+ );
1798
+ }
1799
+
1800
+ // ===========================================================================
1801
+ // 8) §11-7-c B-name-snapshot seam — CONSUMER side (preconditions, doors,
1802
+ // verdict ladder). The PRODUCER (the CLI shim) is gated by
1803
+ // check-probe-cli-shim; these prove the contract it must satisfy, and 8d
1804
+ // pins the runner's arming — the order of the ambient refusal against the
1805
+ // deliberate injection, what is injected, and the instrument's runtime graph.
1806
+ // ===========================================================================
1807
+
1808
+ // --- 8a) CLI-target precondition seam: refusals are NAMED, never fallbacks --
1809
+ {
1810
+ const fakeBin = join(tmp, "fake-claude");
1811
+ writeFileSync(fakeBin, "#!/bin/sh\nexit 0\n");
1812
+ chmodSync(fakeBin, 0o755);
1813
+ const fakeScript = join(tmp, "fake-claude.mjs");
1814
+ writeFileSync(fakeScript, "process.exit(0);\n");
1815
+ chmodSync(fakeScript, 0o755);
1816
+ const fakeDir = join(tmp, "fake-claude-dir");
1817
+ mkdirSync(fakeDir, { recursive: true });
1818
+ const fakeNonExec = join(tmp, "fake-claude-noexec");
1819
+ writeFileSync(fakeNonExec, "#!/bin/sh\nexit 0\n");
1820
+ chmodSync(fakeNonExec, 0o644);
1821
+
1822
+ const reasonOf = async (env: Record<string, string | undefined>, target: string): Promise<string> => {
1823
+ try {
1824
+ await resolveProbeCliTarget({ env, resolveNative: async () => target });
1825
+ return "resolved";
1826
+ } catch (err) {
1827
+ return err instanceof ProbeCliPreconditionError ? err.reason : "unexpected-error";
1828
+ }
1829
+ };
1830
+
1831
+ assert.equal(
1832
+ await reasonOf({ [AMBIENT_OVERRIDE_ENV]: "/somewhere/claude" }, fakeBin),
1833
+ "ambient-override-present",
1834
+ "an ambient CLAUDE_CODE_EXECUTABLE is REFUSED before resolution — claudeCliPath() would return it verbatim [QK:PROBE-TARGET-AMBIENT-REFUSED]",
1835
+ );
1836
+ // KEY PRESENCE is the predicate: upstream's `??` treats "" as set and passes
1837
+ // it on while a truthy check treats it as unset — the probe refuses the
1838
+ // ambiguity instead of picking a side (GPT review 2026-07-29).
1839
+ assert.equal(
1840
+ await reasonOf({ [AMBIENT_OVERRIDE_ENV]: "" }, fakeBin),
1841
+ "ambient-override-present",
1842
+ "an EMPTY-string override is still a present key — refused, not treated as unset",
1843
+ );
1844
+ assert.equal(
1845
+ await reasonOf({}, "relative/claude"),
1846
+ "target-not-absolute",
1847
+ "a non-absolute resolved target is refused — it would resolve against the session cwd at spawn time",
1848
+ );
1849
+ assert.equal(
1850
+ await reasonOf({}, fakeScript),
1851
+ "target-script-suffix",
1852
+ "a script-suffixed target is refused — the SDK would take the node|bun branch, which this seam asserts against instead of reproducing [QK:PROBE-TARGET-NATIVE-BRANCH-ONLY]",
1853
+ );
1854
+ assert.equal(
1855
+ await reasonOf({}, join(tmp, "no-such-claude")),
1856
+ "target-missing",
1857
+ "a missing target is refused before a LIVE turn spends money on a spawn error",
1858
+ );
1859
+ assert.equal(
1860
+ await reasonOf({}, fakeDir),
1861
+ "target-not-regular-file",
1862
+ "a directory target is refused — existsSync alone would have taken it happy (GPT review 2026-07-29)",
1863
+ );
1864
+ assert.equal(
1865
+ await reasonOf({}, fakeNonExec),
1866
+ "target-not-executable",
1867
+ "a non-executable regular file is refused (X_OK) — it would fail only after the pair started spending",
1868
+ );
1869
+ const resolved = await resolveProbeCliTarget({ env: {}, resolveNative: async () => fakeBin });
1870
+ assert.ok(
1871
+ resolved.path === fakeBin && resolved.sha256 === hashFileSha256(fakeBin) && /^[0-9a-f]{64}$/.test(resolved.sha256),
1872
+ "an absolute, extensionless, executable regular file resolves with its content hash",
1873
+ );
1874
+ }
1875
+
1876
+ // --- 8b) upstream override semantics — inspector validated on synthetic
1877
+ // fixtures, THEN applied to the installed dists. node_modules can never be
1878
+ // a mutant subject (§11-7-c), so kill-power here is the synthetic
1879
+ // negatives, not a manifest entry.
1880
+ {
1881
+ const inspect = (acpSrc: string, sdkSrc: string): string[] => {
1882
+ const violations: string[] = [];
1883
+ if (!acpSrc.includes("export async function claudeCliPath()")) {
1884
+ violations.push("claudeCliPath export missing from acp-agent.js");
1885
+ }
1886
+ if (!/if \(process\.env\.CLAUDE_CODE_EXECUTABLE\) \{\s*return process\.env\.CLAUDE_CODE_EXECUTABLE;/.test(acpSrc)) {
1887
+ violations.push(
1888
+ "claudeCliPath no longer returns the ambient override VERBATIM — the refusal precondition's premise moved",
1889
+ );
1890
+ }
1891
+ const overrideLine = "pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath())";
1892
+ const iOverride = acpSrc.indexOf(overrideLine);
1893
+ if (iOverride === -1) {
1894
+ violations.push("the env-??-claudeCliPath resolution at the query options literal is gone");
1895
+ } else {
1896
+ const iSpread = acpSrc.indexOf("...userProvidedOptions,");
1897
+ if (iSpread === -1 || iSpread > iOverride) {
1898
+ violations.push(
1899
+ "the explicit pathToClaudeCodeExecutable key no longer follows ...userProvidedOptions — the key-order assumption (override wins) is broken",
1900
+ );
1901
+ }
1902
+ }
1903
+ if (!sdkSrc.includes(JSON.stringify(SDK_SCRIPT_SUFFIXES))) {
1904
+ violations.push(
1905
+ `the SDK's script-suffix discriminator no longer equals ${JSON.stringify(SDK_SCRIPT_SUFFIXES)} — the native-branch assert would drift from upstream`,
1906
+ );
1907
+ }
1908
+ if (!sdkSrc.includes('?"bun":"node"')) {
1909
+ violations.push("the SDK's node|bun default-executable choice moved — the script branch premise changed");
1910
+ }
1911
+ // The no-shell proof is LOCALIZED to the spawn leaf: asserting only that
1912
+ // one spelling of `shell:!0` is absent SOMEWHERE in a megabyte of minified
1913
+ // source proves nothing — `shell:true`, a variable shell, or the leaf
1914
+ // moving entirely would all pass (GPT review 2026-07-29). Pin the
1915
+ // spawnLocalProcess definition window and judge the spawn options THERE.
1916
+ const iLeaf = sdkSrc.indexOf("spawnLocalProcess(");
1917
+ if (iLeaf === -1) {
1918
+ violations.push("the SDK's spawnLocalProcess leaf is gone — the spawn-shape premise has no anchor");
1919
+ } else {
1920
+ const leaf = sdkSrc.slice(iLeaf, iLeaf + 800);
1921
+ if (!leaf.includes('stdio:["pipe","pipe","pipe"]') || !leaf.includes("windowsHide:!0")) {
1922
+ violations.push("the spawn leaf no longer shows piped stdio + windowsHide — spawn semantics premise changed");
1923
+ }
1924
+ if (/\bshell\s*:/.test(leaf)) {
1925
+ violations.push("the spawn leaf carries a `shell:` option — the no-shell premise is broken");
1926
+ }
1927
+ }
1928
+ return violations;
1929
+ };
1930
+
1931
+ // Synthetic fixtures FIRST — an inspector that cannot see the defect it
1932
+ // exists for is no inspector.
1933
+ const goodAcp =
1934
+ "export async function claudeCliPath() {\n" +
1935
+ " if (process.env.CLAUDE_CODE_EXECUTABLE) {\n return process.env.CLAUDE_CODE_EXECUTABLE;\n }\n}\n" +
1936
+ "const options = {\n ...userProvidedOptions,\n" +
1937
+ " pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n};\n";
1938
+ const goodSdk = `x=![".js",".mjs",".tsx",".ts",".jsx"].some((r)=>e.endsWith(r));y=Cs()?"bun":"node";spawnLocalProcess(e){let{command:t,args:r,cwd:n,env:o,signal:i}=e,s=cxe(t,r,{cwd:n,stdio:["pipe","pipe","pipe"],signal:i,env:o,windowsHide:!0})}`;
1939
+ assert.deepEqual(inspect(goodAcp, goodSdk), [], "inspector passes the correct synthetic fixture");
1940
+ const invertedAcp = goodAcp.replace(
1941
+ " ...userProvidedOptions,\n pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n",
1942
+ " pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n ...userProvidedOptions,\n",
1943
+ );
1944
+ assert.ok(
1945
+ inspect(invertedAcp, goodSdk).some((v) => v.includes("key-order")),
1946
+ "inspector catches userProvidedOptions inverted to win over the explicit override key",
1947
+ );
1948
+ assert.ok(
1949
+ inspect(goodAcp.replace("export async function claudeCliPath()", "async function claudeCliPath()"), goodSdk).some(
1950
+ (v) => v.includes("export missing"),
1951
+ ),
1952
+ "inspector catches the claudeCliPath export disappearing",
1953
+ );
1954
+ assert.ok(
1955
+ inspect(goodAcp, goodSdk.replace('".mjs",', "")).some((v) => v.includes("discriminator")),
1956
+ "inspector catches the SDK suffix list drifting from the pinned SDK_SCRIPT_SUFFIXES",
1957
+ );
1958
+ // The no-shell negatives must be caught IN THE LEAF WINDOW, in both the
1959
+ // minified and the plain spelling — the global one-spelling absence check
1960
+ // this replaced passed them all.
1961
+ assert.ok(
1962
+ inspect(goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:!0")).some((v) =>
1963
+ v.includes("no-shell"),
1964
+ ) &&
1965
+ inspect(goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:true")).some((v) =>
1966
+ v.includes("no-shell"),
1967
+ ) &&
1968
+ inspect(goodAcp, goodSdk.replace("spawnLocalProcess(", "spawnElsewhere(")).some((v) => v.includes("anchor")),
1969
+ "inspector catches shell:!0 AND shell:true inside the spawn leaf, and the leaf disappearing entirely",
1970
+ );
1971
+
1972
+ // Installed dists — the actual §11-7-c load-bearing assumptions.
1973
+ const rootRequire = createRequire(resolve(REPO_ROOT, "package.json"));
1974
+ const acpPkgJson = rootRequire.resolve("@agentclientprotocol/claude-agent-acp/package.json");
1975
+ const acpDist = readFileSync(join(dirname(acpPkgJson), "dist", "acp-agent.js"), "utf8");
1976
+ const acpRequire = createRequire(acpPkgJson);
1977
+ const sdkEntry = acpRequire.resolve("@anthropic-ai/claude-agent-sdk");
1978
+ let sdkDist = readFileSync(sdkEntry, "utf8");
1979
+ if (!sdkDist.includes(JSON.stringify(SDK_SCRIPT_SUFFIXES))) {
1980
+ const sibling = join(dirname(sdkEntry), "sdk.mjs");
1981
+ if (existsSync(sibling)) sdkDist = readFileSync(sibling, "utf8");
1982
+ }
1983
+ assert.deepEqual(
1984
+ inspect(acpDist, sdkDist),
1985
+ [],
1986
+ "installed acp-agent.js + sdk.mjs still carry every §11-7-c launch-semantics assumption (claudeCliPath verbatim env, override key order, suffix discriminator, node|bun choice, piped no-shell spawn)",
1987
+ );
1988
+ }
1989
+
1990
+ // --- 8c) the scrub list is an EXACT allowlist, never a prefix pattern -------
1991
+ {
1992
+ assert.ok(
1993
+ SHIM_SCRUB_ENV_VARS.includes(AMBIENT_OVERRIDE_ENV) &&
1994
+ Object.values(PROBE_SHIM_ENV).every((v) => SHIM_SCRUB_ENV_VARS.includes(v)) &&
1995
+ SHIM_SCRUB_ENV_VARS.length === 1 + Object.values(PROBE_SHIM_ENV).length &&
1996
+ SHIM_SCRUB_ENV_VARS.every((v) => /^[A-Z][A-Z0-9_]*$/.test(v)),
1997
+ "the shim scrub list is the exact enumerated allowlist — the override plus every probe-private var by literal name, no wildcard/prefix semantics [QK:PROBE-SCRUB-EXACT-ALLOWLIST]",
1998
+ );
1999
+ // The shim env names must not collide with the fixture's — two processes,
2000
+ // two channels, one shared log.
2001
+ const fixtureVals = Object.values(PROBE_ENV) as string[];
2002
+ assert.ok(
2003
+ Object.values(PROBE_SHIM_ENV).every((v) => !fixtureVals.includes(v)),
2004
+ "shim env names are disjoint from the fixture's PROBE_ENV names",
2005
+ );
2006
+ }
2007
+
2008
+ // --- 8d) runner pins — one assert per claim, so each [QK:] names exactly what
2009
+ // its mutant kills (bundling four claims under one token let one mutant
2010
+ // stand in for all of them; GPT review 2026-07-29) ----------------------
2011
+ {
2012
+ assert.ok(
2013
+ RUNNER_SRC.includes("assertNoAmbientOverride(spawnEnv, `composed acp child env for ${runId}`);"),
2014
+ "the COMPOSED spawn env of every ACP child is asserted override-free — launch defaults / overlay overrides could inject what process.env did not carry [QK:RUNNER-TARGET-PRECONDITION-PINNED]",
2015
+ );
2016
+ assert.ok(
2017
+ RUNNER_SRC.includes("snapshotInstrumented: true") && !RUNNER_SRC.includes("snapshotInstrumented: false"),
2018
+ "the snapshot channel is ARMED and no run is left declaring otherwise — a roster mixing armed and unarmed runs " +
2019
+ "would let a run whose shim never reported in pass as an ordinary absence [QK:RUNNER-SNAPSHOT-CHANNEL-ARMED]",
2020
+ );
2021
+ // Pinned as the CONTIGUOUS roster-record shape: the same two stamps also ride
2022
+ // the run_start payload (forensics), so field-by-field includes() would stay
2023
+ // green with the roster copy deleted.
2024
+ assert.ok(
2025
+ RUNNER_SRC.includes(
2026
+ "snapshotInstrumented: true,\n\t\tcliTargetPath: CLI_TARGET.path,\n\t\tcliTargetSha256: CLI_TARGET.sha256,",
2027
+ ),
2028
+ "the pair's expected CLI target identity rides EVERY roster record so the classifier can consume it (condition 5) [QK:RUNNER-TARGET-IDENTITY-IN-ROSTER]",
2029
+ );
2030
+ assert.ok(
2031
+ RUNNER_SRC.includes("rehash = hashFileSha256(CLI_TARGET.path);") &&
2032
+ RUNNER_SRC.includes('reason: "cli-target-drift"') &&
2033
+ RUNNER_SRC.includes('reason: "cli-target-unreadable"'),
2034
+ "the target is RE-HASHED after the pair, and both drift and unreadability write a named INVALIDATED classification [QK:RUNNER-DRIFT-REHASH-PINNED]",
2035
+ );
2036
+ assert.ok(
2037
+ RUNNER_SRC.includes("CLI_TARGET = await resolveProbeCliTarget({") &&
2038
+ RUNNER_SRC.includes('"@agentclientprotocol/claude-agent-acp/dist/acp-agent.js"') &&
2039
+ RUNNER_SRC.includes("`precondition-${err.reason}`"),
2040
+ "the runner resolves the target through upstream claudeCliPath BEFORE any run, and a precondition refusal writes a NAMED classification on the artifact (not stderr alone)",
2041
+ );
2042
+ // --- arming (§11-7-c CP3). The producer exists, so the channel is armed — and
2043
+ // arming is the point where two things can go quietly wrong: the order of
2044
+ // the checkpoint against the injection, and WHAT gets injected.
2045
+ assert.ok(
2046
+ RUNNER_SRC.includes(
2047
+ "assertNoAmbientOverride(spawnEnv, `composed acp child env for ${runId}`);\n" + "\t\t// ORDER IS THE CONTRACT",
2048
+ ) &&
2049
+ RUNNER_SRC.indexOf("assertNoAmbientOverride(spawnEnv") <
2050
+ RUNNER_SRC.indexOf("spawnEnv[AMBIENT_OVERRIDE_ENV] = SHIM_TARGET.path;"),
2051
+ "the ambient-override refusal runs against the env as PRODUCTION composed it, and the probe installs its own " +
2052
+ "override only AFTER. Inverted, the checkpoint would inspect the override the probe itself just injected and " +
2053
+ "REFUSE every run — loudly, but for the wrong reason, and the operator's ambient environment would never be " +
2054
+ "examined at all [QK:RUNNER-ARMING-ORDER]",
2055
+ );
2056
+ assert.ok(
2057
+ RUNNER_SRC.includes(
2058
+ "spawnEnv[AMBIENT_OVERRIDE_ENV] = SHIM_TARGET.path;\n" +
2059
+ "\t\tspawnEnv[PROBE_SHIM_ENV.target] = CLI_TARGET.path;\n" +
2060
+ "\t\tspawnEnv[PROBE_SHIM_ENV.eventLog] = logPath;\n" +
2061
+ "\t\tspawnEnv[PROBE_SHIM_ENV.runId] = runId;",
2062
+ ),
2063
+ "the injection is exactly four names: the override pointing at the SHIM, and the three probe-private vars the " +
2064
+ "shim reads — the target it must exec (resolved HERE, never by the shim), the shared log, and this run's id. " +
2065
+ "All four are on the shim's scrub list, so none of them reach the real CLI [QK:RUNNER-SHIM-OVERRIDE-EXACT]",
2066
+ );
2067
+ assert.ok(
2068
+ RUNNER_SRC.includes(
2069
+ "SHIM_TARGET = await resolveProbeCliTarget({ env: {}, resolveNative: async () => PROBE_SHIM });",
2070
+ ) && RUNNER_SRC.includes("`precondition-shim-${err.reason}`"),
2071
+ "the instrument passes the SAME precondition asserts as the stimulus — absolute, native branch, present regular " +
2072
+ "file, executable — and a refusal is a NAMED classification on the artifact. A shim that fails any of those " +
2073
+ "either never runs or runs on the OTHER launch branch, and the pair would measure something else " +
2074
+ "[QK:RUNNER-SHIM-PRECONDITION-PINNED]",
2075
+ );
2076
+ // The path the runner points at is checked on DISK too, not just in source: a
2077
+ // pin proves the runner asks for the right file, not that the file can run.
2078
+ {
2079
+ // --- the instrument is a GRAPH. The launcher is a two-line delegate, so
2080
+ // "control and interventions shared one shim" is a claim about every local
2081
+ // module a fresh Node process reads — and the boot marker cannot see any of
2082
+ // it, because it reports the CLI target rather than the instrument.
2083
+ assert.ok(
2084
+ RUNNER_SRC.includes("SHIM_RUNTIME = hashShimRuntime();") &&
2085
+ RUNNER_SRC.includes("shimRehash = hashShimRuntime();") &&
2086
+ RUNNER_SRC.includes('reason: "shim-runtime-drift"') &&
2087
+ RUNNER_SRC.includes('reason: "shim-runtime-unreadable"') &&
2088
+ RUNNER_SRC.includes("JSON.stringify(shimRehash) !== JSON.stringify(SHIM_RUNTIME)"),
2089
+ "the instrument's runtime graph is pinned before the first run and RE-HASHED after the last, on its own axis " +
2090
+ "with its own two names — an edit to the implementation landing between control and intervention is " +
2091
+ "invisible to every other check, including the shim's own boot marker [QK:RUNNER-SHIM-RUNTIME-PINNED]",
2092
+ );
2093
+ {
2094
+ // The runner's list must equal the STATIC LOCAL IMPORT CLOSURE of the
2095
+ // launcher — derived here, never restated, or the list becomes a second
2096
+ // unverified copy that drifts the moment the implementation grows a helper.
2097
+ // node: builtins and package specifiers are out of scope (they are not
2098
+ // tracked files of ours); a DYNAMIC import inside the closure is refused
2099
+ // outright, because a graph that assembles itself at runtime cannot be
2100
+ // pinned at all.
2101
+ const localImportsOf = (file: string): string[] => {
2102
+ const src = readFileSync(file, "utf8");
2103
+ assert.ok(
2104
+ !/\bimport\s*\(/.test(src),
2105
+ `${file} carries a DYNAMIC import — the shim runtime graph would no longer be statically knowable, so it ` +
2106
+ "could not be pinned across a pair",
2107
+ );
2108
+ return [...src.matchAll(/^\s*import\s[^;]*?from\s+"(\.[^"]+)"|^\s*import\s+"(\.[^"]+)"/gm)]
2109
+ .map((m) => m[1] ?? m[2])
2110
+ .map((spec) => resolve(dirname(file), spec));
2111
+ };
2112
+ const launcher = join(REPO_ROOT, "scripts", "fixtures", "probe-cli-shim");
2113
+ const closure: string[] = [];
2114
+ const walk = (file: string): void => {
2115
+ if (closure.includes(file)) return;
2116
+ closure.push(file);
2117
+ for (const next of localImportsOf(file)) walk(next);
2118
+ };
2119
+ walk(launcher);
2120
+ const declared = [
2121
+ ...RUNNER_SRC.matchAll(/^\t(?:PROBE_SHIM,|join\(REPO_ROOT, "scripts", "lib", "([^"]+)"\),)$/gm),
2122
+ ].map((m) => (m[1] === undefined ? launcher : join(REPO_ROOT, "scripts", "lib", m[1])));
2123
+ assert.deepEqual(
2124
+ [...declared].sort(),
2125
+ [...closure].sort(),
2126
+ "the runner's pinned instrument list is EXACTLY the launcher's static local-import closure — a helper added " +
2127
+ "to the shim without being pinned would otherwise be free to change mid-pair, and a stale entry would " +
2128
+ "pin a file the instrument no longer reads [QK:RUNNER-SHIM-RUNTIME-GRAPH-EXACT]",
2129
+ );
2130
+ }
2131
+ // The path is read OUT OF THE RUNNER rather than restated here, so this
2132
+ // checks the file the runner actually arms. Restating it would pass happily
2133
+ // while the runner pointed somewhere else.
2134
+ const declared = /const PROBE_SHIM = join\(REPO_ROOT, "scripts", "fixtures", "([^"]+)"\);/.exec(RUNNER_SRC);
2135
+ const shimPath = declared ? join(REPO_ROOT, "scripts", "fixtures", declared[1]) : "";
2136
+ assert.ok(
2137
+ declared !== null &&
2138
+ existsSync(shimPath) &&
2139
+ statSync(shimPath).isFile() &&
2140
+ (statSync(shimPath).mode & 0o111) !== 0 &&
2141
+ SDK_SCRIPT_SUFFIXES.every((suffix) => !shimPath.endsWith(suffix)),
2142
+ "the shim the runner arms is present, executable and extensionless ON DISK — the pair is asserted onto the " +
2143
+ "direct-spawn branch, and a script suffix (or a path pointing at nothing) would silently move the " +
2144
+ "instrument to `node|bun <path>` or break the spawn outright [QK:RUNNER-SHIM-ON-DISK-NATIVE]",
2145
+ );
2146
+ }
2147
+ }
2148
+
2149
+ // --- 8e) shim events at the log door: judged payload is typed there ---------
2150
+ {
2151
+ const stampAt = (seq: number, tsMs: number) => ({ seq, pid: 9, ts: new Date(tsMs).toISOString(), tsMs, runId: "sh" });
2152
+ const doorPath = join(tmp, "payload-shim.ndjson");
2153
+ const lines = [
2154
+ { ...stampAt(0, 3_000), event: PROBE_EVENTS.shimBoot, targetPath: "/x/claude" }, // sha missing
2155
+ { ...stampAt(1, 3_001), event: PROBE_EVENTS.shimPromptForwarded, ordinal: 0 }, // ordinal < 1
2156
+ { ...stampAt(2, 3_002), event: PROBE_EVENTS.shimInitSnapshot, tools: "nope", receivedAtMs: 1 },
2157
+ // interval inverted — receivedAtMs AFTER the event's own envelope stamp
2158
+ // (the downstream-write-callback moment, the interval's single-SSOT end)
2159
+ // would silently un-order the §11-7-c "after the wire" read → refused.
2160
+ { ...stampAt(3, 3_003), event: PROBE_EVENTS.shimInitSnapshot, tools: ["a"], receivedAtMs: 3_500 },
2161
+ { ...stampAt(4, 3_004), event: PROBE_EVENTS.shimBoot, targetPath: "/x/claude", targetSha256: "ab12" },
2162
+ { ...stampAt(5, 3_005), event: PROBE_EVENTS.shimPromptForwarded, ordinal: 1 },
2163
+ { ...stampAt(6, 3_006), event: PROBE_EVENTS.shimInitSnapshot, tools: ["a", "b"], receivedAtMs: 3_002 },
2164
+ ];
2165
+ for (const l of lines) appendFileSync(doorPath, `${JSON.stringify(l)}\n`, "utf8");
2166
+ const door = readProbeEvents(doorPath);
2167
+ assert.ok(
2168
+ door.malformed.length === 4 && door.events.length === 3,
2169
+ "shim payload rules hold at the door: missing target hash, ordinal<1, non-array tools, and receivedAtMs AFTER the envelope stamp are MALFORMED; the well-formed trio passes [QK:PROBE-LOG-SNAPSHOT-PAYLOAD]",
2170
+ );
2171
+ }
2172
+
2173
+ // --- 8f) the B-name-snapshot verdict ladder over synthetic paired logs ------
2174
+ {
2175
+ let shimSeq = 5_000;
2176
+ const shimEv = (runId: string, event: string, tsMs: number, payload: Record<string, unknown>): ProbeEvent =>
2177
+ ({ seq: shimSeq++, pid: 9, ts: new Date(tsMs).toISOString(), tsMs, runId, event, ...payload }) as ProbeEvent;
2178
+
2179
+ const EXPECTED_TARGET = { path: "/x/claude", sha256: "ab" } as const;
2180
+
2181
+ /** Shim channel for one run: boot → prompt frame → snapshot(s). The interval
2182
+ * END is the snapshot event's own envelope tsMs (`at`), the moment the shim
2183
+ * appends inside the downstream write callback; payload carries only
2184
+ * `receivedAtMs`. */
2185
+ const shimChannel = (
2186
+ runId: string,
2187
+ base: number,
2188
+ snapshots: Array<{ receivedAtMs: number; at: number; tools: string[] }>,
2189
+ opts: { boot?: boolean; promptForwardedAt?: number; bootTargetSha256?: string } = {},
2190
+ ): ProbeEvent[] => {
2191
+ const out: ProbeEvent[] = [];
2192
+ if (opts.boot !== false) {
2193
+ out.push(
2194
+ shimEv(runId, PROBE_EVENTS.shimBoot, base + 150, {
2195
+ targetPath: EXPECTED_TARGET.path,
2196
+ targetSha256: opts.bootTargetSha256 ?? EXPECTED_TARGET.sha256,
2197
+ }),
2198
+ );
2199
+ }
2200
+ out.push(shimEv(runId, PROBE_EVENTS.shimPromptForwarded, opts.promptForwardedAt ?? base + 1_700, { ordinal: 1 }));
2201
+ for (const s of snapshots) {
2202
+ out.push(shimEv(runId, PROBE_EVENTS.shimInitSnapshot, s.at, { tools: s.tools, receivedAtMs: s.receivedAtMs }));
2203
+ }
2204
+ return out;
2205
+ };
2206
+
2207
+ // An armed roster record also CARRIES the expected target identity — the
2208
+ // classifier consumes it (condition 5), fail-closed when absent.
2209
+ const armed = (r: { record: ProbeRunRecord; events: ProbeEvent[] }, shim: ProbeEvent[]) => ({
2210
+ record: {
2211
+ ...r.record,
2212
+ snapshotInstrumented: true,
2213
+ cliTargetPath: EXPECTED_TARGET.path,
2214
+ cliTargetSha256: EXPECTED_TARGET.sha256,
2215
+ },
2216
+ events: [...r.events, ...shim],
2217
+ });
2218
+
2219
+ // A calibrated, armed control: channel clean and the snapshot CONTAINS the
2220
+ // measured id (fixture call + nonce echo already hold in passingControl).
2221
+ const armedControl = (base = 0) =>
2222
+ armed(
2223
+ passingControl(base),
2224
+ shimChannel("ctl", base, [{ receivedAtMs: base + 600, at: base + 605, tools: [PROVIDER_ID_MEASURED, "other"] }], {
2225
+ promptForwardedAt: base + 500,
2226
+ }),
2227
+ );
2228
+
2229
+ // Intervention timing shape (base b): nsLatency 1500 → promptStart = b+1660;
2230
+ // wireAt 2000 → wire = b+2100 > promptStart (promptRanAhead).
2231
+ const ranAheadSpec = { wireAt: 2_000, nsLatency: 1_500, fixtureCall: false } as const;
2232
+
2233
+ // (1) full floor → B-name-snapshot, promotable.
2234
+ {
2235
+ const ctl = armedControl();
2236
+ const i1 = armed(
2237
+ intervention("d1", 2_000, 10_000, { ...ranAheadSpec }),
2238
+ shimChannel("d1", 10_000, [{ receivedAtMs: 12_190, at: 12_195, tools: ["unrelated_tool"] }]),
2239
+ );
2240
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2241
+ assert.equal(
2242
+ res.verdict,
2243
+ "B-name-snapshot",
2244
+ "snapshot absence of the measured id + promptRanAhead + wire strictly before the interval + calibrated control → B-name-snapshot [QK:VERDICT-SNAPSHOT-PROMOTES]",
2245
+ );
2246
+ assert.ok(res.promotable && res.status.failureVerdict === "B-name-snapshot", "…and it is promotable on axis (b)");
2247
+ assert.notEqual(
2248
+ res.interventions[0].failure,
2249
+ "B",
2250
+ "…and it NEVER upgrades into runtime B — the report is not the failure",
2251
+ );
2252
+ }
2253
+
2254
+ // (2) armed control with NO shim events at all → named instrument absence.
2255
+ {
2256
+ const ctl = armed(passingControl(20_000), []);
2257
+ const i1 = armed(
2258
+ intervention("d1", 2_000, 30_000, { ...ranAheadSpec }),
2259
+ shimChannel("d1", 30_000, [{ receivedAtMs: 32_190, at: 32_195, tools: ["unrelated_tool"] }]),
2260
+ );
2261
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2262
+ assert.equal(res.verdict, "P0", "an armed control whose shim never reported in cannot calibrate — P0");
2263
+ assert.equal(
2264
+ res.control.p0Reason,
2265
+ "snapshot-instrument-absent",
2266
+ "…and the reason NAMES the missing instrument (a hijacked/replaced override looks exactly like this) [QK:VERDICT-SNAPSHOT-INSTRUMENT-ABSENT]",
2267
+ );
2268
+ }
2269
+
2270
+ // (3) armed control, channel clean, but the snapshot LACKS the measured id.
2271
+ {
2272
+ const ctl = armed(
2273
+ passingControl(40_000),
2274
+ shimChannel("ctl", 40_000, [{ receivedAtMs: 40_600, at: 40_605, tools: ["only_this"] }], {
2275
+ promptForwardedAt: 40_500,
2276
+ }),
2277
+ );
2278
+ const i1 = armed(
2279
+ intervention("d1", 2_000, 50_000, { ...ranAheadSpec }),
2280
+ shimChannel("d1", 50_000, [{ receivedAtMs: 52_190, at: 52_195, tools: ["unrelated_tool"] }]),
2281
+ );
2282
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2283
+ assert.ok(
2284
+ res.verdict === "P0" && res.control.p0Reason === "snapshot-calibration",
2285
+ "a control snapshot that cannot SEE the measured id fails calibration — absence readings need a baseline that shows presence [QK:VERDICT-SNAPSHOT-CALIBRATION]",
2286
+ );
2287
+ }
2288
+
2289
+ // (4) absence + wire before the interval, but the prompt did NOT run ahead.
2290
+ {
2291
+ const ctl = armedControl(60_000);
2292
+ // wireAt 200 → wire = 70_300, promptStart = 71_660 → NOT promptRanAhead;
2293
+ // snapshot received at 72_000 (after the wire) with the id absent.
2294
+ const i1 = armed(
2295
+ intervention("d1", 2_000, 70_000, { wireAt: 200, nsLatency: 1_500, fixtureCall: false }),
2296
+ shimChannel("d1", 70_000, [{ receivedAtMs: 72_000, at: 72_005, tools: ["unrelated_tool"] }]),
2297
+ );
2298
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2299
+ assert.ok(
2300
+ res.verdict === "inconclusive" && !res.promotable,
2301
+ "snapshot absence WITHOUT promptRanAhead is not the delayed-window failure mode — never promoted [QK:VERDICT-SNAPSHOT-REQUIRES-RANAHEAD]",
2302
+ );
2303
+ }
2304
+
2305
+ // (5) the wire marker lands INSIDE the snapshot interval → unordered.
2306
+ {
2307
+ const ctl = armedControl(80_000);
2308
+ // wire = 92_100; interval [92_050, 92_150] straddles it.
2309
+ const i1 = armed(
2310
+ intervention("d1", 2_000, 90_000, { ...ranAheadSpec }),
2311
+ shimChannel("d1", 90_000, [{ receivedAtMs: 92_050, at: 92_150, tools: ["unrelated_tool"] }]),
2312
+ );
2313
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2314
+ assert.ok(
2315
+ res.verdict === "inconclusive" &&
2316
+ !res.promotable &&
2317
+ res.interventions[0].evidence.includes("INSIDE the snapshot interval"),
2318
+ "a wire marker inside the received↔forwarded interval is UNORDERED — only wire strictly before the interval reads as after [QK:VERDICT-SNAPSHOT-INTERVAL-UNORDERED]",
2319
+ );
2320
+ }
2321
+
2322
+ // (6) snapshot received BEFORE the wire → a different claim, not promoted.
2323
+ {
2324
+ const ctl = armedControl(100_000);
2325
+ // wire = 112_100; interval [111_800, 111_805] fully before it.
2326
+ const i1 = armed(
2327
+ intervention("d1", 2_000, 110_000, { ...ranAheadSpec }),
2328
+ shimChannel("d1", 110_000, [{ receivedAtMs: 111_800, at: 111_805, tools: ["unrelated_tool"] }]),
2329
+ );
2330
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2331
+ assert.ok(
2332
+ res.verdict === "inconclusive" && res.interventions[0].evidence.includes("snapshot-before-wire"),
2333
+ "a snapshot that predates wire-availability carries a different claim than the §11-7-c after-the-wire row",
2334
+ );
2335
+ }
2336
+
2337
+ // (7) runtime No-such-tool for the measured id + snapshot absence → the
2338
+ // runtime ladder OWNS the run: B, never B-name-snapshot.
2339
+ {
2340
+ const ctl = armedControl(120_000);
2341
+ const i1 = armed(
2342
+ intervention("d1", 2_000, 130_000, { ...ranAheadSpec, noSuchToolId: PROVIDER_ID_MEASURED }),
2343
+ shimChannel("d1", 130_000, [{ receivedAtMs: 132_190, at: 132_195, tools: ["unrelated_tool"] }]),
2344
+ );
2345
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2346
+ assert.ok(
2347
+ res.verdict === "B" && res.interventions[0].failure === "B",
2348
+ "the model's own dispatch failing is the stronger runtime-B evidence and owns the combined run — the snapshot never substitutes for it",
2349
+ );
2350
+ }
2351
+
2352
+ // (8) TWO snapshots after the prompt frame → the exactly-one binding is a
2353
+ // named violation, never a pick-first.
2354
+ {
2355
+ const ctl = armedControl(140_000);
2356
+ const i1 = armed(
2357
+ intervention("d1", 2_000, 150_000, { ...ranAheadSpec }),
2358
+ shimChannel("d1", 150_000, [
2359
+ { receivedAtMs: 152_190, at: 152_195, tools: ["unrelated_tool"] },
2360
+ { receivedAtMs: 152_400, at: 152_405, tools: ["unrelated_tool", PROVIDER_ID_MEASURED] },
2361
+ ]),
2362
+ );
2363
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2364
+ assert.ok(
2365
+ res.verdict === "inconclusive" &&
2366
+ !res.promotable &&
2367
+ res.interventions[0].evidence.includes("snapshot-channel-violation"),
2368
+ "reinit/set-model re-emission making the binding ambiguous is a NAMED channel violation — the (b) reading is unavailable, never a pick-first promotion [QK:VERDICT-SNAPSHOT-ORDINAL-EXACTLY-ONE]",
2369
+ );
2370
+ assert.notEqual(res.status.orderingMeasurement, "unobserved", "…while axis (a) still carries its comparison");
2371
+ }
2372
+
2373
+ // (9) full promotion-shaped shim evidence under an UNARMED roster → ignored.
2374
+ // The roster is the authority on what was instrumented; found evidence never
2375
+ // promotes past the declaration.
2376
+ {
2377
+ const ctl = passingControl(160_000); // unarmed
2378
+ const i1raw = intervention("d1", 2_000, 170_000, { ...ranAheadSpec });
2379
+ const i1 = {
2380
+ record: i1raw.record, // snapshotInstrumented: false
2381
+ events: [
2382
+ ...i1raw.events,
2383
+ ...shimChannel("d1", 170_000, [{ receivedAtMs: 172_190, at: 172_195, tools: ["unrelated_tool"] }]),
2384
+ ],
2385
+ };
2386
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2387
+ assert.ok(
2388
+ res.verdict === "inconclusive" && !res.promotable && res.status.failureVerdict !== "B-name-snapshot",
2389
+ "shim-shaped evidence in the log of an UNARMED run is ignored — the roster declares the instrument, evidence alone never promotes [QK:VERDICT-SNAPSHOT-NEEDS-INSTRUMENT-FLAG]",
2390
+ );
2391
+ }
2392
+
2393
+ // (10) the binding is on the RECEIVE axis, not the append axis. Under stdout
2394
+ // backpressure a BOOT-time init (received before the prompt frame) can have
2395
+ // its downstream callback — and therefore its log append — land after the
2396
+ // prompt marker. A seq-only binding would promote that stale set as the turn
2397
+ // snapshot (GPT review 2026-07-29). Timeline: promptStart 191_660 < wire
2398
+ // 191_670 (ranAhead), shim prompt frame stamped 191_700; the ONLY snapshot
2399
+ // was received 191_680 (BEFORE the frame) but appended at 191_750.
2400
+ {
2401
+ const ctl = armedControl(180_000);
2402
+ const i1 = armed(
2403
+ intervention("d1", 2_000, 190_000, { wireAt: 1_570, nsLatency: 1_500, fixtureCall: false }),
2404
+ shimChannel("d1", 190_000, [{ receivedAtMs: 191_680, at: 191_750, tools: ["unrelated_tool"] }]),
2405
+ );
2406
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2407
+ assert.ok(
2408
+ res.verdict === "inconclusive" &&
2409
+ !res.promotable &&
2410
+ res.interventions[0].evidence.includes("snapshot-channel-violation"),
2411
+ "an init RECEIVED before the prompt frame is not a candidate even when its append lands after — the receive axis, not the append/callback axis, binds; zero candidates is a named violation [QK:VERDICT-SNAPSHOT-BINDING-RECEIVE-AXIS]",
2412
+ );
2413
+ }
2414
+
2415
+ // (11) a broken shim IDENTITY on an armed intervention invalidates the RUN —
2416
+ // the shim intermediates the CLI spawn, so a run without the calibrated shim
2417
+ // did not share the pair's launch path, and it may NOT keep voting on axis
2418
+ // (a) (GPT review 2026-07-29: a missing-shim run could otherwise build A/B
2419
+ // causal windows out of a different stimulus).
2420
+ {
2421
+ const ctl = armedControl(200_000);
2422
+ const i1 = armed(intervention("d1", 2_000, 210_000, { ...ranAheadSpec }), []); // armed, NO shim events
2423
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2424
+ assert.ok(
2425
+ res.verdict === "INVALIDATED" &&
2426
+ res.status.invalidRuns.some((r) => r.reason === "snapshot-topology") &&
2427
+ res.ordering.summary === "unobserved" &&
2428
+ res.status.orderingMeasurement !== "measured",
2429
+ "an armed intervention whose shim never reported in is INVALIDATED (snapshot-topology) and votes on NEITHER axis — not a (b)-only degradation [QK:VERDICT-SNAPSHOT-STRUCTURAL-INVALIDATES]",
2430
+ );
2431
+ }
2432
+
2433
+ // (12) the roster's expected target identity is CONSUMED: a shim boot
2434
+ // reporting a different content hash means this run did not execute the
2435
+ // pair's stimulus (env hijack, target swap) → INVALIDATED, never promoted.
2436
+ {
2437
+ const ctl = armedControl(220_000);
2438
+ const i1 = armed(
2439
+ intervention("d1", 2_000, 230_000, { ...ranAheadSpec }),
2440
+ shimChannel("d1", 230_000, [{ receivedAtMs: 232_190, at: 232_195, tools: ["unrelated_tool"] }], {
2441
+ bootTargetSha256: "zz-different",
2442
+ }),
2443
+ );
2444
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2445
+ assert.ok(
2446
+ res.verdict === "INVALIDATED" && res.status.invalidRuns.some((r) => r.reason === "snapshot-topology"),
2447
+ "a shim boot whose target path+sha does not match the roster's expected identity INVALIDATES the run — identity is verified, not merely recorded [QK:VERDICT-SNAPSHOT-TARGET-IDENTITY]",
2448
+ );
2449
+ }
2450
+
2451
+ // (13) a snapshot CONTAINING the measured id blocks the ladder BEFORE the
2452
+ // promotion rung — the absence check is positional, so this rung is what
2453
+ // keeps a present-id snapshot from promoting as absence.
2454
+ {
2455
+ const ctl = armedControl(240_000);
2456
+ const i1 = armed(
2457
+ intervention("d1", 2_000, 250_000, { ...ranAheadSpec }),
2458
+ shimChannel("d1", 250_000, [
2459
+ { receivedAtMs: 252_190, at: 252_195, tools: [PROVIDER_ID_MEASURED, "unrelated_tool"] },
2460
+ ]),
2461
+ );
2462
+ const res = classifyProbe([ctl.record, i1.record], [...ctl.events, ...i1.events]);
2463
+ assert.ok(
2464
+ res.verdict === "inconclusive" &&
2465
+ !res.promotable &&
2466
+ res.interventions[0].evidence.includes("CONTAINS the measured id"),
2467
+ "a snapshot that CONTAINS the measured id reads model-compliance, never absence — the contains-id rung blocks promotion [QK:VERDICT-SNAPSHOT-CONTAINS-ID-BLOCKS]",
2468
+ );
2469
+ }
2470
+ }
2471
+
2472
+ rmSync(tmp, { recursive: true, force: true });
2473
+ console.log("[check-probe-ordering] PASS — §11-7 probe seam: sameness pinned to backend.ts, phase attribution");
2474
+ console.log(" (set-model included), fixture wire markers + required probeRunId + legacy compat, the event-log");
2475
+ console.log(" door contract (reserved keys refused; unknown marker / broken axis / unjudgeable payload →");
2476
+ console.log(" MALFORMED, while a legitimately absent optional field stays an observation) plus the stream");
2477
+ console.log(" door (per-pid seq/clock judged on RAW append order), the observation-window protocol");
2478
+ console.log(" (censored ≠ candidate), runner-owned marker topology, the two reported axes, the");
2479
+ console.log(" paired-verdict truth table (P0/I0 outside the space, phase-qualified D, B promotion ladder, C,");
2480
+ console.log(" A's two-delay rule), and the §11-7-c consumer seam (CLI-target preconditions, upstream");
2481
+ console.log(" launch-semantics inspector, shim event doors, B-name-snapshot ladder with calibration and");
2482
+ console.log(" the roster-armed channel).");