@haystackeditor/cli 0.15.25 → 0.15.26

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.
@@ -4,11 +4,13 @@ export interface HostedReproducibilityOptions {
4
4
  head: string;
5
5
  runs?: string;
6
6
  seriesKey?: string;
7
+ freshPlans?: boolean;
7
8
  account?: string;
8
9
  interval?: string;
9
10
  timeout?: string;
10
11
  json?: boolean;
11
12
  }
13
+ type HostedReproducibilityMode = 'exact_plan_replay' | 'fresh_plans';
12
14
  interface HostedReproducibilityRun {
13
15
  state: HostedRunState;
14
16
  timedOut: boolean;
@@ -19,6 +21,7 @@ interface HostedReproducibilityIdentity {
19
21
  baseCommit: string;
20
22
  headCommit: string;
21
23
  requestedRuns?: number;
24
+ mode?: HostedReproducibilityMode;
22
25
  }
23
26
  interface ParsedAssessment {
24
27
  schemaVersion: 8 | 9;
@@ -60,6 +63,7 @@ export interface HostedReproducibilityReport {
60
63
  repository: string;
61
64
  base_commit: string;
62
65
  head_commit: string;
66
+ reproducibility_mode: HostedReproducibilityMode;
63
67
  requested_runs: number;
64
68
  launched_runs: number;
65
69
  unstarted_runs: number;
@@ -67,7 +71,7 @@ export interface HostedReproducibilityReport {
67
71
  run_ids: string[];
68
72
  replay_source_run_id: string | null;
69
73
  plan_replay_verified: boolean;
70
- planner_reproducibility_tested: false;
74
+ planner_reproducibility_tested: boolean;
71
75
  planner_reproducible: boolean;
72
76
  assessment_reproducible: boolean;
73
77
  reproducible: boolean;
@@ -78,7 +82,7 @@ export interface HostedReproducibilityReport {
78
82
  inconclusive_cell_ids: string[];
79
83
  unrunnable_runs: RunFailure[];
80
84
  }
81
- export declare function hostedReproducibilityRunKey(seriesKey: string, repository: string, baseCommit: string, headCommit: string, ordinal: number): string;
85
+ export declare function hostedReproducibilityRunKey(seriesKey: string, repository: string, baseCommit: string, headCommit: string, ordinal: number, mode?: HostedReproducibilityMode): string;
82
86
  export declare function analyzeHostedReproducibility(runs: HostedReproducibilityRun[], identity: HostedReproducibilityIdentity): HostedReproducibilityReport;
83
87
  export declare function isHostedReproducibilityReplaySource(run: HostedReproducibilityRun, identity: HostedReproducibilityIdentity): boolean;
84
88
  export declare function verifyHostedReproducibilityCommand(repositoryArg: string, options: HostedReproducibilityOptions): Promise<void>;
@@ -69,15 +69,24 @@ function requireSeriesKey(value) {
69
69
  }
70
70
  return key;
71
71
  }
72
- export function hostedReproducibilityRunKey(seriesKey, repository, baseCommit, headCommit, ordinal) {
72
+ export function hostedReproducibilityRunKey(seriesKey, repository, baseCommit, headCommit, ordinal, mode = 'exact_plan_replay') {
73
73
  const digest = createHash('sha256')
74
- .update(JSON.stringify([
75
- seriesKey,
76
- repository,
77
- baseCommit,
78
- headCommit,
79
- ordinal,
80
- ]))
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
+ ]))
81
90
  .digest('hex');
82
91
  return `repro:${digest}`;
83
92
  }
@@ -371,10 +380,17 @@ export function analyzeHostedReproducibility(runs, identity) {
371
380
  });
372
381
  }
373
382
  }
383
+ const requestedRuns = identity.requestedRuns ?? runs.length;
384
+ const allRequestedRunsLaunched = runs.length === requestedRuns;
374
385
  const planVariants = groupedPlanVariants(parsed);
375
386
  const assessmentVariants = groupedOutcomeVariants(parsed);
376
- const replaySourceRunId = parsed[0]?.runId ?? null;
377
- const planReplayVerified = parsed.length === runs.length
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
378
394
  && parsed.length > 1
379
395
  && parsed[0]?.assessment.schemaVersion === 8
380
396
  && parsed[0]?.assessment.replaySourceRunId === null
@@ -384,7 +400,17 @@ export function analyzeHostedReproducibility(runs, identity) {
384
400
  === parsed[0].assessment.schemaVersion
385
401
  && item.assessment.riskPlanSha256
386
402
  === parsed[0].assessment.riskPlanSha256);
