@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.
Files changed (133) hide show
  1. package/README.md +111 -60
  2. package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
  3. package/dist/assets/hooks/scripts/post-commit.sh +3 -0
  4. package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
  5. package/dist/assets/hooks/scripts/pre-push.sh +3 -0
  6. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  7. package/dist/commands/case-batch-contract.js +608 -0
  8. package/dist/commands/case-batch.js +713 -0
  9. package/dist/commands/cloud-verifier-identity-census.js +5 -2
  10. package/dist/commands/combination-search-hook.js +139 -0
  11. package/dist/commands/dismiss.js +2 -1
  12. package/dist/commands/hooks.js +66 -7
  13. package/dist/commands/install-session-hooks.js +131 -44
  14. package/dist/commands/policy.js +32 -45
  15. package/dist/commands/precompute-delivery.js +6 -1
  16. package/dist/commands/scaffold-provisional-universe.js +8 -10
  17. package/dist/commands/setup.js +32 -10
  18. package/dist/commands/submit.js +7 -2
  19. package/dist/commands/telemetry.js +17 -2
  20. package/dist/commands/triage.js +2 -1
  21. package/dist/commands/verify-explore.js +1 -0
  22. package/dist/commands/verify-hosted-mcp.js +3 -12
  23. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  24. package/dist/commands/verify-hosted.js +66 -168
  25. package/dist/commands/verify-precompute.js +57 -10
  26. package/dist/commands/verify.js +51 -139
  27. package/dist/index.js +238 -350
  28. package/dist/lazy.js +8 -0
  29. package/dist/schema.js +1 -0
  30. package/dist/tools/detect.js +3 -24
  31. package/dist/triage/prompts.js +46 -45
  32. package/dist/triage/runner.js +119 -33
  33. package/dist/types.js +3 -3
  34. package/dist/utils/auth.js +14 -2
  35. package/dist/utils/git.js +60 -29
  36. package/dist/utils/github-api.js +14 -1
  37. package/dist/utils/haystack-api.js +26 -7
  38. package/dist/utils/hooks.js +43 -6
  39. package/dist/utils/prompter.js +25 -10
  40. package/dist/utils/safe-write.js +31 -0
  41. package/dist/utils/secret-paths.js +115 -0
  42. package/dist/utils/secrets.js +0 -1
  43. package/dist/utils/telemetry.js +34 -14
  44. package/dist/utils/update-check.js +151 -0
  45. package/package.json +18 -14
  46. package/schemas/case-batch.v1.json +184 -0
  47. package/schemas/cloud-verifier.v1.json +55 -100
  48. package/dist/commands/ask.d.ts +0 -14
  49. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  50. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  51. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  52. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  53. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  54. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  55. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  56. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  57. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  58. package/dist/commands/config.d.ts +0 -46
  59. package/dist/commands/design-verify.d.ts +0 -33
  60. package/dist/commands/dismiss.d.ts +0 -29
  61. package/dist/commands/hooks.d.ts +0 -13
  62. package/dist/commands/inbox.d.ts +0 -65
  63. package/dist/commands/init.d.ts +0 -10
  64. package/dist/commands/install-session-hooks.d.ts +0 -17
  65. package/dist/commands/login.d.ts +0 -8
  66. package/dist/commands/mcp.d.ts +0 -1
  67. package/dist/commands/policy.d.ts +0 -31
  68. package/dist/commands/pr-status.d.ts +0 -144
  69. package/dist/commands/pr.d.ts +0 -40
  70. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  71. package/dist/commands/precompute-delivery.d.ts +0 -20
  72. package/dist/commands/prepare-universe-review.d.ts +0 -115
  73. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  74. package/dist/commands/request-review.d.ts +0 -26
  75. package/dist/commands/review.d.ts +0 -25
  76. package/dist/commands/rules.d.ts +0 -4
  77. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  78. package/dist/commands/schema-cmd.d.ts +0 -2
  79. package/dist/commands/setup.d.ts +0 -28
  80. package/dist/commands/skills.d.ts +0 -8
  81. package/dist/commands/status.d.ts +0 -4
  82. package/dist/commands/submit.d.ts +0 -30
  83. package/dist/commands/system-map.d.ts +0 -42
  84. package/dist/commands/telemetry.d.ts +0 -53
  85. package/dist/commands/tokens.d.ts +0 -14
  86. package/dist/commands/triage.d.ts +0 -35
  87. package/dist/commands/verify-core.d.ts +0 -449
  88. package/dist/commands/verify-core.js +0 -789
  89. package/dist/commands/verify-explore.d.ts +0 -14
  90. package/dist/commands/verify-history.d.ts +0 -14
  91. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  92. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  93. package/dist/commands/verify-hosted.d.ts +0 -92
  94. package/dist/commands/verify-mcp.d.ts +0 -8
  95. package/dist/commands/verify-mcp.js +0 -517
  96. package/dist/commands/verify-ops.d.ts +0 -158
  97. package/dist/commands/verify-ops.js +0 -1148
  98. package/dist/commands/verify-precompute.d.ts +0 -30
  99. package/dist/commands/verify-reproducibility.d.ts +0 -85
  100. package/dist/commands/verify-reproducibility.js +0 -494
  101. package/dist/commands/verify-reseal.d.ts +0 -9
  102. package/dist/commands/verify-reseal.js +0 -148
  103. package/dist/commands/verify-sandboxes.d.ts +0 -95
  104. package/dist/commands/verify-sandboxes.js +0 -352
  105. package/dist/commands/verify.d.ts +0 -28
  106. package/dist/commands/webhooks.d.ts +0 -30
  107. package/dist/index.d.ts +0 -22
  108. package/dist/schema.d.ts +0 -28
  109. package/dist/states.d.ts +0 -29
  110. package/dist/tools/detect.d.ts +0 -50
  111. package/dist/triage/prompts.d.ts +0 -24
  112. package/dist/triage/runner.d.ts +0 -34
  113. package/dist/triage/types.d.ts +0 -42
  114. package/dist/types/verify-history.d.ts +0 -38
  115. package/dist/types.d.ts +0 -1684
  116. package/dist/utils/action-output.d.ts +0 -24
  117. package/dist/utils/analysis-api.d.ts +0 -187
  118. package/dist/utils/auth.d.ts +0 -79
  119. package/dist/utils/config.d.ts +0 -24
  120. package/dist/utils/design-verifier-api.d.ts +0 -208
  121. package/dist/utils/design-verifier-history.d.ts +0 -46
  122. package/dist/utils/design-verifier-result.d.ts +0 -74
  123. package/dist/utils/detect.d.ts +0 -43
  124. package/dist/utils/git.d.ts +0 -135
  125. package/dist/utils/github-api.d.ts +0 -104
  126. package/dist/utils/haystack-api.d.ts +0 -37
  127. package/dist/utils/hooks.d.ts +0 -12
  128. package/dist/utils/pending-state.d.ts +0 -40
  129. package/dist/utils/pr-ref.d.ts +0 -27
  130. package/dist/utils/prompter.d.ts +0 -85
  131. package/dist/utils/secrets.d.ts +0 -47
  132. package/dist/utils/telemetry.d.ts +0 -19
  133. /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 CELL_ID = /^cell_[0-9a-f]{32}$/;
