@haystackeditor/cli 0.15.21 → 0.15.22
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.
- package/README.md +28 -2
- package/dist/commands/verify-core.d.ts +26 -1
- package/dist/commands/verify-core.js +59 -23
- package/dist/commands/verify-hosted.d.ts +60 -0
- package/dist/commands/verify-hosted.js +342 -0
- package/dist/commands/verify-mcp.js +59 -0
- package/dist/commands/verify-ops.js +1 -1
- package/dist/commands/verify-reproducibility.d.ts +85 -0
- package/dist/commands/verify-reproducibility.js +494 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +55 -22
- package/dist/index.js +71 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/dist/utils/haystack-api.d.ts +1 -1
- package/dist/utils/haystack-api.js +2 -2
- package/package.json +1 -1
- package/schemas/cloud-verifier.v1.json +96 -0
|
@@ -99,6 +99,19 @@ export const VERIFY_TOOLS = [
|
|
|
99
99
|
required: ['cell'],
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'verify_universe',
|
|
104
|
+
description: 'Get or open a live universe for one test: returns the running environment when one is still live, otherwise reopens the retained state onto fresh compute (reusing an existing attachment when present).',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
run: runInput,
|
|
109
|
+
cell: { type: 'string', description: 'Stable test selector: ordinal (1-based), riskCellId, or exact planner cellKey.' },
|
|
110
|
+
side: { type: 'string', enum: ['base', 'head'], description: 'Matched universe side (default head).' },
|
|
111
|
+
},
|
|
112
|
+
required: ['cell'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
102
115
|
{
|
|
103
116
|
name: 'verify_cleanup',
|
|
104
117
|
description: 'Read retained-state cleanup for one test universe by its exact fork ID: shared references, provider deletion attempts, and the durable cleanup-SLO verdict.',
|
|
@@ -292,6 +305,52 @@ export async function runVerifyMcpServer(options) {
|
|
|
292
305
|
return ok(match.cell);
|
|
293
306
|
return ok(cellDetail(loaded.run, match));
|
|
294
307
|
}
|
|
308
|
+
if (name === 'verify_universe') {
|
|
309
|
+
if (typeof input.cell !== 'string')
|
|
310
|
+
return fail('cell required');
|
|
311
|
+
if (input.side !== undefined
|
|
312
|
+
&& input.side !== 'base'
|
|
313
|
+
&& input.side !== 'head') {
|
|
314
|
+
return fail('side must be base or head');
|
|
315
|
+
}
|
|
316
|
+
const loaded = await loadRunAuto({ server, repoRoot, selector });
|
|
317
|
+
const match = findCellByStableSelector(loaded.run, input.cell);
|
|
318
|
+
const side = input.side === 'base' ? 'base' : 'head';
|
|
319
|
+
const universe = side === 'base' ? match.cell.base : match.cell.head;
|
|
320
|
+
const expired = universe.expiresAt
|
|
321
|
+
? Date.parse(universe.expiresAt) < Date.now()
|
|
322
|
+
: false;
|
|
323
|
+
if (universe.browserUrl && universe.status === 'running' && !expired) {
|
|
324
|
+
return ok({
|
|
325
|
+
schema_version: '1.0.0',
|
|
326
|
+
run_id: loaded.run.runId,
|
|
327
|
+
ordinal: match.cell.ordinal,
|
|
328
|
+
side,
|
|
329
|
+
source: 'running',
|
|
330
|
+
url: universe.browserUrl,
|
|
331
|
+
expires_at: universe.expiresAt ?? null,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (!universe.forkId) {
|
|
335
|
+
return fail(`Test ${match.cell.ordinal}'s ${side} universe is not running and `
|
|
336
|
+
+ 'has no durable fork ID to reopen.');
|
|
337
|
+
}
|
|
338
|
+
const { requestUniverseReopen } = await import('./verify-ops.js');
|
|
339
|
+
const reopened = await requestUniverseReopen(server, universe.forkId);
|
|
340
|
+
return ok({
|
|
341
|
+
schema_version: '1.0.0',
|
|
342
|
+
run_id: loaded.run.runId,
|
|
343
|
+
ordinal: match.cell.ordinal,
|
|
344
|
+
side,
|
|
345
|
+
source: 'reopened',
|
|
346
|
+
url: reopened.browserUrl,
|
|
347
|
+
expires_at: reopened.expiresAt,
|
|
348
|
+
fork_id: reopened.forkId,
|
|
349
|
+
sandbox_id: reopened.sandboxId,
|
|
350
|
+
state_expires_at: reopened.stateExpiresAt,
|
|
351
|
+
reused: reopened.reused,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
295
354
|
if (name === 'verify_cleanup') {
|
|
296
355
|
if (typeof input.cell !== 'string')
|
|
297
356
|
return fail('cell required');
|
|
@@ -26,7 +26,7 @@ function renderStatus(summary) {
|
|
|
26
26
|
: summary.status === 'complete' ? chalk.green(summary.status)
|
|
27
27
|
: summary.status === 'failed' ? chalk.red(summary.status)
|
|
28
28
|
: chalk.cyan(summary.status);
|
|
29
|
-
console.log(`${chalk.bold(summary.run_id)} — ${state}${summary.age ? chalk.dim(` (
|
|
29
|
+
console.log(`${chalk.bold(summary.run_id)} — ${state}${summary.age ? chalk.dim(` (started ${summary.age} ago)`) : ''}`);
|
|
30
30
|
for (const chapter of summary.chapters) {
|
|
31
31
|
console.log(`${chalk.dim(chapter.repository)} ${chapter.pull_request} — ${chapter.label}`);
|
|
32
32
|
if (chapter.execution_mode === 'bisect' && chapter.bisect) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type RiskCell, type VerificationRun } from "./verify-core.js";
|
|
2
|
+
import { type VerifyOptions } from "./verify.js";
|
|
3
|
+
export type ReproducibilityScope = "executor" | "planner" | "full";
|
|
4
|
+
export interface CellOutcomeObservation {
|
|
5
|
+
run_id: string;
|
|
6
|
+
status: string;
|
|
7
|
+
verdict: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface CellPlanObservation {
|
|
10
|
+
run_id: string;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReproducibilityCell {
|
|
14
|
+
cell_id: string;
|
|
15
|
+
chapter_id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
outcomes: CellOutcomeObservation[];
|
|
18
|
+
plans: CellPlanObservation[];
|
|
19
|
+
missing_run_ids: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface BisectOutcomeObservation {
|
|
22
|
+
run_id: string;
|
|
23
|
+
status: string;
|
|
24
|
+
culprit_sha: string | null;
|
|
25
|
+
parent_sha: string | null;
|
|
26
|
+
incident: {
|
|
27
|
+
commit: string;
|
|
28
|
+
status: string;
|
|
29
|
+
} | null;
|
|
30
|
+
probes: Array<{
|
|
31
|
+
commit: string;
|
|
32
|
+
status: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface ReproducibilityBisect {
|
|
36
|
+
chapter_id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
outcomes: BisectOutcomeObservation[];
|
|
39
|
+
missing_run_ids: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ReproducibilityReport {
|
|
42
|
+
schema_version: "1.0.0";
|
|
43
|
+
scope: ReproducibilityScope;
|
|
44
|
+
requested_runs: number;
|
|
45
|
+
completed_runs: number;
|
|
46
|
+
run_ids: string[];
|
|
47
|
+
plan_source_run_id: string | null;
|
|
48
|
+
compared_cells: number;
|
|
49
|
+
stable_cells: number;
|
|
50
|
+
compared_bisects: number;
|
|
51
|
+
stable_bisects: number;
|
|
52
|
+
plan_drift_cells: ReproducibilityCell[];
|
|
53
|
+
outcome_drift_cells: ReproducibilityCell[];
|
|
54
|
+
unrunnable_cells: ReproducibilityCell[];
|
|
55
|
+
bisect_drift_chapters: ReproducibilityBisect[];
|
|
56
|
+
unrunnable_bisects: ReproducibilityBisect[];
|
|
57
|
+
planner_incomplete_run_ids: string[];
|
|
58
|
+
executor_incomplete_run_ids: string[];
|
|
59
|
+
incomplete_run_ids: string[];
|
|
60
|
+
timed_out_run_ids: string[];
|
|
61
|
+
planner_reproducible: boolean;
|
|
62
|
+
executor_reproducible: boolean;
|
|
63
|
+
reproducible: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function cellPlanFingerprint(cell: RiskCell): string;
|
|
66
|
+
/**
|
|
67
|
+
* Plan identity and runtime outcomes are independent axes:
|
|
68
|
+
*
|
|
69
|
+
* - missing IDs or changed frozen contracts are planner drift;
|
|
70
|
+
* - status/judgment changes under an identical contract are executor drift.
|
|
71
|
+
*
|
|
72
|
+
* Titles and other prose are fingerprinted to detect a changed judge input,
|
|
73
|
+
* but cells are correlated only by their exact stable IDs.
|
|
74
|
+
*/
|
|
75
|
+
export declare function analyzeReproducibility(runs: VerificationRun[], requestedRuns?: number, timedOutRunIds?: string[], scope?: ReproducibilityScope): ReproducibilityReport;
|
|
76
|
+
export interface VerifyReproducibilityOptions extends VerifyOptions {
|
|
77
|
+
runs?: string;
|
|
78
|
+
interval?: string;
|
|
79
|
+
timeout?: string;
|
|
80
|
+
repoRoot?: string;
|
|
81
|
+
scope?: string;
|
|
82
|
+
planSource?: string;
|
|
83
|
+
}
|
|
84
|
+
export declare function runHasFrozenRiskPlan(run: VerificationRun): boolean;
|
|
85
|
+
export declare function verifyReproducibilityCommand(specimen: string, options: VerifyReproducibilityOptions): Promise<void>;
|
|
@@ -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>;
|