387
- const assessmentReproducible = planReplayVerified && assessmentVariants.length === 1;
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;
388
414
  const planDriftCellIds = driftIds(parsed, assessment => new Map(assessment.planIdentities.map(plan => [
389
415
  plan.cellId,
390
416
  { capabilityId: plan.capabilityId },
@@ -401,12 +427,12 @@ export function analyzeHostedReproducibility(runs, identity) {
401
427
  const inconclusiveCellIds = [...new Set(parsed.flatMap(item => item.assessment.outcomes
402
428
  .filter(outcome => outcome.status === 'inconclusive')
403
429
  .map(outcome => outcome.cellId)))].sort();
404
- const requestedRuns = identity.requestedRuns ?? runs.length;
405
430
  return {
406
431
  series_key: identity.seriesKey,
407
432
  repository: identity.repository,
408
433
  base_commit: identity.baseCommit,
409
434
  head_commit: identity.headCommit,
435
+ reproducibility_mode: mode,
410
436
  requested_runs: requestedRuns,
411
437
  launched_runs: runs.length,
412
438
  unstarted_runs: Math.max(0, requestedRuns - runs.length),
@@ -414,10 +440,12 @@ export function analyzeHostedReproducibility(runs, identity) {
414
440
  run_ids: runs.map(run => run.state.runId),
415
441
  replay_source_run_id: replaySourceRunId,
416
442
  plan_replay_verified: planReplayVerified,
417
- planner_reproducibility_tested: false,
418
- planner_reproducible: false,
443
+ planner_reproducibility_tested: plannerReproducibilityTested,
444
+ planner_reproducible: plannerReproducible,
419
445
  assessment_reproducible: assessmentReproducible,
420
- reproducible: planReplayVerified
446
+ reproducible: (mode === 'exact_plan_replay'
447
+ ? planReplayVerified
448
+ : plannerReproducible)
421
449
  && assessmentReproducible
422
450
  && inconclusiveCellIds.length === 0,
423
451
  plan_variants: planVariants,
@@ -444,9 +472,20 @@ function renderHostedReproducibility(report) {
444
472
  const icon = report.reproducible ? chalk.green('✓') : chalk.yellow('!');
445
473
  console.log(`${icon} Hosted reproducibility: ${report.reproducible ? 'reproducible' : 'not established'}`);
446
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'}`);
447
478
  console.log(` ${chalk.dim('Plan variants:')} ${report.plan_variants.length}`);
448
- console.log(` ${chalk.dim('Exact plan replay:')} ${report.plan_replay_verified ? 'verified' : 'not verified'}`);
449
- console.log(` ${chalk.dim('Independent planner reproducibility:')} not tested`);
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'}`);
450
489
  console.log(` ${chalk.dim('Assessment variants:')} ${report.assessment_variants.length}`);
451
490
  if (report.plan_drift_cell_ids.length > 0) {
452
491
  console.log(` ${chalk.dim('Plan drift:')} ${report.plan_drift_cell_ids.length} stable cell id(s)`);
@@ -465,6 +504,9 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
465
504
  const repository = parseHostedRepository(repositoryArg);
466
505
  const requestedRuns = boundedInteger(options.runs, DEFAULT_RUNS, '--runs', 2, MAX_RUNS);
467
506
  const seriesKey = requireSeriesKey(options.seriesKey);
507
+ const mode = options.freshPlans
508
+ ? 'fresh_plans'
509
+ : 'exact_plan_replay';
468
510
  const auth = await resolveAuthContext({
469
511
  preferredLogin: options.account,
470
512
  owner: repository.owner,
@@ -479,8 +521,8 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
479
521
  const started = await startHostedVerification(repositoryArg, {
480
522
  base: options.base,
481
523
  head: options.head,
482
- idempotencyKey: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1),
483
- replayPlanFrom: index === 0
524
+ idempotencyKey: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1, mode),
525
+ replayPlanFrom: options.freshPlans || index === 0
484
526
  ? undefined
485
527
  : runs[0]?.state.runId,
486
528
  wait: false,
@@ -497,7 +539,9 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
497
539
  timedOut = waited.timedOut;
498
540
  }
499
541
  runs.push({ state, timedOut });
500
- if (index === 0
542
+ if (!options.freshPlans
543
+ &&
544
+ index === 0
501
545
  && !isHostedReproducibilityReplaySource(runs[0], {
502
546
  seriesKey,
503
547
  repository: repository.fullName,
@@ -516,6 +560,7 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
516
560
  baseCommit: options.base,
517
561
  headCommit: options.head,
518
562
  requestedRuns,
563
+ mode,
519
564
  });
520
565
  if (options.json) {
521
566
  process.stdout.write(`${JSON.stringify(withSchema('cloud-verifier', {
package/dist/index.js CHANGED
@@ -262,12 +262,13 @@ Example:
262
262
  });
263
263
  hostedVerify
264
264
  .command('reproducibility')
265
- .description('Replay one exact hosted risk plan and report executor or assessment drift')
265
+ .description('Replay one exact hosted risk plan or run fresh plans to report drift')
266
266
  .argument('<repository>', 'Exact GitHub owner/repository name')
267
267
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
268
268
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
269
269
  .option('--runs <n>', 'Number of sequential production runs (default 3; min 2, max 10)')
270
270
  .option('--series-key <key>', 'Stable series key for resumable idempotent retries')
271
+ .option('--fresh-plans', 'Run the planner independently for every run instead of replaying the first plan')
271
272
  .option('--account <login>', 'Use a specific saved Haystack account')
272
273
  .option('--interval <seconds>', 'Polling interval for each run (default 5)')
273
274
  .option('--timeout <minutes>', 'Maximum wait per run (default 35)')
@@ -278,8 +279,14 @@ validated plan against the same repository and commit pair with a distinct,
278
279
  series-derived idempotency key. A reproducible verdict requires schema-9 replay
279
280
  provenance, stable assessment outcomes, and no inconclusive cells.
280
281
 
282
+ With --fresh-plans, every run plans independently. A reproducible verdict then
283
+ requires one stable-id plan variant, one assessment variant, no replay
284
+ provenance, and no inconclusive cells. Use this mode to detect planner drift;
285
+ use the default mode to isolate executor or assessment drift.
286
+
281
287
  Example:
282
288
  haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3
289
+ haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3 --fresh-plans
283
290
  `)
284
291
  .action(async (repository, _options, cmd) => {
285
292
  const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.25",
3
+ "version": "0.15.26",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {