@haystackeditor/cli 0.15.21 → 0.15.23

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.
@@ -0,0 +1,494 @@
1
+ import { createHash } from "node:crypto";
2
+ import chalk from "chalk";
3
+ import { discoverVerifyRoot, resolveServer, summarizeRun, waitForTerminal, } from "./verify-core.js";
4
+ import { startVerificationRun, } from "./verify.js";
5
+ function compareCodeUnits(left, right) {
6
+ return left < right ? -1 : left > right ? 1 : 0;
7
+ }
8
+ function requiredPlanString(value, label) {
9
+ if (typeof value !== "string" || value.trim() === "") {
10
+ throw new Error(`Frozen plan ${label} must be a non-empty string`);
11
+ }
12
+ return value;
13
+ }
14
+ function requiredPlanStrings(value, label) {
15
+ if (!Array.isArray(value)) {
16
+ throw new Error(`Frozen plan ${label} must be an array`);
17
+ }
18
+ return value.map((item, index) => requiredPlanString(item, `${label}[${index}]`));
19
+ }
20
+ function canonicalCellPlan(cell) {
21
+ if (!Array.isArray(cell.sourceEvidence)) {
22
+ throw new Error("Frozen plan sourceEvidence must be an array");
23
+ }
24
+ if (!Array.isArray(cell.oracles)) {
25
+ throw new Error("Frozen plan oracles must be an array");
26
+ }
27
+ if (typeof cell.confidence !== "number" || !Number.isFinite(cell.confidence)) {
28
+ throw new Error("Frozen plan confidence must be a finite number");
29
+ }
30
+ return {
31
+ risk_source_id: requiredPlanString(cell.riskSourceId, "riskSourceId"),
32
+ capability_id: requiredPlanString(cell.capabilityId, "capabilityId"),
33
+ title: requiredPlanString(cell.title, "title"),
34
+ rationale: requiredPlanString(cell.rationale, "rationale"),
35
+ observable_consequence: requiredPlanString(cell.observableConsequence, "observableConsequence"),
36
+ activation_conditions: requiredPlanStrings(cell.activationConditions, "activationConditions").sort(compareCodeUnits),
37
+ causal_path: requiredPlanStrings(cell.causalPath, "causalPath"),
38
+ severity: requiredPlanString(cell.severity, "severity"),
39
+ confidence: cell.confidence,
40
+ source_evidence: cell.sourceEvidence.map((item, index) => ({
41
+ evidence_id: requiredPlanString(item.evidenceId, `sourceEvidence[${index}].evidenceId`),
42
+ file: requiredPlanString(item.file, `sourceEvidence[${index}].file`),
43
+ line: item.line ?? null,
44
+ excerpt: requiredPlanString(item.excerpt, `sourceEvidence[${index}].excerpt`),
45
+ relationship: requiredPlanString(item.relationship, `sourceEvidence[${index}].relationship`),
46
+ })).sort((left, right) => compareCodeUnits(JSON.stringify(left), JSON.stringify(right))),
47
+ oracles: cell.oracles.map((oracle, index) => ({
48
+ oracle_id: requiredPlanString(oracle.oracleId, `oracles[${index}].oracleId`),
49
+ kind: requiredPlanString(oracle.kind, `oracles[${index}].kind`),
50
+ measurement: requiredPlanString(oracle.measurement, `oracles[${index}].measurement`),
51
+ threshold: oracle.threshold ?? null,
52
+ description: requiredPlanString(oracle.description, `oracles[${index}].description`),
53
+ })).sort((left, right) => compareCodeUnits(JSON.stringify(left), JSON.stringify(right))),
54
+ };
55
+ }
56
+ function bisectOutcomeSignature(observation) {
57
+ if (!observation)
58
+ return "missing";
59
+ return JSON.stringify({
60
+ status: observation.status,
61
+ culprit_sha: observation.culprit_sha,
62
+ parent_sha: observation.parent_sha,
63
+ incident: observation.incident,
64
+ probes: observation.probes,
65
+ });
66
+ }
67
+ function canonicalBisectObservation(runId, chapter) {
68
+ const bisect = chapter.bisect;
69
+ if (!bisect || !Array.isArray(bisect.probes))
70
+ return null;
71
+ return {
72
+ run_id: runId,
73
+ status: bisect.status,
74
+ culprit_sha: bisect.culpritSha ?? null,
75
+ parent_sha: bisect.parentSha ?? null,
76
+ incident: bisect.incidentProbe
77
+ ? {
78
+ commit: bisect.incidentProbe.commit,
79
+ status: bisect.incidentProbe.status,
80
+ }
81
+ : null,
82
+ probes: bisect.probes
83
+ .map(probe => ({
84
+ commit: probe.commit,
85
+ status: probe.status,
86
+ }))
87
+ .sort((left, right) => compareCodeUnits(left.commit, right.commit)),
88
+ };
89
+ }
90
+ function reportBisectOf(bisect, runIds) {
91
+ return {
92
+ chapter_id: bisect.chapter_id,
93
+ label: bisect.label,
94
+ outcomes: runIds.flatMap((runId) => {
95
+ const observation = bisect.byRun.get(runId);
96
+ return observation ? [observation] : [];
97
+ }),
98
+ missing_run_ids: runIds.filter((runId) => !bisect.byRun.has(runId)),
99
+ };
100
+ }
101
+ export function cellPlanFingerprint(cell) {
102
+ return createHash("sha256")
103
+ .update(JSON.stringify(canonicalCellPlan(cell)))
104
+ .digest("hex");
105
+ }
106
+ function outcomeSignature(observation) {
107
+ if (!observation)
108
+ return "missing";
109
+ return JSON.stringify({
110
+ status: observation.status,
111
+ verdict: observation.verdict,
112
+ });
113
+ }
114
+ function reportCellOf(cell, runIds) {
115
+ return {
116
+ cell_id: cell.cell_id,
117
+ chapter_id: cell.chapter_id,
118
+ title: cell.title,
119
+ outcomes: runIds.flatMap((runId) => {
120
+ const observation = cell.byRun.get(runId);
121
+ return observation ? [observation] : [];
122
+ }),
123
+ plans: runIds.flatMap((runId) => {
124
+ const observation = cell.planByRun.get(runId);
125
+ return observation ? [observation] : [];
126
+ }),
127
+ missing_run_ids: runIds.filter((runId) => !cell.byRun.has(runId)),
128
+ };
129
+ }
130
+ /**
131
+ * Plan identity and runtime outcomes are independent axes:
132
+ *
133
+ * - missing IDs or changed frozen contracts are planner drift;
134
+ * - status/judgment changes under an identical contract are executor drift.
135
+ *
136
+ * Titles and other prose are fingerprinted to detect a changed judge input,
137
+ * but cells are correlated only by their exact stable IDs.
138
+ */
139
+ export function analyzeReproducibility(runs, requestedRuns = runs.length, timedOutRunIds = [], scope = "full") {
140
+ const runIds = runs.map((run) => run.runId);
141
+ const cells = new Map();
142
+ const bisects = new Map();
143
+ const plannerIncompleteRunIds = new Set(timedOutRunIds);
144
+ const executorIncompleteRunIds = new Set(timedOutRunIds);
145
+ for (const run of runs) {
146
+ if (!["complete", "failed"].includes(run.status)
147
+ || run.events.some(event => event.eventId.startsWith("evt-stale-"))) {
148
+ plannerIncompleteRunIds.add(run.runId);
149
+ executorIncompleteRunIds.add(run.runId);
150
+ }
151
+ const seenInRun = new Set();
152
+ const seenBisectsInRun = new Set();
153
+ for (const chapter of run.chapters) {
154
+ if (chapter.executionMode === "bisect") {
155
+ if (seenBisectsInRun.has(chapter.chapterId)) {
156
+ throw new Error(`Run ${run.runId} contains duplicate stable bisect chapter id ${chapter.chapterId}`);
157
+ }
158
+ seenBisectsInRun.add(chapter.chapterId);
159
+ const observation = canonicalBisectObservation(run.runId, chapter);
160
+ if (!observation) {
161
+ executorIncompleteRunIds.add(run.runId);
162
+ continue;
163
+ }
164
+ const indexed = bisects.get(chapter.chapterId) ?? {
165
+ chapter_id: chapter.chapterId,
166
+ label: chapter.label,
167
+ byRun: new Map(),
168
+ };
169
+ indexed.byRun.set(run.runId, observation);
170
+ bisects.set(chapter.chapterId, indexed);
171
+ if (observation.status !== "found") {
172
+ executorIncompleteRunIds.add(run.runId);
173
+ }
174
+ continue;
175
+ }
176
+ if (!Array.isArray(chapter.riskCells)) {
177
+ plannerIncompleteRunIds.add(run.runId);
178
+ executorIncompleteRunIds.add(run.runId);
179
+ continue;
180
+ }
181
+ for (const cell of chapter.riskCells) {
182
+ const stableKey = `${chapter.chapterId}:${cell.riskCellId}`;
183
+ if (seenInRun.has(stableKey)) {
184
+ throw new Error(`Run ${run.runId} contains duplicate stable cell id ${stableKey}`);
185
+ }
186
+ seenInRun.add(stableKey);
187
+ const indexed = cells.get(stableKey) ?? {
188
+ cell_id: cell.riskCellId,
189
+ chapter_id: chapter.chapterId,
190
+ title: cell.title,
191
+ byRun: new Map(),
192
+ planByRun: new Map(),
193
+ };
194
+ indexed.byRun.set(run.runId, {
195
+ run_id: run.runId,
196
+ status: cell.status,
197
+ verdict: cell.agentJudgment?.verdict ?? null,
198
+ });
199
+ indexed.planByRun.set(run.runId, {
200
+ run_id: run.runId,
201
+ fingerprint: cellPlanFingerprint(cell),
202
+ });
203
+ cells.set(stableKey, indexed);
204
+ if (cell.status === "infra_error"
205
+ || cell.status === "inconclusive"
206
+ || !["passed", "failed"].includes(cell.status)) {
207
+ executorIncompleteRunIds.add(run.runId);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ const planDriftCells = [];
213
+ const outcomeDriftCells = [];
214
+ const unrunnableCells = [];
215
+ const bisectDriftChapters = [];
216
+ const unrunnableBisects = [];
217
+ let stableCells = 0;
218
+ let stableBisects = 0;
219
+ for (const cell of [...cells.values()].sort((left, right) => compareCodeUnits(`${left.chapter_id}:${left.cell_id}`, `${right.chapter_id}:${right.cell_id}`))) {
220
+ const reportCell = reportCellOf(cell, runIds);
221
+ const planSignatures = new Set(runIds.map((runId) => cell.planByRun.get(runId)?.fingerprint ?? "missing"));
222
+ const planDrift = planSignatures.size > 1;
223
+ if (planDrift)
224
+ planDriftCells.push(reportCell);
225
+ const outcomeSignatures = new Set(runIds.map((runId) => outcomeSignature(cell.byRun.get(runId))));
226
+ const outcomeDrift = !planDrift && outcomeSignatures.size > 1;
227
+ if (outcomeDrift)
228
+ outcomeDriftCells.push(reportCell);
229
+ const outcomes = [...cell.byRun.values()];
230
+ const unrunnable = outcomes.some((outcome) => !["passed", "failed"].includes(outcome.status));
231
+ if (unrunnable)
232
+ unrunnableCells.push(reportCell);
233
+ if (!planDrift
234
+ && !outcomeDrift
235
+ && !unrunnable
236
+ && reportCell.missing_run_ids.length === 0) {
237
+ stableCells += 1;
238
+ }
239
+ }
240
+ for (const bisect of [...bisects.values()].sort((left, right) => compareCodeUnits(left.chapter_id, right.chapter_id))) {
241
+ const reportBisect = reportBisectOf(bisect, runIds);
242
+ const outcomeSignatures = new Set(runIds.map((runId) => bisectOutcomeSignature(bisect.byRun.get(runId))));
243
+ const outcomeDrift = outcomeSignatures.size > 1;
244
+ if (outcomeDrift)
245
+ bisectDriftChapters.push(reportBisect);
246
+ const unrunnable = [...bisect.byRun.values()].some(observation => observation.status !== "found");
247
+ if (unrunnable)
248
+ unrunnableBisects.push(reportBisect);
249
+ if (!outcomeDrift
250
+ && !unrunnable
251
+ && reportBisect.missing_run_ids.length === 0) {
252
+ stableBisects += 1;
253
+ }
254
+ }
255
+ const plannerHasComparableWork = cells.size > 0;
256
+ const executorHasComparableWork = cells.size > 0 || bisects.size > 0;
257
+ const plannerIncomplete = plannerIncompleteRunIds.size > 0
258
+ || runs.length < requestedRuns
259
+ || !plannerHasComparableWork;
260
+ const executorIncomplete = executorIncompleteRunIds.size > 0
261
+ || runs.length < requestedRuns
262
+ || !executorHasComparableWork;
263
+ const plannerReproducible = !plannerIncomplete
264
+ && planDriftCells.length === 0;
265
+ const executorReproducible = !executorIncomplete
266
+ && planDriftCells.length === 0
267
+ && outcomeDriftCells.length === 0
268
+ && unrunnableCells.length === 0
269
+ && bisectDriftChapters.length === 0
270
+ && unrunnableBisects.length === 0;
271
+ const reproducible = scope === "planner"
272
+ ? plannerReproducible
273
+ : scope === "executor"
274
+ ? executorReproducible
275
+ : plannerReproducible && executorReproducible;
276
+ const incompleteRunIds = scope === "planner"
277
+ ? plannerIncompleteRunIds
278
+ : executorIncompleteRunIds;
279
+ return {
280
+ schema_version: "1.0.0",
281
+ scope,
282
+ requested_runs: requestedRuns,
283
+ completed_runs: runs.filter((run) => ["complete", "failed"].includes(run.status)).length,
284
+ run_ids: runIds,
285
+ plan_source_run_id: scope === "executor" && cells.size > 0
286
+ ? runs[0]?.runId ?? null
287
+ : null,
288
+ compared_cells: cells.size,
289
+ stable_cells: stableCells,
290
+ compared_bisects: bisects.size,
291
+ stable_bisects: stableBisects,
292
+ plan_drift_cells: planDriftCells,
293
+ outcome_drift_cells: outcomeDriftCells,
294
+ unrunnable_cells: unrunnableCells,
295
+ bisect_drift_chapters: bisectDriftChapters,
296
+ unrunnable_bisects: unrunnableBisects,
297
+ planner_incomplete_run_ids: [...plannerIncompleteRunIds],
298
+ executor_incomplete_run_ids: [...executorIncompleteRunIds],
299
+ incomplete_run_ids: [...incompleteRunIds],
300
+ timed_out_run_ids: timedOutRunIds,
301
+ planner_reproducible: plannerReproducible,
302
+ executor_reproducible: executorReproducible,
303
+ reproducible,
304
+ };
305
+ }
306
+ function positiveNumber(raw, fallback, flag) {
307
+ const value = raw === undefined ? fallback : Number(raw);
308
+ if (!Number.isFinite(value) || value <= 0) {
309
+ throw new Error(`${flag} takes a positive number`);
310
+ }
311
+ return value;
312
+ }
313
+ function parseScope(raw) {
314
+ const scope = raw ?? "executor";
315
+ if (scope === "executor" || scope === "planner" || scope === "full") {
316
+ return scope;
317
+ }
318
+ throw new Error("--scope must be executor, planner, or full");
319
+ }
320
+ function renderCell(cell, kind) {
321
+ const glyph = kind === "unrunnable" ? chalk.yellow("!") : chalk.red("✗");
322
+ console.log(` ${glyph} ${cell.chapter_id}/${cell.cell_id} ${chalk.dim(cell.title)}`);
323
+ if (kind === "plan") {
324
+ for (const plan of cell.plans) {
325
+ console.log(` ${plan.run_id}: plan ${plan.fingerprint.slice(0, 12)}`);
326
+ }
327
+ }
328
+ else {
329
+ for (const outcome of cell.outcomes) {
330
+ console.log(` ${outcome.run_id}: ${outcome.status}`
331
+ + `${outcome.verdict ? ` / ${outcome.verdict}` : ""}`);
332
+ }
333
+ }
334
+ for (const runId of cell.missing_run_ids) {
335
+ console.log(` ${runId}: ${chalk.yellow("missing from frozen plan")}`);
336
+ }
337
+ }
338
+ function renderBisect(bisect, kind) {
339
+ const glyph = kind === "unrunnable" ? chalk.yellow("!") : chalk.red("✗");
340
+ console.log(` ${glyph} ${bisect.chapter_id} ${chalk.dim(bisect.label)}`);
341
+ for (const outcome of bisect.outcomes) {
342
+ console.log(` ${outcome.run_id}: ${outcome.status}`
343
+ + `${outcome.culprit_sha
344
+ ? ` / ${outcome.parent_sha ?? "?"}..${outcome.culprit_sha}`
345
+ : ""}`);
346
+ }
347
+ for (const runId of bisect.missing_run_ids) {
348
+ console.log(` ${runId}: ${chalk.yellow("missing bisect result")}`);
349
+ }
350
+ }
351
+ export function runHasFrozenRiskPlan(run) {
352
+ return run.chapters.some(chapter => chapter.executionMode !== "bisect"
353
+ && (chapter.riskCells?.length ?? 0) > 0);
354
+ }
355
+ export async function verifyReproducibilityCommand(specimen, options) {
356
+ const requestedRuns = positiveNumber(options.runs, 3, "--runs");
357
+ if (!Number.isInteger(requestedRuns) || requestedRuns < 2 || requestedRuns > 20) {
358
+ throw new Error("--runs must be an integer from 2 to 20");
359
+ }
360
+ const scope = parseScope(options.scope);
361
+ if (options.planSource && scope !== "executor") {
362
+ throw new Error("--plan-source is only valid with --scope executor");
363
+ }
364
+ const intervalMs = positiveNumber(options.interval, 3, "--interval") * 1_000;
365
+ const timeoutMs = options.timeout
366
+ ? positiveNumber(options.timeout, 0, "--timeout") * 60_000
367
+ : undefined;
368
+ const server = resolveServer(options.server);
369
+ const repoRoot = discoverVerifyRoot(options.repoRoot);
370
+ const runs = [];
371
+ const timedOutRunIds = [];
372
+ let executorPlanSourceRunId = options.planSource;
373
+ for (let index = 0; index < requestedRuns; index += 1) {
374
+ const planSourceRunId = scope === "executor"
375
+ ? executorPlanSourceRunId
376
+ : undefined;
377
+ if (!options.json) {
378
+ console.error(chalk.dim(`Reproducibility run ${index + 1}/${requestedRuns}: `
379
+ + `${planSourceRunId
380
+ ? `replaying plan from ${planSourceRunId}`
381
+ : `planning ${specimen}`}…`));
382
+ }
383
+ const started = await startVerificationRun(specimen, {
384
+ ...options,
385
+ json: true,
386
+ open: false,
387
+ planSourceRunId,
388
+ });
389
+ const waited = await waitForTerminal({
390
+ server,
391
+ repoRoot,
392
+ selector: started.runId,
393
+ intervalMs,
394
+ timeoutMs,
395
+ });
396
+ runs.push(waited.loaded.run);
397
+ if (scope === "executor"
398
+ && !executorPlanSourceRunId
399
+ && runHasFrozenRiskPlan(waited.loaded.run)) {
400
+ executorPlanSourceRunId = waited.loaded.run.runId;
401
+ }
402
+ if (!options.json) {
403
+ const summary = summarizeRun(waited.loaded, server);
404
+ console.error(` ${started.runId}: ${summary.counts.passed} passed, `
405
+ + `${summary.counts.failed} failed, `
406
+ + `${summary.counts.could_not_run} could not run`);
407
+ }
408
+ if (waited.timedOut) {
409
+ timedOutRunIds.push(started.runId);
410
+ break;
411
+ }
412
+ if (waited.endReason === "stale")
413
+ break;
414
+ }
415
+ const report = analyzeReproducibility(runs, requestedRuns, timedOutRunIds, scope);
416
+ if (scope === "executor") {
417
+ report.plan_source_run_id = executorPlanSourceRunId ?? null;
418
+ }
419
+ if (options.json) {
420
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
421
+ }
422
+ else {
423
+ const scopeHasComparableWork = scope === "planner"
424
+ ? report.compared_cells > 0
425
+ : report.compared_cells > 0 || report.compared_bisects > 0;
426
+ console.log("");
427
+ console.log(chalk.bold(`Compared ${report.compared_cells} stable cell ID`
428
+ + `${report.compared_cells === 1 ? "" : "s"} and `
429
+ + `${report.compared_bisects} stable bisect chapter ID`
430
+ + `${report.compared_bisects === 1 ? "" : "s"} across `
431
+ + `${report.completed_runs}/${report.requested_runs} runs (${scope}).`));
432
+ if (report.plan_drift_cells.length > 0) {
433
+ console.log(chalk.red.bold(`${report.plan_drift_cells.length} planner-drift cell`
434
+ + `${report.plan_drift_cells.length === 1 ? "" : "s"}:`));
435
+ for (const cell of report.plan_drift_cells)
436
+ renderCell(cell, "plan");
437
+ }
438
+ else if (report.compared_cells > 0) {
439
+ console.log(chalk.green("✓ Frozen plan contracts were identical."));
440
+ }
441
+ if (scope !== "planner") {
442
+ if (report.outcome_drift_cells.length > 0) {
443
+ console.log(chalk.red.bold(`${report.outcome_drift_cells.length} runtime/judgment-drift cell`
444
+ + `${report.outcome_drift_cells.length === 1 ? "" : "s"}:`));
445
+ for (const cell of report.outcome_drift_cells) {
446
+ renderCell(cell, "outcome");
447
+ }
448
+ }
449
+ else if (report.executor_reproducible && report.compared_cells > 0) {
450
+ console.log(chalk.green(`✓ All ${report.stable_cells} cells produced the same status and judgment.`));
451
+ }
452
+ if (report.bisect_drift_chapters.length > 0) {
453
+ console.log(chalk.red.bold(`${report.bisect_drift_chapters.length} bisect chapter`
454
+ + `${report.bisect_drift_chapters.length === 1 ? "" : "s"} changed outcomes:`));
455
+ for (const bisect of report.bisect_drift_chapters) {
456
+ renderBisect(bisect, "drift");
457
+ }
458
+ }
459
+ else if (report.compared_bisects > 0) {
460
+ console.log(chalk.green(`✓ All ${report.stable_bisects} bisect chapters found the same exact boundary.`));
461
+ }
462
+ }
463
+ const driftIds = new Set([...report.plan_drift_cells, ...report.outcome_drift_cells].map(cell => `${cell.chapter_id}:${cell.cell_id}`));
464
+ const repeatablyUnrunnable = report.unrunnable_cells.filter(cell => !driftIds.has(`${cell.chapter_id}:${cell.cell_id}`));
465
+ if (scope !== "planner" && repeatablyUnrunnable.length > 0) {
466
+ console.log(chalk.yellow.bold(`${repeatablyUnrunnable.length} cell`
467
+ + `${repeatablyUnrunnable.length === 1 ? "" : "s"} never produced a verdict:`));
468
+ for (const cell of repeatablyUnrunnable) {
469
+ renderCell(cell, "unrunnable");
470
+ }
471
+ }
472
+ if (scope !== "planner" && report.unrunnable_bisects.length > 0) {
473
+ console.log(chalk.yellow.bold(`${report.unrunnable_bisects.length} bisect chapter`
474
+ + `${report.unrunnable_bisects.length === 1 ? "" : "s"} did not find a boundary:`));
475
+ for (const bisect of report.unrunnable_bisects) {
476
+ renderBisect(bisect, "unrunnable");
477
+ }
478
+ }
479
+ if (report.incomplete_run_ids.length > 0
480
+ || !scopeHasComparableWork) {
481
+ console.log(chalk.yellow("Reproducibility is inconclusive because at least one run or cell did not produce the required result."));
482
+ }
483
+ }
484
+ const scopeHasComparableWork = scope === "planner"
485
+ ? report.compared_cells > 0
486
+ : report.compared_cells > 0 || report.compared_bisects > 0;
487
+ process.exitCode = report.incomplete_run_ids.length > 0
488
+ || report.completed_runs < report.requested_runs
489
+ || !scopeHasComparableWork
490
+ ? 2
491
+ : report.reproducible
492
+ ? 0
493
+ : 1;
494
+ }
@@ -1,3 +1,4 @@
1
+ export declare const VERIFY_SPECIMENS: Set<string>;
1
2
  export interface VerifyOptions {
2
3
  server?: string;
3
4
  token?: string;
@@ -7,5 +8,15 @@ export interface VerifyOptions {
7
8
  intentTranscript?: string;
8
9
  json?: boolean;
9
10
  open?: boolean;
11
+ /** Internal reproducibility option: execute an already-frozen plan. */
12
+ planSourceRunId?: string;
10
13
  }
14
+ export interface StartRunResponse {
15
+ runId: string;
16
+ status: string;
17
+ resultsUrl: string;
18
+ }
19
+ export declare function parseStartRunResponse(raw: string): StartRunResponse;
20
+ export declare function verifierErrorReason(raw: string, contentType: string | null): string;
21
+ export declare function startVerificationRun(specimenArg: string | undefined, options: VerifyOptions): Promise<StartRunResponse>;
11
22
  export declare function verifyCommand(specimenArg: string | undefined, options: VerifyOptions): Promise<void>;
@@ -4,7 +4,51 @@ import { resolve } from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import { setVerifyToken, verifyAuthHeaders } from './verify-core.js';
6
6
  const DEFAULT_CONTROL_PLANE = 'http://127.0.0.1:3000';
7
- const SPECIMENS = new Set(['all', 'posthog-js-478', 'analytics-js-255']);
7
+ export const VERIFY_SPECIMENS = new Set(['all', 'posthog-js-478', 'analytics-js-255']);
8
+ export function parseStartRunResponse(raw) {
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(raw);
12
+ }
13
+ catch {
14
+ throw new Error('Verifier returned an invalid JSON response.');
15
+ }
16
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
17
+ throw new Error('Verifier returned an invalid response object.');
18
+ }
19
+ const response = parsed;
20
+ const requiredString = (field) => {
21
+ const value = response[field];
22
+ if (typeof value !== 'string' || value.trim() === '') {
23
+ throw new Error(`Verifier response is missing a valid ${field}.`);
24
+ }
25
+ return value;
26
+ };
27
+ return {
28
+ runId: requiredString('runId'),
29
+ status: requiredString('status'),
30
+ resultsUrl: requiredString('resultsUrl'),
31
+ };
32
+ }
33
+ export function verifierErrorReason(raw, contentType) {
34
+ if (!contentType?.toLowerCase().includes('application/json'))
35
+ return raw;
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(raw);
39
+ }
40
+ catch (error) {
41
+ return `malformed JSON error response (${error instanceof Error ? error.message : String(error)}); raw body: ${raw}`;
42
+ }
43
+ if (parsed
44
+ && typeof parsed === 'object'
45
+ && !Array.isArray(parsed)
46
+ && typeof parsed.error === 'string'
47
+ && parsed.error.trim() !== '') {
48
+ return parsed.error;
49
+ }
50
+ return `JSON error response omitted a string error; raw body: ${raw}`;
51
+ }
8
52
  function openResults(url) {
9
53
  const platform = process.platform;
10
54
  const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -24,18 +68,15 @@ async function readIntentTranscript(path) {
24
68
  throw new Error(`Could not read intent transcript at ${absolutePath}: ${message}`);
25
69
  }
26
70
  }
27
- export async function verifyCommand(specimenArg, options) {
71
+ export async function startVerificationRun(specimenArg, options) {
28
72
  const specimen = specimenArg || 'all';
29
- if (!SPECIMENS.has(specimen)) {
73
+ if (!VERIFY_SPECIMENS.has(specimen)) {
30
74
  throw new Error(`Unknown or unreproduced specimen "${specimen}". Choose all, posthog-js-478, or analytics-js-255.`);
31
75
  }
32
76
  setVerifyToken(options.token);
33
77
  const server = (options.server || process.env.HAYSTACK_VERIFY_SERVER || DEFAULT_CONTROL_PLANE)
34
78
  .replace(/\/+$/, '');
35
79
  const intentTranscript = await readIntentTranscript(options.intentTranscript);
36
- if (!options.json) {
37
- console.error(chalk.dim('Preparing the cloud verification run…'));
38
- }
39
80
  let response;
40
81
  try {
41
82
  response = await fetch(`${server}/api/cloud-verifier/runs`, {
@@ -47,6 +88,7 @@ export async function verifyCommand(specimenArg, options) {
47
88
  baseRef: options.base,
48
89
  headRef: options.head,
49
90
  intentTranscript,
91
+ planSourceRunId: options.planSourceRunId,
50
92
  }),
51
93
  });
52
94
  }
@@ -59,25 +101,16 @@ export async function verifyCommand(specimenArg, options) {
59
101
  throw new Error(`Control plane at ${server} requires a token (HTTP 401). Pass --token or set HAYSTACK_VERIFY_TOKEN.`);
60
102
  }
61
103
  if (!response.ok) {
62
- let reason = raw;
63
- try {
64
- reason = JSON.parse(raw).error || raw;
65
- }
66
- catch {
67
- // Preserve the response body when it is not JSON.
68
- }
104
+ const reason = verifierErrorReason(raw, response.headers.get('content-type'));
69
105
  throw new Error(`Verifier rejected the run (${response.status}): ${reason}`);
70
106
  }
71
- let started;
72
- try {
73
- started = JSON.parse(raw);
74
- }
75
- catch {
76
- throw new Error('Verifier returned an invalid response.');
77
- }
78
- if (!started.runId || !started.resultsUrl) {
79
- throw new Error('Verifier response is missing runId or resultsUrl.');
107
+ return parseStartRunResponse(raw);
108
+ }
109
+ export async function verifyCommand(specimenArg, options) {
110
+ if (!options.json) {
111
+ console.error(chalk.dim('Preparing the cloud verification run…'));
80
112
  }
113
+ const started = await startVerificationRun(specimenArg, options);
81
114
  if (options.json) {
82
115
  process.stdout.write(`${JSON.stringify({
83
116
  schema_version: '1.0.0',