7
- const CAPABILITY_ID = /^repository_driver_[0-9a-f]{64}$/;
8
- const COMPARISON_ID = /^driver_comparison_[0-9a-f]{64}$/;
9
- const RUN_ID = /^cv_[0-9a-f]{48}$/;
10
- const SHA256 = /^[0-9a-f]{64}$/;
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
- return value;
47
- }
48
- function array(value, label) {
49
- if (!Array.isArray(value))
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
- return parsed;
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
- return key;
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 runs = [];
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 hosted reproducibility run ${index + 1}/${requestedRuns}…`));
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: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1, mode),
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
- let state = started.state;
534
- let timedOut = false;
535
- if (!TERMINAL_STATUSES.has(state.status)) {
536
- const waited = await waitForHostedVerification(state.runId, auth.token, hostedPollingOptions(options.interval, options.timeout, current => {
537
- if (!options.json) {
538
- console.error(chalk.dim(`Run ${index + 1}/${requestedRuns}: ${current.status}…`));
539
- }
540
- }));
541
- state = waited.state;
542
- timedOut = waited.timedOut;
543
- }
544
- runs.push({ state, timedOut });
545
- if (!options.freshPlans
546
- &&
547
- index === 0
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
- renderHostedReproducibility(report);
77
+ console.log(`Fleet runs recorded: ${report.runs.length}/${report.requested_runs}`);
78
+ console.log(report.reason);
576
79
  }
577
- if (!report.reproducible)
578
- process.exitCode = 2;
80
+ process.exitCode = 2;
579
81
  }