@haystackeditor/cli 0.15.31 → 0.16.0
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 +111 -60
- package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
- package/dist/assets/hooks/scripts/post-commit.sh +3 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
- package/dist/assets/hooks/scripts/pre-push.sh +3 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/commands/case-batch-contract.js +608 -0
- package/dist/commands/case-batch.js +713 -0
- package/dist/commands/cloud-verifier-identity-census.js +5 -2
- package/dist/commands/combination-search-hook.js +139 -0
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/hooks.js +66 -7
- package/dist/commands/install-session-hooks.js +131 -44
- package/dist/commands/policy.js +32 -45
- package/dist/commands/precompute-delivery.js +6 -1
- package/dist/commands/scaffold-provisional-universe.js +8 -10
- package/dist/commands/setup.js +32 -10
- package/dist/commands/submit.js +7 -2
- package/dist/commands/telemetry.js +17 -2
- package/dist/commands/triage.js +2 -1
- package/dist/commands/verify-explore.js +1 -0
- package/dist/commands/verify-hosted-mcp.js +3 -12
- package/dist/commands/verify-hosted-reproducibility.js +49 -547
- package/dist/commands/verify-hosted.js +66 -168
- package/dist/commands/verify-precompute.js +57 -10
- package/dist/commands/verify.js +51 -139
- package/dist/index.js +238 -350
- package/dist/lazy.js +8 -0
- package/dist/schema.js +1 -0
- package/dist/tools/detect.js +3 -24
- package/dist/triage/prompts.js +46 -45
- package/dist/triage/runner.js +119 -33
- package/dist/types.js +3 -3
- package/dist/utils/auth.js +14 -2
- package/dist/utils/git.js +60 -29
- package/dist/utils/github-api.js +14 -1
- package/dist/utils/haystack-api.js +26 -7
- package/dist/utils/hooks.js +43 -6
- package/dist/utils/prompter.js +25 -10
- package/dist/utils/safe-write.js +31 -0
- package/dist/utils/secret-paths.js +115 -0
- package/dist/utils/secrets.js +0 -1
- package/dist/utils/telemetry.js +34 -14
- package/dist/utils/update-check.js +151 -0
- package/package.json +18 -14
- package/schemas/case-batch.v1.json +184 -0
- package/schemas/cloud-verifier.v1.json +55 -100
- package/dist/commands/ask.d.ts +0 -14
- package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
- package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
- package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
- package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
- package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
- package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
- package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
- package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
- package/dist/commands/cloud-verifier-universe.d.ts +0 -31
- package/dist/commands/config.d.ts +0 -46
- package/dist/commands/design-verify.d.ts +0 -33
- package/dist/commands/dismiss.d.ts +0 -29
- package/dist/commands/hooks.d.ts +0 -13
- package/dist/commands/inbox.d.ts +0 -65
- package/dist/commands/init.d.ts +0 -10
- package/dist/commands/install-session-hooks.d.ts +0 -17
- package/dist/commands/login.d.ts +0 -8
- package/dist/commands/mcp.d.ts +0 -1
- package/dist/commands/policy.d.ts +0 -31
- package/dist/commands/pr-status.d.ts +0 -144
- package/dist/commands/pr.d.ts +0 -40
- package/dist/commands/precompute-delivery-contract.d.ts +0 -68
- package/dist/commands/precompute-delivery.d.ts +0 -20
- package/dist/commands/prepare-universe-review.d.ts +0 -115
- package/dist/commands/production-source-deny-policy.d.ts +0 -15
- package/dist/commands/request-review.d.ts +0 -26
- package/dist/commands/review.d.ts +0 -25
- package/dist/commands/rules.d.ts +0 -4
- package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
- package/dist/commands/schema-cmd.d.ts +0 -2
- package/dist/commands/setup.d.ts +0 -28
- package/dist/commands/skills.d.ts +0 -8
- package/dist/commands/status.d.ts +0 -4
- package/dist/commands/submit.d.ts +0 -30
- package/dist/commands/system-map.d.ts +0 -42
- package/dist/commands/telemetry.d.ts +0 -53
- package/dist/commands/tokens.d.ts +0 -14
- package/dist/commands/triage.d.ts +0 -35
- package/dist/commands/verify-core.d.ts +0 -449
- package/dist/commands/verify-core.js +0 -789
- package/dist/commands/verify-explore.d.ts +0 -14
- package/dist/commands/verify-history.d.ts +0 -14
- package/dist/commands/verify-hosted-mcp.d.ts +0 -12
- package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
- package/dist/commands/verify-hosted.d.ts +0 -92
- package/dist/commands/verify-mcp.d.ts +0 -8
- package/dist/commands/verify-mcp.js +0 -517
- package/dist/commands/verify-ops.d.ts +0 -158
- package/dist/commands/verify-ops.js +0 -1148
- package/dist/commands/verify-precompute.d.ts +0 -30
- package/dist/commands/verify-reproducibility.d.ts +0 -85
- package/dist/commands/verify-reproducibility.js +0 -494
- package/dist/commands/verify-reseal.d.ts +0 -9
- package/dist/commands/verify-reseal.js +0 -148
- package/dist/commands/verify-sandboxes.d.ts +0 -95
- package/dist/commands/verify-sandboxes.js +0 -352
- package/dist/commands/verify.d.ts +0 -28
- package/dist/commands/webhooks.d.ts +0 -30
- package/dist/index.d.ts +0 -22
- package/dist/schema.d.ts +0 -28
- package/dist/states.d.ts +0 -29
- package/dist/tools/detect.d.ts +0 -50
- package/dist/triage/prompts.d.ts +0 -24
- package/dist/triage/runner.d.ts +0 -34
- package/dist/triage/types.d.ts +0 -42
- package/dist/types/verify-history.d.ts +0 -38
- package/dist/types.d.ts +0 -1684
- package/dist/utils/action-output.d.ts +0 -24
- package/dist/utils/analysis-api.d.ts +0 -187
- package/dist/utils/auth.d.ts +0 -79
- package/dist/utils/config.d.ts +0 -24
- package/dist/utils/design-verifier-api.d.ts +0 -208
- package/dist/utils/design-verifier-history.d.ts +0 -46
- package/dist/utils/design-verifier-result.d.ts +0 -74
- package/dist/utils/detect.d.ts +0 -43
- package/dist/utils/git.d.ts +0 -135
- package/dist/utils/github-api.d.ts +0 -104
- package/dist/utils/haystack-api.d.ts +0 -37
- package/dist/utils/hooks.d.ts +0 -12
- package/dist/utils/pending-state.d.ts +0 -40
- package/dist/utils/pr-ref.d.ts +0 -27
- package/dist/utils/prompter.d.ts +0 -85
- package/dist/utils/secrets.d.ts +0 -47
- package/dist/utils/telemetry.d.ts +0 -19
- /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
|
@@ -2,569 +2,71 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { withSchema } from '../schema.js';
|
|
4
4
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
5
|
-
import { hostedPollingOptions, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const SERIES_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/;
|
|
12
|
-
const TERMINAL_STATUSES = new Set([
|
|
13
|
-
'complete',
|
|
14
|
-
'errored',
|
|
15
|
-
'expired',
|
|
16
|
-
'terminated',
|
|
17
|
-
]);
|
|
18
|
-
const ASSESSMENT_STATUSES = new Set([
|
|
19
|
-
'improvement',
|
|
20
|
-
'inconclusive',
|
|
21
|
-
'regression',
|
|
22
|
-
'unchanged',
|
|
23
|
-
]);
|
|
24
|
-
const ASSESSMENT_REASON_CODES = new Set([
|
|
25
|
-
'case_universe_changed',
|
|
26
|
-
'coverage_increased',
|
|
27
|
-
'coverage_reduced',
|
|
28
|
-
'inconclusive',
|
|
29
|
-
'mixed_outcomes',
|
|
30
|
-
'new_failures',
|
|
31
|
-
'no_bound_driver',
|
|
32
|
-
'resolved_failures',
|
|
33
|
-
'unchanged',
|
|
34
|
-
]);
|
|
35
|
-
const OVERALL_ASSESSMENT_STATUSES = new Set([
|
|
36
|
-
'inconclusive',
|
|
37
|
-
'no_regressions_detected',
|
|
38
|
-
'regressions_detected',
|
|
39
|
-
]);
|
|
40
|
-
const DEFAULT_RUNS = 3;
|
|
41
|
-
const MAX_RUNS = 10;
|
|
42
|
-
function record(value, label) {
|
|
43
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
44
|
-
throw new Error(`${label} is malformed`);
|
|
5
|
+
import { buildHostedJson, hostedPollingOptions, hostedResumeCommand, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
6
|
+
const TERMINAL_STATUSES = new Set(['complete', 'errored', 'expired', 'terminated']);
|
|
7
|
+
export async function verifyHostedReproducibilityCommand(repositoryArg, options) {
|
|
8
|
+
const repository = parseHostedRepository(repositoryArg);
|
|
9
|
+
if (!/^[0-9a-f]{40}$/.test(options.base ?? '') || !/^[0-9a-f]{40}$/.test(options.head ?? '')) {
|
|
10
|
+
throw new Error('--base and --head must be exact 40-character lowercase commit SHAs.');
|
|
45
11
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error(`${label} is malformed`);
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
function boundedInteger(value, fallback, label, minimum, maximum) {
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
return fallback;
|
|
56
|
-
const parsed = Number(value);
|
|
57
|
-
if (!Number.isSafeInteger(parsed)
|
|
58
|
-
|| parsed < minimum
|
|
59
|
-
|| parsed > maximum) {
|
|
60
|
-
throw new Error(`${label} must be an integer from ${minimum} through ${maximum}.`);
|
|
12
|
+
const intent = options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
|
|
13
|
+
const requestedRuns = options.runs === undefined ? 3 : Number(options.runs);
|
|
14
|
+
if (!Number.isInteger(requestedRuns) || requestedRuns < 2 || requestedRuns > 10) {
|
|
15
|
+
throw new Error('--runs must be an integer from 2 through 10.');
|
|
61
16
|
}
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
function requireSeriesKey(value) {
|
|
65
|
-
const key = value
|
|
66
|
-
?? `series:${new Date().toISOString().replace(/[^0-9]/g, '').slice(0, 14)}:${randomBytes(6).toString('hex')}`;
|
|
67
|
-
if (!SERIES_KEY.test(key)) {
|
|
17
|
+
const seriesKey = options.seriesKey ?? `series:${randomBytes(12).toString('hex')}`;
|
|
18
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/.test(seriesKey)) {
|
|
68
19
|
throw new Error('--series-key must be 8-64 characters and use only letters, numbers, ".", "_", ":", or "-".');
|
|
69
20
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
export function hostedReproducibilityRunKey(seriesKey, repository, baseCommit, headCommit, ordinal, mode = 'exact_plan_replay') {
|
|
73
|
-
const digest = createHash('sha256')
|
|
74
|
-
.update(JSON.stringify(mode === 'exact_plan_replay'
|
|
75
|
-
? [
|
|
76
|
-
seriesKey,
|
|
77
|
-
repository,
|
|
78
|
-
baseCommit,
|
|
79
|
-
headCommit,
|
|
80
|
-
ordinal,
|
|
81
|
-
]
|
|
82
|
-
: [
|
|
83
|
-
mode,
|
|
84
|
-
seriesKey,
|
|
85
|
-
repository,
|
|
86
|
-
baseCommit,
|
|
87
|
-
headCommit,
|
|
88
|
-
ordinal,
|
|
89
|
-
]))
|
|
90
|
-
.digest('hex');
|
|
91
|
-
return `repro:${digest}`;
|
|
92
|
-
}
|
|
93
|
-
function stable(value) {
|
|
94
|
-
if (Array.isArray(value))
|
|
95
|
-
return value.map(stable);
|
|
96
|
-
if (value && typeof value === 'object') {
|
|
97
|
-
const object = value;
|
|
98
|
-
return Object.fromEntries(Object.keys(object)
|
|
99
|
-
.sort()
|
|
100
|
-
.map(key => [key, stable(object[key])]));
|
|
101
|
-
}
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
|
-
function fingerprint(value) {
|
|
105
|
-
return createHash('sha256')
|
|
106
|
-
.update(JSON.stringify(stable(value)))
|
|
107
|
-
.digest('hex');
|
|
108
|
-
}
|
|
109
|
-
function expectedCellStatus(reasonCode) {
|
|
110
|
-
if (reasonCode === 'new_failures'
|
|
111
|
-
|| reasonCode === 'coverage_reduced'
|
|
112
|
-
|| reasonCode === 'mixed_outcomes') {
|
|
113
|
-
return 'regression';
|
|
114
|
-
}
|
|
115
|
-
if (reasonCode === 'resolved_failures'
|
|
116
|
-
|| reasonCode === 'coverage_increased') {
|
|
117
|
-
return 'improvement';
|
|
118
|
-
}
|
|
119
|
-
if (reasonCode === 'unchanged')
|
|
120
|
-
return 'unchanged';
|
|
121
|
-
return 'inconclusive';
|
|
122
|
-
}
|
|
123
|
-
function expectedOverallStatus(outcomes) {
|
|
124
|
-
if (outcomes.some(outcome => outcome.status === 'regression')) {
|
|
125
|
-
return 'regressions_detected';
|
|
126
|
-
}
|
|
127
|
-
if (outcomes.length === 0
|
|
128
|
-
|| outcomes.some(outcome => outcome.status === 'inconclusive')) {
|
|
129
|
-
return 'inconclusive';
|
|
130
|
-
}
|
|
131
|
-
return 'no_regressions_detected';
|
|
132
|
-
}
|
|
133
|
-
function requireCount(receipt, field, expected) {
|
|
134
|
-
if (receipt[field] !== expected) {
|
|
135
|
-
throw new Error(`driver assessment ${field} is inconsistent`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function parseAssessment(state, identity) {
|
|
139
|
-
if (state.status !== 'complete' || !state.output) {
|
|
140
|
-
throw new Error(`run ended with status ${state.status}`);
|
|
141
|
-
}
|
|
142
|
-
if (state.repository !== identity.repository
|
|
143
|
-
|| state.baseCommit !== identity.baseCommit
|
|
144
|
-
|| state.headCommit !== identity.headCommit) {
|
|
145
|
-
throw new Error('run identity does not match the requested exact launch');
|
|
146
|
-
}
|
|
147
|
-
const output = record(state.output, 'terminal output');
|
|
148
|
-
if ((output.schemaVersion !== 8 && output.schemaVersion !== 9)
|
|
149
|
-
|| output.status !== 'driver_assessment_recorded') {
|
|
150
|
-
throw new Error('run did not produce a schema-8 or schema-9 driver assessment');
|
|
151
|
-
}
|
|
152
|
-
const riskPlan = record(output.riskPlanReceipt, 'risk plan receipt');
|
|
153
|
-
const riskPlanSha256 = fingerprint(riskPlan);
|
|
154
|
-
let replaySourceRunId = null;
|
|
155
|
-
let replaySourceResultSchemaVersion = null;
|
|
156
|
-
if (output.schemaVersion === 9) {
|
|
157
|
-
if (output.planningCredentialDelivery !== 'not-required-plan-replay') {
|
|
158
|
-
throw new Error('schema-9 replay used an invalid planning credential mode');
|
|
159
|
-
}
|
|
160
|
-
const replay = record(output.riskPlanReplayReceipt, 'risk plan replay receipt');
|
|
161
|
-
const fields = Object.keys(replay);
|
|
162
|
-
if (fields.length !== 6
|
|
163
|
-
|| fields.some(field => ![
|
|
164
|
-
'schemaVersion',
|
|
165
|
-
'sourceContentPersisted',
|
|
166
|
-
'sourceResultSchemaVersion',
|
|
167
|
-
'sourceRiskPlanReceiptSha256',
|
|
168
|
-
'sourceRunId',
|
|
169
|
-
'status',
|
|
170
|
-
].includes(field))
|
|
171
|
-
|| replay.schemaVersion !== 1
|
|
172
|
-
|| replay.status !== 'risk_plan_replay_verified'
|
|
173
|
-
|| typeof replay.sourceRunId !== 'string'
|
|
174
|
-
|| !RUN_ID.test(replay.sourceRunId)
|
|
175
|
-
|| (replay.sourceResultSchemaVersion !== 8
|
|
176
|
-
&& replay.sourceResultSchemaVersion !== 9)
|
|
177
|
-
|| replay.sourceRiskPlanReceiptSha256 !== riskPlanSha256
|
|
178
|
-
|| typeof replay.sourceRiskPlanReceiptSha256 !== 'string'
|
|
179
|
-
|| !SHA256.test(replay.sourceRiskPlanReceiptSha256)
|
|
180
|
-
|| replay.sourceContentPersisted !== false) {
|
|
181
|
-
throw new Error('risk plan replay receipt is malformed');
|
|
182
|
-
}
|
|
183
|
-
replaySourceRunId = replay.sourceRunId;
|
|
184
|
-
replaySourceResultSchemaVersion = replay.sourceResultSchemaVersion;
|
|
185
|
-
}
|
|
186
|
-
else if (output.riskPlanReplayReceipt !== undefined) {
|
|
187
|
-
throw new Error('schema-8 result contains unversioned replay provenance');
|
|
188
|
-
}
|
|
189
|
-
else if (output.planningCredentialDelivery
|
|
190
|
-
!== 'per-command-env-gateway-placeholder') {
|
|
191
|
-
throw new Error('schema-8 result used an invalid planning credential mode');
|
|
192
|
-
}
|
|
193
|
-
const assessment = record(output.driverAssessmentReceipt, 'driver assessment receipt');
|
|
194
|
-
const planCells = array(riskPlan.cells, 'risk plan cells');
|
|
195
|
-
const assessmentCells = array(assessment.cells, 'assessment cells');
|
|
196
|
-
if (riskPlan.schemaVersion !== 2
|
|
197
|
-
|| riskPlan.status !== 'risk_plan_ready'
|
|
198
|
-
|| riskPlan.baseCommit !== identity.baseCommit
|
|
199
|
-
|| riskPlan.headCommit !== identity.headCommit
|
|
200
|
-
|| riskPlan.sourceContentPersisted !== false) {
|
|
201
|
-
throw new Error('risk plan receipt identity is malformed');
|
|
202
|
-
}
|
|
203
|
-
const planIdentities = planCells.map((raw) => {
|
|
204
|
-
const cell = record(raw, 'risk plan cell');
|
|
205
|
-
if (typeof cell.cellId !== 'string'
|
|
206
|
-
|| !CELL_ID.test(cell.cellId)
|
|
207
|
-
|| (cell.capabilityId !== null
|
|
208
|
-
&& (typeof cell.capabilityId !== 'string'
|
|
209
|
-
|| !CAPABILITY_ID.test(cell.capabilityId)))) {
|
|
210
|
-
throw new Error('risk plan cell identity is malformed');
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
213
|
-
cellId: cell.cellId,
|
|
214
|
-
capabilityId: cell.capabilityId,
|
|
215
|
-
};
|
|
216
|
-
}).sort((left, right) => left.cellId.localeCompare(right.cellId));
|
|
217
|
-
const cellIds = planIdentities.map(cell => cell.cellId);
|
|
218
|
-
if (cellIds.length === 0
|
|
219
|
-
|| new Set(cellIds).size !== cellIds.length
|
|
220
|
-
|| assessmentCells.length !== cellIds.length) {
|
|
221
|
-
throw new Error('risk plan and assessment cell sets are inconsistent');
|
|
222
|
-
}
|
|
223
|
-
if (assessment.schemaVersion !== 1
|
|
224
|
-
|| typeof assessment.status !== 'string'
|
|
225
|
-
|| !OVERALL_ASSESSMENT_STATUSES.has(assessment.status)
|
|
226
|
-
|| assessment.baseCommit !== identity.baseCommit
|
|
227
|
-
|| assessment.headCommit !== identity.headCommit
|
|
228
|
-
|| typeof assessment.driverComparisonReceiptSha256 !== 'string'
|
|
229
|
-
|| !SHA256.test(assessment.driverComparisonReceiptSha256)
|
|
230
|
-
|| assessment.sourceContentPersisted !== false) {
|
|
231
|
-
throw new Error('driver assessment receipt identity is malformed');
|
|
232
|
-
}
|
|
233
|
-
const outcomes = assessmentCells.map((raw) => {
|
|
234
|
-
const cell = record(raw, 'assessment cell');
|
|
235
|
-
if (typeof cell.cellId !== 'string'
|
|
236
|
-
|| !CELL_ID.test(cell.cellId)
|
|
237
|
-
|| (cell.capabilityId !== null
|
|
238
|
-
&& (typeof cell.capabilityId !== 'string'
|
|
239
|
-
|| !CAPABILITY_ID.test(cell.capabilityId)))
|
|
240
|
-
|| (cell.comparisonId !== null
|
|
241
|
-
&& (typeof cell.comparisonId !== 'string'
|
|
242
|
-
|| !COMPARISON_ID.test(cell.comparisonId)))
|
|
243
|
-
|| typeof cell.status !== 'string'
|
|
244
|
-
|| !ASSESSMENT_STATUSES.has(cell.status)
|
|
245
|
-
|| typeof cell.reasonCode !== 'string'
|
|
246
|
-
|| !ASSESSMENT_REASON_CODES.has(cell.reasonCode)
|
|
247
|
-
|| cell.status !== expectedCellStatus(cell.reasonCode)
|
|
248
|
-
|| (cell.capabilityId === null
|
|
249
|
-
? (cell.comparisonId !== null
|
|
250
|
-
|| cell.reasonCode !== 'no_bound_driver')
|
|
251
|
-
: (cell.comparisonId === null
|
|
252
|
-
|| cell.reasonCode === 'no_bound_driver'))) {
|
|
253
|
-
throw new Error('assessment cell outcome is malformed');
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
cellId: cell.cellId,
|
|
257
|
-
capabilityId: cell.capabilityId,
|
|
258
|
-
comparisonId: cell.comparisonId,
|
|
259
|
-
status: cell.status,
|
|
260
|
-
reasonCode: cell.reasonCode,
|
|
261
|
-
};
|
|
262
|
-
}).sort((left, right) => left.cellId.localeCompare(right.cellId));
|
|
263
|
-
if (new Set(outcomes.map(outcome => outcome.cellId)).size !== outcomes.length
|
|
264
|
-
|| outcomes.some((outcome, index) => outcome.cellId !== cellIds[index]
|
|
265
|
-
|| outcome.capabilityId !== planIdentities[index].capabilityId)) {
|
|
266
|
-
throw new Error('risk plan and assessment cell identities do not match');
|
|
267
|
-
}
|
|
268
|
-
const counts = {
|
|
269
|
-
comparedCellCount: outcomes.filter(outcome => outcome.status !== 'inconclusive').length,
|
|
270
|
-
regressionCellCount: outcomes.filter(outcome => outcome.status === 'regression').length,
|
|
271
|
-
improvementCellCount: outcomes.filter(outcome => outcome.status === 'improvement').length,
|
|
272
|
-
unchangedCellCount: outcomes.filter(outcome => outcome.status === 'unchanged').length,
|
|
273
|
-
inconclusiveCellCount: outcomes.filter(outcome => outcome.status === 'inconclusive').length,
|
|
274
|
-
};
|
|
275
|
-
requireCount(assessment, 'plannedCellCount', outcomes.length);
|
|
276
|
-
for (const [field, count] of Object.entries(counts)) {
|
|
277
|
-
requireCount(assessment, field, count);
|
|
278
|
-
}
|
|
279
|
-
if (assessment.status !== expectedOverallStatus(outcomes)) {
|
|
280
|
-
throw new Error('driver assessment overall status is inconsistent');
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
schemaVersion: output.schemaVersion,
|
|
284
|
-
riskPlanSha256,
|
|
285
|
-
replaySourceRunId,
|
|
286
|
-
replaySourceResultSchemaVersion,
|
|
287
|
-
planIdentities,
|
|
288
|
-
cellIds,
|
|
289
|
-
status: assessment.status,
|
|
290
|
-
outcomes,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function groupedPlanVariants(parsed) {
|
|
294
|
-
const variants = new Map();
|
|
295
|
-
for (const item of parsed) {
|
|
296
|
-
const digest = fingerprint(item.assessment.planIdentities);
|
|
297
|
-
const existing = variants.get(digest);
|
|
298
|
-
if (existing) {
|
|
299
|
-
existing.run_ids.push(item.runId);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
variants.set(digest, {
|
|
303
|
-
fingerprint: digest,
|
|
304
|
-
run_ids: [item.runId],
|
|
305
|
-
cell_ids: item.assessment.cellIds,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return [...variants.values()].sort((left, right) => left.fingerprint.localeCompare(right.fingerprint));
|
|
310
|
-
}
|
|
311
|
-
function groupedOutcomeVariants(parsed) {
|
|
312
|
-
const variants = new Map();
|
|
313
|
-
for (const item of parsed) {
|
|
314
|
-
const digest = fingerprint({
|
|
315
|
-
status: item.assessment.status,
|
|
316
|
-
outcomes: item.assessment.outcomes,
|
|
317
|
-
});
|
|
318
|
-
const existing = variants.get(digest);
|
|
319
|
-
if (existing) {
|
|
320
|
-
existing.run_ids.push(item.runId);
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
variants.set(digest, {
|
|
324
|
-
fingerprint: digest,
|
|
325
|
-
run_ids: [item.runId],
|
|
326
|
-
assessment_status: item.assessment.status,
|
|
327
|
-
outcomes: item.assessment.outcomes,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return [...variants.values()].sort((left, right) => left.fingerprint.localeCompare(right.fingerprint));
|
|
332
|
-
}
|
|
333
|
-
function driftIds(parsed, values) {
|
|
334
|
-
const byCell = new Map();
|
|
335
|
-
for (const [runIndex, item] of parsed.entries()) {
|
|
336
|
-
const present = values(item.assessment);
|
|
337
|
-
for (const [cellId, observed] of byCell) {
|
|
338
|
-
observed.add(present.has(cellId)
|
|
339
|
-
? JSON.stringify(present.get(cellId))
|
|
340
|
-
: '__missing__');
|
|
341
|
-
}
|
|
342
|
-
for (const [cellId, value] of present) {
|
|
343
|
-
if (byCell.has(cellId))
|
|
344
|
-
continue;
|
|
345
|
-
const observed = new Set();
|
|
346
|
-
if (runIndex > 0)
|
|
347
|
-
observed.add('__missing__');
|
|
348
|
-
observed.add(JSON.stringify(value));
|
|
349
|
-
byCell.set(cellId, observed);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return [...byCell.entries()]
|
|
353
|
-
.filter(([, observed]) => observed.size > 1)
|
|
354
|
-
.map(([cellId]) => cellId)
|
|
355
|
-
.sort();
|
|
356
|
-
}
|
|
357
|
-
export function analyzeHostedReproducibility(runs, identity) {
|
|
358
|
-
const parsed = [];
|
|
359
|
-
const unrunnableRuns = [];
|
|
360
|
-
for (const run of runs) {
|
|
361
|
-
if (run.timedOut) {
|
|
362
|
-
unrunnableRuns.push({
|
|
363
|
-
run_id: run.state.runId,
|
|
364
|
-
status: run.state.status,
|
|
365
|
-
reason: 'wait_timed_out',
|
|
366
|
-
});
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
parsed.push({
|
|
371
|
-
runId: run.state.runId,
|
|
372
|
-
assessment: parseAssessment(run.state, identity),
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
unrunnableRuns.push({
|
|
377
|
-
run_id: run.state.runId,
|
|
378
|
-
status: run.state.status,
|
|
379
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const requestedRuns = identity.requestedRuns ?? runs.length;
|
|
384
|
-
const allRequestedRunsLaunched = runs.length === requestedRuns;
|
|
385
|
-
const planVariants = groupedPlanVariants(parsed);
|
|
386
|
-
const assessmentVariants = groupedOutcomeVariants(parsed);
|
|
387
|
-
const mode = identity.mode ?? 'exact_plan_replay';
|
|
388
|
-
const replaySourceRunId = mode === 'exact_plan_replay'
|
|
389
|
-
? parsed[0]?.runId ?? null
|
|
390
|
-
: null;
|
|
391
|
-
const planReplayVerified = mode === 'exact_plan_replay'
|
|
392
|
-
&& allRequestedRunsLaunched
|
|
393
|
-
&& parsed.length === runs.length
|
|
394
|
-
&& parsed.length > 1
|
|
395
|
-
&& parsed[0]?.assessment.schemaVersion === 8
|
|
396
|
-
&& parsed[0]?.assessment.replaySourceRunId === null
|
|
397
|
-
&& parsed.slice(1).every(item => item.assessment.schemaVersion === 9
|
|
398
|
-
&& item.assessment.replaySourceRunId === replaySourceRunId
|
|
399
|
-
&& item.assessment.replaySourceResultSchemaVersion
|
|
400
|
-
=== parsed[0].assessment.schemaVersion
|
|
401
|
-
&& item.assessment.riskPlanSha256
|
|
402
|
-
=== parsed[0].assessment.riskPlanSha256);
|
|
403
|
-
const plannerReproducibilityTested = mode === 'fresh_plans' && parsed.length > 1;
|
|
404
|
-
const plannerReproducible = plannerReproducibilityTested
|
|
405
|
-
&& allRequestedRunsLaunched
|
|
406
|
-
&& parsed.length === runs.length
|
|
407
|
-
&& parsed.every(item => item.assessment.schemaVersion === 8
|
|
408
|
-
&& item.assessment.replaySourceRunId === null)
|
|
409
|
-
&& planVariants.length === 1;
|
|
410
|
-
const assessmentReproducible = (mode === 'exact_plan_replay'
|
|
411
|
-
? planReplayVerified
|
|
412
|
-
: plannerReproducible)
|
|
413
|
-
&& assessmentVariants.length === 1;
|
|
414
|
-
const planDriftCellIds = driftIds(parsed, assessment => new Map(assessment.planIdentities.map(plan => [
|
|
415
|
-
plan.cellId,
|
|
416
|
-
{ capabilityId: plan.capabilityId },
|
|
417
|
-
])));
|
|
418
|
-
const assessmentDriftCellIds = driftIds(parsed, assessment => new Map(assessment.outcomes.map(outcome => [
|
|
419
|
-
outcome.cellId,
|
|
420
|
-
{
|
|
421
|
-
capabilityId: outcome.capabilityId,
|
|
422
|
-
comparisonId: outcome.comparisonId,
|
|
423
|
-
status: outcome.status,
|
|
424
|
-
reasonCode: outcome.reasonCode,
|
|
425
|
-
},
|
|
426
|
-
])));
|
|
427
|
-
const inconclusiveCellIds = [...new Set(parsed.flatMap(item => item.assessment.outcomes
|
|
428
|
-
.filter(outcome => outcome.status === 'inconclusive')
|
|
429
|
-
.map(outcome => outcome.cellId)))].sort();
|
|
430
|
-
return {
|
|
431
|
-
series_key: identity.seriesKey,
|
|
432
|
-
repository: identity.repository,
|
|
433
|
-
base_commit: identity.baseCommit,
|
|
434
|
-
head_commit: identity.headCommit,
|
|
435
|
-
reproducibility_mode: mode,
|
|
436
|
-
requested_runs: requestedRuns,
|
|
437
|
-
launched_runs: runs.length,
|
|
438
|
-
unstarted_runs: Math.max(0, requestedRuns - runs.length),
|
|
439
|
-
analyzable_runs: parsed.length,
|
|
440
|
-
run_ids: runs.map(run => run.state.runId),
|
|
441
|
-
replay_source_run_id: replaySourceRunId,
|
|
442
|
-
plan_replay_verified: planReplayVerified,
|
|
443
|
-
planner_reproducibility_tested: plannerReproducibilityTested,
|
|
444
|
-
planner_reproducible: plannerReproducible,
|
|
445
|
-
assessment_reproducible: assessmentReproducible,
|
|
446
|
-
reproducible: (mode === 'exact_plan_replay'
|
|
447
|
-
? planReplayVerified
|
|
448
|
-
: plannerReproducible)
|
|
449
|
-
&& assessmentReproducible
|
|
450
|
-
&& inconclusiveCellIds.length === 0,
|
|
451
|
-
plan_variants: planVariants,
|
|
452
|
-
assessment_variants: assessmentVariants,
|
|
453
|
-
plan_drift_cell_ids: planDriftCellIds,
|
|
454
|
-
assessment_drift_cell_ids: assessmentDriftCellIds,
|
|
455
|
-
inconclusive_cell_ids: inconclusiveCellIds,
|
|
456
|
-
unrunnable_runs: unrunnableRuns,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
export function isHostedReproducibilityReplaySource(run, identity) {
|
|
460
|
-
if (run.timedOut || run.state.status !== 'complete')
|
|
461
|
-
return false;
|
|
462
|
-
try {
|
|
463
|
-
const assessment = parseAssessment(run.state, identity);
|
|
464
|
-
return assessment.schemaVersion === 8
|
|
465
|
-
&& assessment.replaySourceRunId === null;
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
function renderHostedReproducibility(report) {
|
|
472
|
-
const icon = report.reproducible ? chalk.green('✓') : chalk.yellow('!');
|
|
473
|
-
console.log(`${icon} Hosted reproducibility: ${report.reproducible ? 'reproducible' : 'not established'}`);
|
|
474
|
-
console.log(` ${chalk.dim('Runs:')} ${report.analyzable_runs}/${report.requested_runs} analyzable; ${report.launched_runs} launched`);
|
|
475
|
-
console.log(` ${chalk.dim('Mode:')} ${report.reproducibility_mode === 'fresh_plans'
|
|
476
|
-
? 'independent fresh plans'
|
|
477
|
-
: 'exact plan replay'}`);
|
|
478
|
-
console.log(` ${chalk.dim('Plan variants:')} ${report.plan_variants.length}`);
|
|
479
|
-
console.log(` ${chalk.dim('Exact plan replay:')} ${report.reproducibility_mode === 'fresh_plans'
|
|
480
|
-
? 'not requested'
|
|
481
|
-
: report.plan_replay_verified
|
|
482
|
-
? 'verified'
|
|
483
|
-
: 'not verified'}`);
|
|
484
|
-
console.log(` ${chalk.dim('Independent planner reproducibility:')} ${report.planner_reproducibility_tested
|
|
485
|
-
? report.planner_reproducible
|
|
486
|
-
? 'verified'
|
|
487
|
-
: 'not reproducible'
|
|
488
|
-
: 'not tested'}`);
|
|
489
|
-
console.log(` ${chalk.dim('Assessment variants:')} ${report.assessment_variants.length}`);
|
|
490
|
-
if (report.plan_drift_cell_ids.length > 0) {
|
|
491
|
-
console.log(` ${chalk.dim('Plan drift:')} ${report.plan_drift_cell_ids.length} stable cell id(s)`);
|
|
492
|
-
}
|
|
493
|
-
if (report.assessment_drift_cell_ids.length > 0) {
|
|
494
|
-
console.log(` ${chalk.dim('Assessment drift:')} ${report.assessment_drift_cell_ids.length} cell(s)`);
|
|
495
|
-
}
|
|
496
|
-
if (report.inconclusive_cell_ids.length > 0) {
|
|
497
|
-
console.log(` ${chalk.dim('Inconclusive:')} ${report.inconclusive_cell_ids.length} cell(s)`);
|
|
498
|
-
}
|
|
499
|
-
if (report.unrunnable_runs.length > 0) {
|
|
500
|
-
console.log(` ${chalk.dim('Unrunnable:')} ${report.unrunnable_runs.length} run(s)`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
export async function verifyHostedReproducibilityCommand(repositoryArg, options) {
|
|
504
|
-
const repository = parseHostedRepository(repositoryArg);
|
|
505
|
-
// A reproducibility series reuses one explicit agent intent for every run.
|
|
506
|
-
readHostedIntentFile(options.intentFile);
|
|
507
|
-
const requestedRuns = boundedInteger(options.runs, DEFAULT_RUNS, '--runs', 2, MAX_RUNS);
|
|
508
|
-
const seriesKey = requireSeriesKey(options.seriesKey);
|
|
509
|
-
const mode = options.freshPlans
|
|
510
|
-
? 'fresh_plans'
|
|
511
|
-
: 'exact_plan_replay';
|
|
21
|
+
const polling = hostedPollingOptions(options.interval, options.timeout);
|
|
512
22
|
const auth = await resolveAuthContext({
|
|
513
23
|
preferredLogin: options.account,
|
|
514
24
|
owner: repository.owner,
|
|
515
25
|
repo: repository.repository,
|
|
516
26
|
});
|
|
27
|
+
const account = auth.token.startsWith('hsk_live_') ? undefined : auth.login;
|
|
517
28
|
console.error(chalk.dim(`Hosted reproducibility series: ${seriesKey}`));
|
|
518
|
-
const
|
|
29
|
+
const report = {
|
|
30
|
+
series_key: seriesKey,
|
|
31
|
+
repository: repository.fullName,
|
|
32
|
+
base_commit: options.base,
|
|
33
|
+
head_commit: options.head,
|
|
34
|
+
requested_runs: requestedRuns,
|
|
35
|
+
runs: [],
|
|
36
|
+
reproducible: false,
|
|
37
|
+
reason: 'Independent fleet plans do not share an execution identity, so reproducibility is not established.',
|
|
38
|
+
};
|
|
519
39
|
for (let index = 0; index < requestedRuns; index += 1) {
|
|
520
|
-
if (!options.json)
|
|
521
|
-
console.error(chalk.dim(`Starting
|
|
522
|
-
|
|
40
|
+
if (!options.json)
|
|
41
|
+
console.error(chalk.dim(`Starting fleet run ${index + 1}/${requestedRuns}…`));
|
|
42
|
+
const digest = createHash('sha256').update(JSON.stringify([
|
|
43
|
+
'haystack', seriesKey, repository.fullName, options.base, options.head, intent ?? null, index + 1,
|
|
44
|
+
])).digest('hex');
|
|
523
45
|
const started = await startHostedVerification(repositoryArg, {
|
|
46
|
+
provider: 'haystack',
|
|
524
47
|
base: options.base,
|
|
525
48
|
head: options.head,
|
|
526
49
|
intentFile: options.intentFile,
|
|
527
|
-
idempotencyKey:
|
|
528
|
-
replayPlanFrom: options.freshPlans || index === 0
|
|
529
|
-
? undefined
|
|
530
|
-
: runs[0]?.state.runId,
|
|
50
|
+
idempotencyKey: `series:${digest}`,
|
|
531
51
|
wait: false,
|
|
532
52
|
}, auth.token);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
timedOut
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (!options.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
&& !isHostedReproducibilityReplaySource(runs[0], {
|
|
549
|
-
seriesKey,
|
|
550
|
-
repository: repository.fullName,
|
|
551
|
-
baseCommit: options.base,
|
|
552
|
-
headCommit: options.head,
|
|
553
|
-
})) {
|
|
554
|
-
if (!options.json) {
|
|
555
|
-
console.error(chalk.yellow('Source run is not replayable; later runs were not launched.'));
|
|
556
|
-
}
|
|
53
|
+
const { state, timedOut } = TERMINAL_STATUSES.has(started.state.status)
|
|
54
|
+
? { state: started.state, timedOut: false }
|
|
55
|
+
: await waitForHostedVerification(started.state.runId, auth.token, polling);
|
|
56
|
+
if (state.repository !== repository.fullName || state.baseCommit !== options.base || state.headCommit !== options.head) {
|
|
57
|
+
throw new Error('Hosted fleet run does not match the requested repository and exact commits.');
|
|
58
|
+
}
|
|
59
|
+
report.runs.push(buildHostedJson('status', state, {
|
|
60
|
+
account,
|
|
61
|
+
idempotencyKey: started.idempotencyKey,
|
|
62
|
+
timedOut,
|
|
63
|
+
nextCommand: TERMINAL_STATUSES.has(state.status) ? undefined : hostedResumeCommand(state.runId, account),
|
|
64
|
+
}));
|
|
65
|
+
if (!options.json)
|
|
66
|
+
console.log(` ${state.runId}: ${state.status}${timedOut ? ' (wait timed out)' : ''}`);
|
|
67
|
+
if (timedOut)
|
|
557
68
|
break;
|
|
558
|
-
}
|
|
559
69
|
}
|
|
560
|
-
const report = analyzeHostedReproducibility(runs, {
|
|
561
|
-
seriesKey,
|
|
562
|
-
repository: repository.fullName,
|
|
563
|
-
baseCommit: options.base,
|
|
564
|
-
headCommit: options.head,
|
|
565
|
-
requestedRuns,
|
|
566
|
-
mode,
|
|
567
|
-
});
|
|
568
70
|
if (options.json) {
|
|
569
71
|
process.stdout.write(`${JSON.stringify(withSchema('cloud-verifier', {
|
|
570
72
|
operation: 'reproducibility',
|
|
@@ -572,8 +74,8 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
|
|
|
572
74
|
}), null, 2)}\n`);
|
|
573
75
|
}
|
|
574
76
|
else {
|
|
575
|
-
|
|
77
|
+
console.log(`Fleet runs recorded: ${report.runs.length}/${report.requested_runs}`);
|
|
78
|
+
console.log(report.reason);
|
|
576
79
|
}
|
|
577
|
-
|
|
578
|
-
process.exitCode = 2;
|
|
80
|
+
process.exitCode = 2;
|
|
579
81
|
}
|