mustflow 1.31.0 → 2.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 (66) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +77 -2
  5. package/dist/cli/commands/explain-verify.js +11 -1
  6. package/dist/cli/commands/index.js +14 -0
  7. package/dist/cli/commands/run.js +4 -1
  8. package/dist/cli/commands/verify.js +986 -43
  9. package/dist/cli/i18n/en.js +61 -10
  10. package/dist/cli/i18n/es.js +61 -10
  11. package/dist/cli/i18n/fr.js +61 -10
  12. package/dist/cli/i18n/hi.js +61 -10
  13. package/dist/cli/i18n/ko.js +61 -10
  14. package/dist/cli/i18n/zh.js +61 -10
  15. package/dist/cli/lib/dashboard-export.js +62 -12
  16. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  17. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  18. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  19. package/dist/cli/lib/dashboard-html/template.js +134 -0
  20. package/dist/cli/lib/dashboard-html/types.js +1 -0
  21. package/dist/cli/lib/dashboard-html.js +1 -1907
  22. package/dist/cli/lib/dashboard-locale.js +37 -0
  23. package/dist/cli/lib/local-index/constants.js +48 -0
  24. package/dist/cli/lib/local-index/index.js +2951 -0
  25. package/dist/cli/lib/local-index/sql.js +15 -0
  26. package/dist/cli/lib/local-index/types.js +1 -0
  27. package/dist/cli/lib/local-index.js +1 -1911
  28. package/dist/cli/lib/run-plan.js +76 -1
  29. package/dist/cli/lib/templates.js +18 -1
  30. package/dist/cli/lib/validation/command-intents.js +11 -0
  31. package/dist/cli/lib/validation/constants.js +238 -0
  32. package/dist/cli/lib/validation/index.js +1384 -0
  33. package/dist/cli/lib/validation/primitives.js +198 -0
  34. package/dist/cli/lib/validation/test-selection.js +95 -0
  35. package/dist/cli/lib/validation/types.js +1 -0
  36. package/dist/cli/lib/validation.js +1 -1770
  37. package/dist/core/check-issues.js +6 -0
  38. package/dist/core/completion-verdict.js +341 -0
  39. package/dist/core/contract-lint.js +221 -6
  40. package/dist/core/external-evidence.js +9 -0
  41. package/dist/core/public-json-contracts.js +21 -0
  42. package/dist/core/repeated-failure.js +179 -0
  43. package/dist/core/repro-evidence.js +134 -0
  44. package/dist/core/scope-risk.js +64 -0
  45. package/dist/core/skill-route-alignment.js +20 -0
  46. package/dist/core/source-anchor-status.js +4 -1
  47. package/dist/core/test-selection.js +3 -0
  48. package/dist/core/validation-ratchet.js +196 -0
  49. package/dist/core/verification-evidence.js +249 -0
  50. package/examples/README.md +12 -4
  51. package/package.json +3 -3
  52. package/schemas/README.md +13 -3
  53. package/schemas/change-verification-report.schema.json +16 -2
  54. package/schemas/commands.schema.json +4 -0
  55. package/schemas/contract-lint-report.schema.json +29 -0
  56. package/schemas/dashboard-export.schema.json +310 -0
  57. package/schemas/explain-report.schema.json +173 -1
  58. package/schemas/latest-run-pointer.schema.json +601 -0
  59. package/schemas/run-receipt.schema.json +4 -0
  60. package/schemas/test-selection.schema.json +81 -0
  61. package/schemas/verify-report.schema.json +578 -1
  62. package/schemas/verify-run-manifest.schema.json +627 -0
  63. package/templates/default/i18n.toml +1 -1
  64. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  65. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  66. package/templates/default/manifest.toml +29 -2
@@ -1,15 +1,24 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { createClassifyOutput } from './classify.js';
4
5
  import { runRun } from './run.js';
5
6
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
7
+ import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
8
+ import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
9
+ import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
10
+ import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
11
+ import { createVerifyEvidenceModel } from '../../core/verification-evidence.js';
12
+ import { createScopeDiffRisks } from '../../core/scope-risk.js';
13
+ import { countValidationRatchetVerdictEffects, createValidationRatchetRisks, } from '../../core/validation-ratchet.js';
6
14
  import { readCommandContract } from '../../core/config-loading.js';
7
15
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
8
16
  import { t } from '../lib/i18n.js';
9
- import { readLocalCommandEffectGraph, readLocalPathSurfaces, } from '../lib/local-index.js';
17
+ import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
10
18
  import { resolveMustflowRoot } from '../lib/project-root.js';
11
19
  const VERIFY_SCHEMA_VERSION = '1';
12
20
  const VERIFY_RUN_DIR = path.join('.mustflow', 'state', 'runs', 'verify-latest');
21
+ const VERIFY_MANIFEST_PATH = path.join(VERIFY_RUN_DIR, 'manifest.json');
13
22
  const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
14
23
  function createBufferedOutput() {
15
24
  const stdout = [];
@@ -33,13 +42,16 @@ function createBufferedOutput() {
33
42
  }
34
43
  export function getVerifyHelp(lang = 'en') {
35
44
  return renderHelp({
36
- usage: 'mf verify --reason <event> [options] | mf verify --from-plan <path> [options] | mf verify --changed [options]',
45
+ usage: 'mf verify --reason <event> [options] | mf verify --from-classification <path> [options] | mf verify --changed [options]',
37
46
  summary: t(lang, 'verify.help.summary'),
38
47
  options: [
39
48
  { label: '--reason <event>', description: t(lang, 'verify.help.option.reason') },
49
+ { label: '--from-classification <path>', description: t(lang, 'verify.help.option.fromClassification') },
40
50
  { label: '--from-plan <path>', description: t(lang, 'verify.help.option.fromPlan') },
41
51
  { label: '--changed', description: t(lang, 'verify.help.option.changed') },
42
52
  { label: '--write-plan <path>', description: t(lang, 'verify.help.option.writePlan') },
53
+ { label: '--repro-evidence <path>', description: t(lang, 'verify.help.option.reproEvidence') },
54
+ { label: '--external-evidence <path>', description: t(lang, 'verify.help.option.externalEvidence') },
43
55
  { label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
44
56
  { label: '--json', description: t(lang, 'cli.option.json') },
45
57
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
@@ -48,9 +60,9 @@ export function getVerifyHelp(lang = 'en') {
48
60
  'mf verify --reason code_change',
49
61
  'mf verify --reason docs_change --json',
50
62
  'mf verify --reason docs_change --plan-only --json',
51
- 'mf verify --from-plan verify-plan.json --json',
63
+ 'mf verify --from-classification .mustflow/state/change-classification.json --json',
64
+ 'mf verify --reason bug_fix --repro-evidence repro-evidence.json --json',
52
65
  'mf verify --changed --plan-only --json',
53
- 'mf verify --changed --write-plan .mustflow/state/change-plan.json --json',
54
66
  'mf verify --reason mustflow_docs_change',
55
67
  ],
56
68
  exitCodes: [
@@ -61,8 +73,11 @@ export function getVerifyHelp(lang = 'en') {
61
73
  }
62
74
  function parseVerifyArgs(args) {
63
75
  let reason;
76
+ let fromClassification;
64
77
  let fromPlan;
65
78
  let writePlan;
79
+ let reproEvidence;
80
+ let externalEvidence;
66
81
  let json = false;
67
82
  let planOnly = false;
68
83
  let changed = false;
@@ -92,21 +107,86 @@ function parseVerifyArgs(args) {
92
107
  if (arg === '--from-plan') {
93
108
  const value = args[index + 1];
94
109
  if (!value || value.startsWith('-')) {
95
- return { json, planOnly, changed, reason, fromPlan, error: 'missing_from_plan_value' };
110
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
96
111
  }
97
112
  fromPlan = value;
98
113
  index += 1;
99
114
  continue;
100
115
  }
116
+ if (arg === '--from-classification') {
117
+ const value = args[index + 1];
118
+ if (!value || value.startsWith('-')) {
119
+ return {
120
+ json,
121
+ planOnly,
122
+ changed,
123
+ reason,
124
+ fromClassification,
125
+ fromPlan,
126
+ error: 'missing_from_classification_value',
127
+ };
128
+ }
129
+ fromClassification = value;
130
+ index += 1;
131
+ continue;
132
+ }
101
133
  if (arg === '--write-plan') {
102
134
  const value = args[index + 1];
103
135
  if (!value || value.startsWith('-')) {
104
- return { json, planOnly, changed, reason, fromPlan, writePlan, error: 'missing_write_plan_value' };
136
+ return {
137
+ json,
138
+ planOnly,
139
+ changed,
140
+ reason,
141
+ fromClassification,
142
+ fromPlan,
143
+ writePlan,
144
+ error: 'missing_write_plan_value',
145
+ };
105
146
  }
106
147
  writePlan = value;
107
148
  index += 1;
108
149
  continue;
109
150
  }
151
+ if (arg === '--external-evidence') {
152
+ const value = args[index + 1];
153
+ if (!value || value.startsWith('-')) {
154
+ return {
155
+ json,
156
+ planOnly,
157
+ changed,
158
+ reason,
159
+ fromClassification,
160
+ fromPlan,
161
+ writePlan,
162
+ externalEvidence,
163
+ error: 'missing_external_evidence_value',
164
+ };
165
+ }
166
+ externalEvidence = value;
167
+ index += 1;
168
+ continue;
169
+ }
170
+ if (arg === '--repro-evidence') {
171
+ const value = args[index + 1];
172
+ if (!value || value.startsWith('-')) {
173
+ return {
174
+ json,
175
+ planOnly,
176
+ changed,
177
+ reason,
178
+ fromClassification,
179
+ fromPlan,
180
+ writePlan,
181
+ reproEvidence,
182
+ externalEvidence,
183
+ error: 'missing_repro_evidence_value',
184
+ };
185
+ }
186
+ reproEvidence = value;
187
+ index += 1;
188
+ continue;
189
+ }
110
190
  if (arg.startsWith('--reason=')) {
111
191
  const value = arg.slice('--reason='.length);
112
192
  if (value.length === 0) {
@@ -118,25 +198,98 @@ function parseVerifyArgs(args) {
118
198
  if (arg.startsWith('--from-plan=')) {
119
199
  const value = arg.slice('--from-plan='.length);
120
200
  if (value.length === 0) {
121
- return { json, planOnly, changed, reason, fromPlan, error: 'missing_from_plan_value' };
201
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
122
202
  }
123
203
  fromPlan = value;
124
204
  continue;
125
205
  }
206
+ if (arg.startsWith('--from-classification=')) {
207
+ const value = arg.slice('--from-classification='.length);
208
+ if (value.length === 0) {
209
+ return {
210
+ json,
211
+ planOnly,
212
+ changed,
213
+ reason,
214
+ fromClassification,
215
+ fromPlan,
216
+ error: 'missing_from_classification_value',
217
+ };
218
+ }
219
+ fromClassification = value;
220
+ continue;
221
+ }
126
222
  if (arg.startsWith('--write-plan=')) {
127
223
  const value = arg.slice('--write-plan='.length);
128
224
  if (value.length === 0) {
129
- return { json, planOnly, changed, reason, fromPlan, writePlan, error: 'missing_write_plan_value' };
225
+ return {
226
+ json,
227
+ planOnly,
228
+ changed,
229
+ reason,
230
+ fromClassification,
231
+ fromPlan,
232
+ writePlan,
233
+ error: 'missing_write_plan_value',
234
+ };
130
235
  }
131
236
  writePlan = value;
132
237
  continue;
133
238
  }
239
+ if (arg.startsWith('--external-evidence=')) {
240
+ const value = arg.slice('--external-evidence='.length);
241
+ if (value.length === 0) {
242
+ return {
243
+ json,
244
+ planOnly,
245
+ changed,
246
+ reason,
247
+ fromClassification,
248
+ fromPlan,
249
+ writePlan,
250
+ externalEvidence,
251
+ error: 'missing_external_evidence_value',
252
+ };
253
+ }
254
+ externalEvidence = value;
255
+ continue;
256
+ }
257
+ if (arg.startsWith('--repro-evidence=')) {
258
+ const value = arg.slice('--repro-evidence='.length);
259
+ if (value.length === 0) {
260
+ return {
261
+ json,
262
+ planOnly,
263
+ changed,
264
+ reason,
265
+ fromClassification,
266
+ fromPlan,
267
+ writePlan,
268
+ reproEvidence,
269
+ externalEvidence,
270
+ error: 'missing_repro_evidence_value',
271
+ };
272
+ }
273
+ reproEvidence = value;
274
+ continue;
275
+ }
134
276
  if (arg.startsWith('-')) {
135
- return { json, planOnly, changed, reason, fromPlan, writePlan, error: arg };
277
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence, error: arg };
136
278
  }
137
- return { json, planOnly, changed, reason, fromPlan, writePlan, error: `unexpected:${arg}` };
279
+ return {
280
+ json,
281
+ planOnly,
282
+ changed,
283
+ reason,
284
+ fromClassification,
285
+ fromPlan,
286
+ writePlan,
287
+ reproEvidence,
288
+ externalEvidence,
289
+ error: `unexpected:${arg}`,
290
+ };
138
291
  }
139
- return { json, planOnly, changed, reason, fromPlan, writePlan };
292
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence };
140
293
  }
141
294
  function uniqueStrings(values) {
142
295
  return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
@@ -307,6 +460,285 @@ export function readInputFromPlan(projectRoot, inputPath) {
307
460
  classificationReport,
308
461
  };
309
462
  }
463
+ function isExternalEvidenceStatus(value) {
464
+ return value === 'passed' || value === 'failed' || value === 'cancelled' || value === 'unknown';
465
+ }
466
+ function isLegacyReproEvidenceStatus(value) {
467
+ return value === 'present' || value === 'unavailable' || value === 'missing';
468
+ }
469
+ function isReproBeforeFixStatus(value) {
470
+ return value === 'reproduced' || value === 'unavailable' || value === 'missing';
471
+ }
472
+ function isReproBeforeFixOutcome(value) {
473
+ return value === 'failed_as_expected' || value === 'failed_differently' || value === 'passed_unexpectedly' || value === null;
474
+ }
475
+ function isReproAfterFixStatus(value) {
476
+ return value === 'passed' || value === 'failed' || value === 'unavailable' || value === 'missing';
477
+ }
478
+ function isReproAfterFixOutcome(value) {
479
+ return value === 'passed_expected_behavior' || value === 'failed_same_route' || value === 'failed_differently' || value === null;
480
+ }
481
+ function isReproRegressionGuardStatus(value) {
482
+ return value === 'passed' || value === 'failed' || value === 'unavailable' || value === 'missing';
483
+ }
484
+ function isReproRouteKind(value) {
485
+ return (value === 'test' ||
486
+ value === 'cli' ||
487
+ value === 'browser' ||
488
+ value === 'api' ||
489
+ value === 'manual' ||
490
+ value === 'unknown' ||
491
+ value === null);
492
+ }
493
+ function readOptionalString(value) {
494
+ return typeof value === 'string' && value.length > 0 ? value : null;
495
+ }
496
+ function readRouteStep(value, index) {
497
+ if (!isPlainRecord(value)) {
498
+ return {
499
+ ordinal: index + 1,
500
+ action: null,
501
+ target: null,
502
+ input_digest: null,
503
+ observation_digest: null,
504
+ summary: null,
505
+ };
506
+ }
507
+ const ordinal = typeof value.ordinal === 'number' && Number.isInteger(value.ordinal) && value.ordinal > 0 ? value.ordinal : index + 1;
508
+ return {
509
+ ordinal,
510
+ action: readOptionalString(value.action),
511
+ target: readOptionalString(value.target),
512
+ input_digest: readOptionalString(value.input_digest),
513
+ observation_digest: readOptionalString(value.observation_digest),
514
+ summary: readOptionalString(value.summary),
515
+ };
516
+ }
517
+ function readReproductionRoute(value) {
518
+ if (!isPlainRecord(value)) {
519
+ return {
520
+ route_id: null,
521
+ route_kind: null,
522
+ route_digest: null,
523
+ failure_oracle_hash: null,
524
+ steps: [],
525
+ };
526
+ }
527
+ const routeKind = value.route_kind ?? null;
528
+ if (!isReproRouteKind(routeKind)) {
529
+ throw new Error('invalid_repro_evidence_file');
530
+ }
531
+ const rawSteps = Array.isArray(value.steps) ? value.steps : [];
532
+ return {
533
+ route_id: readOptionalString(value.route_id),
534
+ route_kind: routeKind,
535
+ route_digest: readOptionalString(value.route_digest),
536
+ failure_oracle_hash: readOptionalString(value.failure_oracle_hash),
537
+ steps: rawSteps.map((step, index) => readRouteStep(step, index)),
538
+ };
539
+ }
540
+ function readLegacyReproEvidenceItem(value) {
541
+ if (!isPlainRecord(value)) {
542
+ return {
543
+ status: 'missing',
544
+ summary: null,
545
+ reason: null,
546
+ };
547
+ }
548
+ if (!isLegacyReproEvidenceStatus(value.status)) {
549
+ throw new Error('invalid_repro_evidence_file');
550
+ }
551
+ return {
552
+ status: value.status,
553
+ summary: readOptionalString(value.summary),
554
+ reason: readOptionalString(value.reason),
555
+ };
556
+ }
557
+ function legacyBeforeFixEvidence(value) {
558
+ const item = readLegacyReproEvidenceItem(value);
559
+ return {
560
+ status: item.status === 'present' ? 'reproduced' : item.status,
561
+ outcome: item.status === 'present' ? 'failed_as_expected' : null,
562
+ receipt_path: null,
563
+ receipt_sha256: null,
564
+ verification_plan_id: null,
565
+ summary: item.summary,
566
+ reason: item.reason,
567
+ };
568
+ }
569
+ function legacyAfterFixEvidence(value) {
570
+ const item = readLegacyReproEvidenceItem(value);
571
+ return {
572
+ status: item.status === 'present' ? 'passed' : item.status,
573
+ outcome: item.status === 'present' ? 'passed_expected_behavior' : null,
574
+ same_route_as: null,
575
+ receipt_path: null,
576
+ receipt_sha256: null,
577
+ verification_plan_id: null,
578
+ summary: item.summary,
579
+ reason: item.reason,
580
+ };
581
+ }
582
+ function legacyRegressionGuardEvidence(value) {
583
+ const item = readLegacyReproEvidenceItem(value);
584
+ return {
585
+ status: item.status === 'present' ? 'passed' : item.status,
586
+ intent: null,
587
+ test_path: null,
588
+ receipt_path: null,
589
+ receipt_sha256: null,
590
+ verification_plan_id: null,
591
+ summary: item.summary,
592
+ reason: item.reason,
593
+ };
594
+ }
595
+ function readBeforeFixEvidence(value) {
596
+ if (!isPlainRecord(value)) {
597
+ return {
598
+ status: 'missing',
599
+ outcome: null,
600
+ receipt_path: null,
601
+ receipt_sha256: null,
602
+ verification_plan_id: null,
603
+ summary: null,
604
+ reason: null,
605
+ };
606
+ }
607
+ const outcome = value.outcome ?? null;
608
+ if (!isReproBeforeFixStatus(value.status) || !isReproBeforeFixOutcome(outcome)) {
609
+ throw new Error('invalid_repro_evidence_file');
610
+ }
611
+ return {
612
+ status: value.status,
613
+ outcome,
614
+ receipt_path: readOptionalString(value.receipt_path),
615
+ receipt_sha256: readOptionalString(value.receipt_sha256),
616
+ verification_plan_id: readOptionalString(value.verification_plan_id),
617
+ summary: readOptionalString(value.summary),
618
+ reason: readOptionalString(value.reason),
619
+ };
620
+ }
621
+ function readAfterFixEvidence(value) {
622
+ if (!isPlainRecord(value)) {
623
+ return {
624
+ status: 'missing',
625
+ outcome: null,
626
+ same_route_as: null,
627
+ receipt_path: null,
628
+ receipt_sha256: null,
629
+ verification_plan_id: null,
630
+ summary: null,
631
+ reason: null,
632
+ };
633
+ }
634
+ const outcome = value.outcome ?? null;
635
+ if (!isReproAfterFixStatus(value.status) || !isReproAfterFixOutcome(outcome)) {
636
+ throw new Error('invalid_repro_evidence_file');
637
+ }
638
+ return {
639
+ status: value.status,
640
+ outcome,
641
+ same_route_as: readOptionalString(value.same_route_as),
642
+ receipt_path: readOptionalString(value.receipt_path),
643
+ receipt_sha256: readOptionalString(value.receipt_sha256),
644
+ verification_plan_id: readOptionalString(value.verification_plan_id),
645
+ summary: readOptionalString(value.summary),
646
+ reason: readOptionalString(value.reason),
647
+ };
648
+ }
649
+ function readRegressionGuardEvidence(value) {
650
+ if (!isPlainRecord(value)) {
651
+ return {
652
+ status: 'missing',
653
+ intent: null,
654
+ test_path: null,
655
+ receipt_path: null,
656
+ receipt_sha256: null,
657
+ verification_plan_id: null,
658
+ summary: null,
659
+ reason: null,
660
+ };
661
+ }
662
+ if (!isReproRegressionGuardStatus(value.status)) {
663
+ throw new Error('invalid_repro_evidence_file');
664
+ }
665
+ return {
666
+ status: value.status,
667
+ intent: readOptionalString(value.intent),
668
+ test_path: readOptionalString(value.test_path),
669
+ receipt_path: readOptionalString(value.receipt_path),
670
+ receipt_sha256: readOptionalString(value.receipt_sha256),
671
+ verification_plan_id: readOptionalString(value.verification_plan_id),
672
+ summary: readOptionalString(value.summary),
673
+ reason: readOptionalString(value.reason),
674
+ };
675
+ }
676
+ function readReproEvidenceFile(projectRoot, inputPath) {
677
+ let parsed;
678
+ const evidencePath = resolvePlanPath(projectRoot, inputPath);
679
+ try {
680
+ parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
681
+ }
682
+ catch {
683
+ throw new Error('invalid_repro_evidence_file');
684
+ }
685
+ if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'repro-evidence') {
686
+ throw new Error('unsupported_repro_evidence_source');
687
+ }
688
+ const regressionGuard = isPlainRecord(parsed.regression_guard) && isReproRegressionGuardStatus(parsed.regression_guard.status)
689
+ ? readRegressionGuardEvidence(parsed.regression_guard)
690
+ : legacyRegressionGuardEvidence(parsed.regression_guard);
691
+ return {
692
+ source: 'repro_first_debug',
693
+ authority: 'claim_evidence',
694
+ reported_symptom: readOptionalString(parsed.reported_symptom),
695
+ expected_behavior: readOptionalString(parsed.expected_behavior),
696
+ observed_behavior: readOptionalString(parsed.observed_behavior),
697
+ reproduction_route: readReproductionRoute(parsed.reproduction_route),
698
+ before_fix: isPlainRecord(parsed.before_fix)
699
+ ? readBeforeFixEvidence(parsed.before_fix)
700
+ : legacyBeforeFixEvidence(parsed.evidence_before_fix),
701
+ after_fix: isPlainRecord(parsed.after_fix)
702
+ ? readAfterFixEvidence(parsed.after_fix)
703
+ : legacyAfterFixEvidence(parsed.evidence_after_fix),
704
+ regression_guard: regressionGuard,
705
+ };
706
+ }
707
+ function readExternalEvidenceFile(projectRoot, inputPath) {
708
+ let parsed;
709
+ const evidencePath = resolvePlanPath(projectRoot, inputPath);
710
+ try {
711
+ parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
712
+ }
713
+ catch {
714
+ throw new Error('invalid_external_evidence_file');
715
+ }
716
+ if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'external-evidence') {
717
+ throw new Error('unsupported_external_evidence_source');
718
+ }
719
+ if (!Array.isArray(parsed.checks)) {
720
+ throw new Error('invalid_external_evidence_file');
721
+ }
722
+ return parsed.checks.map((check) => {
723
+ if (!isPlainRecord(check) ||
724
+ typeof check.provider !== 'string' ||
725
+ check.provider.length === 0 ||
726
+ typeof check.name !== 'string' ||
727
+ check.name.length === 0 ||
728
+ !isExternalEvidenceStatus(check.status)) {
729
+ throw new Error('invalid_external_evidence_file');
730
+ }
731
+ return {
732
+ source: 'external_ci',
733
+ authority: 'supporting_only',
734
+ provider: check.provider,
735
+ name: check.name,
736
+ status: check.status,
737
+ url: readOptionalString(check.url),
738
+ summary: readOptionalString(check.summary),
739
+ };
740
+ });
741
+ }
310
742
  function createInputFromChanged(projectRoot) {
311
743
  const plan = createClassifyOutput(projectRoot, 'changed', []);
312
744
  return {
@@ -344,6 +776,9 @@ function skippedResult(candidate) {
344
776
  reason: candidate.reason,
345
777
  detail: candidate.detail,
346
778
  exit_code: null,
779
+ verification_plan_id: null,
780
+ receipt_path: null,
781
+ receipt_sha256: null,
347
782
  receipt: null,
348
783
  };
349
784
  }
@@ -383,7 +818,7 @@ function testTargetsByScheduledIntent(report) {
383
818
  candidate.appliedTestTargets.length > 0)
384
819
  .map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
385
820
  }
386
- async function runVerificationIntent(intent, lang, testTargets = []) {
821
+ async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
387
822
  const output = createBufferedOutput();
388
823
  const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
389
824
  writeLatestReceipt: false,
@@ -413,6 +848,9 @@ async function runVerificationIntent(intent, lang, testTargets = []) {
413
848
  reason: exitCode === 0 ? null : 'run_failed',
414
849
  detail: output.stderr().trim() || null,
415
850
  exit_code: exitCode,
851
+ verification_plan_id: verificationPlanId,
852
+ receipt_path: null,
853
+ receipt_sha256: null,
416
854
  receipt,
417
855
  };
418
856
  }
@@ -430,6 +868,210 @@ function summarizeResults(results) {
430
868
  skipped,
431
869
  };
432
870
  }
871
+ function countUndeclaredWriteDrift(results) {
872
+ return results.filter((result) => {
873
+ const writeDrift = result.receipt?.write_drift;
874
+ if (typeof writeDrift !== 'object' || writeDrift === null) {
875
+ return false;
876
+ }
877
+ return writeDrift.has_undeclared_changes === true;
878
+ }).length;
879
+ }
880
+ function stringField(value) {
881
+ return typeof value === 'string' && value.length > 0 ? value : null;
882
+ }
883
+ function objectField(value) {
884
+ return typeof value === 'object' && value !== null ? value : null;
885
+ }
886
+ function performanceForResult(result) {
887
+ return objectField(result.receipt?.performance);
888
+ }
889
+ function resultSummaryForResult(result) {
890
+ return objectField(performanceForResult(result)?.result_summary);
891
+ }
892
+ function commandFingerprintForResult(result) {
893
+ return stringField(performanceForResult(result)?.command_fingerprint);
894
+ }
895
+ function exitCodeClassForResult(result) {
896
+ const resultSummary = resultSummaryForResult(result);
897
+ const explicitClass = stringField(resultSummary?.exit_code_class);
898
+ if (explicitClass) {
899
+ return explicitClass;
900
+ }
901
+ if (result.exit_code === null) {
902
+ return 'no_exit_code';
903
+ }
904
+ return result.exit_code === 0 ? 'success' : 'failure';
905
+ }
906
+ function timedOutForResult(result) {
907
+ const resultSummary = resultSummaryForResult(result);
908
+ return result.status === 'timed_out' || resultSummary?.timed_out === true;
909
+ }
910
+ function errorKindForResult(result) {
911
+ return stringField(resultSummaryForResult(result)?.error_kind) ?? (result.status === 'start_failed' ? 'start_failed' : null);
912
+ }
913
+ function failedResults(results) {
914
+ return results.filter((result) => !result.skipped &&
915
+ (result.status === 'failed' || result.status === 'timed_out' || result.status === 'start_failed'));
916
+ }
917
+ function createFailureFingerprintForVerify(input) {
918
+ const failures = failedResults(input.results);
919
+ return createVerificationFailureFingerprint({
920
+ verificationPlanId: input.verificationPlanId,
921
+ failedIntents: failures.map((result) => result.intent).filter((intent) => intent !== null),
922
+ exitCodeClasses: failures.map(exitCodeClassForResult).filter((value) => value !== null),
923
+ timeoutFlags: failures.map(timedOutForResult),
924
+ errorKinds: failures.map(errorKindForResult).filter((value) => value !== null),
925
+ riskCodes: input.riskCodes,
926
+ affectedSurfaces: input.report.requirements.flatMap((requirement) => requirement.surfaces),
927
+ commandFingerprints: failures.map(commandFingerprintForResult).filter((value) => value !== null),
928
+ });
929
+ }
930
+ function riskCodesForFailureFingerprint(input) {
931
+ const writeDriftRiskCodes = countUndeclaredWriteDrift(input.results) > 0 ? ['undeclared_write_drift'] : [];
932
+ return [
933
+ ...input.sourceAnchorRisks.map(() => 'source_anchor_invariant_review_required'),
934
+ ...input.scopeDiffRisks.map((risk) => risk.code),
935
+ ...input.validationRatchetRisks.map((risk) => risk.code),
936
+ ...input.reproEvidenceRisks.map((risk) => risk.code),
937
+ ...input.externalEvidenceRisks.map((risk) => risk.code),
938
+ ...writeDriftRiskCodes,
939
+ ];
940
+ }
941
+ function createReceiptBindingEvidence(results, verificationPlanId) {
942
+ let planBoundCount = 0;
943
+ let planUnboundCount = 0;
944
+ let fingerprintBoundCount = 0;
945
+ let fingerprintUnboundCount = 0;
946
+ let currentStateBoundCount = 0;
947
+ let currentStateUnavailableCount = 0;
948
+ let staleCount = 0;
949
+ let planMismatchCount = 0;
950
+ for (const result of results) {
951
+ if (!result.receipt) {
952
+ continue;
953
+ }
954
+ const receiptPlanId = stringField(result.receipt.verification_plan_id);
955
+ const resultPlanId = result.verification_plan_id;
956
+ const resultPlanMatches = resultPlanId === verificationPlanId;
957
+ const receiptPlanMatches = receiptPlanId === verificationPlanId;
958
+ if (resultPlanMatches && receiptPlanMatches) {
959
+ planBoundCount += 1;
960
+ }
961
+ else if ((typeof resultPlanId === 'string' && resultPlanId !== verificationPlanId) ||
962
+ (receiptPlanId !== null && receiptPlanId !== verificationPlanId)) {
963
+ planMismatchCount += 1;
964
+ }
965
+ else {
966
+ planUnboundCount += 1;
967
+ }
968
+ const performance = objectField(result.receipt.performance);
969
+ const hasFingerprints = performance !== null &&
970
+ stringField(performance.command_fingerprint) !== null &&
971
+ stringField(performance.intent_fingerprint) !== null &&
972
+ stringField(performance.contract_fingerprint) !== null;
973
+ if (hasFingerprints) {
974
+ fingerprintBoundCount += 1;
975
+ }
976
+ else {
977
+ fingerprintUnboundCount += 1;
978
+ }
979
+ const currentStateBinding = stringField(result.receipt.head_tree_hash) ??
980
+ stringField(result.receipt.changed_files_hash) ??
981
+ stringField(result.receipt.current_state_hash);
982
+ if (currentStateBinding !== null) {
983
+ currentStateBoundCount += 1;
984
+ }
985
+ else {
986
+ currentStateUnavailableCount += 1;
987
+ }
988
+ if (!result.receipt_path || !result.receipt_sha256) {
989
+ staleCount += 1;
990
+ }
991
+ }
992
+ return {
993
+ plan_bound_count: planBoundCount,
994
+ plan_unbound_count: planUnboundCount,
995
+ fingerprint_bound_count: fingerprintBoundCount,
996
+ fingerprint_unbound_count: fingerprintUnboundCount,
997
+ current_state_bound_count: currentStateBoundCount,
998
+ current_state_unavailable_count: currentStateUnavailableCount,
999
+ stale_count: staleCount,
1000
+ plan_mismatch_count: planMismatchCount,
1001
+ };
1002
+ }
1003
+ function resultForSelectedIntent(results, intent) {
1004
+ return results.find((result) => result.intent === intent && result.status !== 'skipped') ?? null;
1005
+ }
1006
+ function createCriteriaEvidence(report, results) {
1007
+ const evidence = {
1008
+ total: report.requirements.length,
1009
+ covered: 0,
1010
+ partially_covered: 0,
1011
+ uncovered: 0,
1012
+ blocked: 0,
1013
+ contradicted: 0,
1014
+ };
1015
+ return report.requirements.reduce((current, requirement) => {
1016
+ const candidates = report.candidates.filter((candidate) => candidate.reason === requirement.reason);
1017
+ const selectedIntents = candidates
1018
+ .filter((candidate) => candidate.selectionState === 'selected')
1019
+ .map((candidate) => candidate.intent)
1020
+ .filter((intent) => intent !== null);
1021
+ const skippedIntents = candidates
1022
+ .filter((candidate) => candidate.status !== 'runnable')
1023
+ .map((candidate) => candidate.intent)
1024
+ .filter((intent) => intent !== null);
1025
+ const gapCount = report.gaps.filter((gap) => gap.reason === requirement.reason).length;
1026
+ const selectedResults = selectedIntents.map((intent) => resultForSelectedIntent(results, intent));
1027
+ if (selectedResults.some((result) => result?.status === 'failed' || result?.status === 'timed_out' || result?.status === 'start_failed')) {
1028
+ return { ...current, contradicted: current.contradicted + 1 };
1029
+ }
1030
+ if (gapCount > 0 || (selectedIntents.length === 0 && skippedIntents.length > 0)) {
1031
+ return { ...current, blocked: current.blocked + 1 };
1032
+ }
1033
+ if (selectedIntents.length === 0) {
1034
+ return { ...current, uncovered: current.uncovered + 1 };
1035
+ }
1036
+ if (skippedIntents.length > 0) {
1037
+ return { ...current, partially_covered: current.partially_covered + 1 };
1038
+ }
1039
+ if (selectedResults.every((result) => result?.status === 'passed')) {
1040
+ return { ...current, covered: current.covered + 1 };
1041
+ }
1042
+ return { ...current, uncovered: current.uncovered + 1 };
1043
+ }, evidence);
1044
+ }
1045
+ function createCompletionVerdictForResults(input) {
1046
+ const receiptBinding = createReceiptBindingEvidence(input.results, input.verificationPlanId);
1047
+ const receiptBindingRiskCount = receiptBinding.plan_unbound_count + receiptBinding.fingerprint_unbound_count;
1048
+ const repeatedFailureBlockerCount = input.repeatedFailureRisks.filter((risk) => risk.verdict_effect === 'blocker').length;
1049
+ return createVerifyCompletionVerdict({
1050
+ verificationPlanId: input.verificationPlanId,
1051
+ matchedIntents: input.summary.matched,
1052
+ ranIntents: input.summary.ran,
1053
+ passedIntents: input.summary.passed,
1054
+ failedIntents: input.summary.failed,
1055
+ skippedIntents: input.summary.skipped,
1056
+ receiptCount: input.results.filter((result) => result.receipt !== null).length,
1057
+ sourceAnchorRiskCount: input.sourceAnchorRiskCount,
1058
+ scopeDiffRiskCount: input.scopeDiffRiskCount,
1059
+ repeatedFailureCount: input.repeatedFailureRisks.length,
1060
+ repeatedFailureBlockerCount,
1061
+ validationRatchetRiskCount: input.validationRatchetRiskCount,
1062
+ validationRatchetContradictionCount: input.validationRatchetContradictionCount,
1063
+ reproEvidenceRiskCount: input.reproEvidenceRiskCount,
1064
+ reproEvidenceContradictionCount: input.reproEvidenceContradictionCount,
1065
+ reproEvidenceUnverifiedCount: input.reproEvidenceUnverifiedCount,
1066
+ externalEvidenceRiskCount: input.externalEvidenceRiskCount,
1067
+ writeDriftRiskCount: countUndeclaredWriteDrift(input.results),
1068
+ receiptBindingRiskCount,
1069
+ staleReceiptCount: receiptBinding.stale_count,
1070
+ planMismatchCount: receiptBinding.plan_mismatch_count,
1071
+ criteria: createCriteriaEvidence(input.report, input.results),
1072
+ receiptBinding,
1073
+ });
1074
+ }
433
1075
  function getVerificationStatus(summary) {
434
1076
  if (summary.failed > 0) {
435
1077
  return 'failed';
@@ -442,35 +1084,223 @@ function getVerificationStatus(summary) {
442
1084
  }
443
1085
  return 'passed';
444
1086
  }
445
- function writeVerifyRunReceipts(projectRoot, output) {
1087
+ function isVerificationStatus(value) {
1088
+ return value === 'passed' || value === 'partial' || value === 'failed' || value === 'blocked';
1089
+ }
1090
+ function readVerificationFailureFingerprint(value) {
1091
+ const record = objectField(value);
1092
+ if (record?.schema_version !== '1' ||
1093
+ typeof record.fingerprint !== 'string' ||
1094
+ typeof record.verification_plan_id !== 'string' ||
1095
+ typeof record.failed_intents_hash !== 'string' ||
1096
+ typeof record.exit_code_classes_hash !== 'string' ||
1097
+ typeof record.timeout_flags_hash !== 'string' ||
1098
+ typeof record.error_kinds_hash !== 'string' ||
1099
+ typeof record.diagnostic_hash !== 'string' ||
1100
+ typeof record.risk_codes_hash !== 'string' ||
1101
+ typeof record.affected_surfaces_hash !== 'string' ||
1102
+ typeof record.command_fingerprints_hash !== 'string') {
1103
+ return null;
1104
+ }
1105
+ return {
1106
+ schema_version: '1',
1107
+ fingerprint: record.fingerprint,
1108
+ verification_plan_id: record.verification_plan_id,
1109
+ failed_intents_hash: record.failed_intents_hash,
1110
+ exit_code_classes_hash: record.exit_code_classes_hash,
1111
+ timeout_flags_hash: record.timeout_flags_hash,
1112
+ error_kinds_hash: record.error_kinds_hash,
1113
+ diagnostic_hash: record.diagnostic_hash,
1114
+ risk_codes_hash: record.risk_codes_hash,
1115
+ affected_surfaces_hash: record.affected_surfaces_hash,
1116
+ command_fingerprints_hash: record.command_fingerprints_hash,
1117
+ };
1118
+ }
1119
+ function readPreviousVerifyLatestSummary(projectRoot) {
1120
+ try {
1121
+ const parsed = JSON.parse(readFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), 'utf8'));
1122
+ if (parsed.command !== 'verify' ||
1123
+ parsed.kind !== 'verify_run_summary' ||
1124
+ typeof parsed.verification_plan_id !== 'string' ||
1125
+ !isVerificationStatus(parsed.status)) {
1126
+ return null;
1127
+ }
1128
+ return {
1129
+ verification_plan_id: parsed.verification_plan_id,
1130
+ status: parsed.status,
1131
+ failure_fingerprint: readVerificationFailureFingerprint(parsed.failure_fingerprint),
1132
+ };
1133
+ }
1134
+ catch {
1135
+ return null;
1136
+ }
1137
+ }
1138
+ function hashTextSha256(content) {
1139
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
1140
+ }
1141
+ function stableJson(value) {
1142
+ if (Array.isArray(value)) {
1143
+ return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
1144
+ }
1145
+ if (value && typeof value === 'object') {
1146
+ const record = value;
1147
+ return `{${Object.keys(record)
1148
+ .sort((left, right) => left.localeCompare(right))
1149
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
1150
+ .join(',')}}`;
1151
+ }
1152
+ return JSON.stringify(value) ?? 'null';
1153
+ }
1154
+ function getCandidateIntentNames(report) {
1155
+ return [...new Set(report.candidates.map((candidate) => candidate.intent).filter((intent) => Boolean(intent)))]
1156
+ .sort((left, right) => left.localeCompare(right));
1157
+ }
1158
+ function createVerificationPlanId(report, contract) {
1159
+ const relatedIntents = Object.fromEntries(getCandidateIntentNames(report).map((intent) => [intent, contract.intents[intent] ?? null]));
1160
+ const fingerprintSource = {
1161
+ schema_version: '1',
1162
+ algorithm: 'mustflow.verify_plan_id.v1',
1163
+ report: {
1164
+ source: report.source,
1165
+ files: report.files,
1166
+ classification_summary: report.classification_summary,
1167
+ requirements: report.requirements,
1168
+ candidates: report.candidates,
1169
+ gaps: report.gaps,
1170
+ schedule: report.schedule,
1171
+ test_selection: report.test_selection,
1172
+ },
1173
+ command_contract: {
1174
+ defaults: contract.defaults,
1175
+ resources: contract.resources,
1176
+ intents: relatedIntents,
1177
+ },
1178
+ };
1179
+ return hashTextSha256(stableJson(fingerprintSource));
1180
+ }
1181
+ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
446
1182
  const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
447
1183
  const intentDir = path.join(runDir, 'intents');
448
1184
  const receipts = [];
1185
+ const results = [];
449
1186
  rmSync(runDir, { recursive: true, force: true });
450
1187
  mkdirSync(intentDir, { recursive: true });
451
1188
  for (const [index, result] of output.results.entries()) {
452
1189
  let receiptPath = null;
1190
+ let receiptSha256 = null;
1191
+ let receipt = result.receipt;
453
1192
  if (result.intent && result.receipt) {
454
1193
  const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
455
1194
  const absoluteReceiptPath = path.join(intentDir, fileName);
456
1195
  receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
457
- writeFileSync(absoluteReceiptPath, `${JSON.stringify({ ...result.receipt, receipt_path: receiptPath }, null, 2)}\n`, 'utf8');
1196
+ receipt = {
1197
+ ...result.receipt,
1198
+ verification_plan_id: output.verification_plan_id,
1199
+ receipt_path: receiptPath,
1200
+ };
1201
+ const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
1202
+ receiptSha256 = hashTextSha256(receiptContent);
1203
+ writeFileSync(absoluteReceiptPath, receiptContent, 'utf8');
458
1204
  }
459
1205
  receipts.push({
460
1206
  intent: result.intent,
461
1207
  status: result.status,
462
1208
  skipped: result.skipped,
1209
+ verification_plan_id: result.skipped ? null : output.verification_plan_id,
463
1210
  receipt_path: receiptPath,
1211
+ receipt_sha256: receiptSha256,
1212
+ });
1213
+ results.push({
1214
+ ...result,
1215
+ verification_plan_id: result.skipped ? null : output.verification_plan_id,
1216
+ receipt_path: receiptPath,
1217
+ receipt_sha256: receiptSha256,
1218
+ receipt,
464
1219
  });
465
1220
  }
1221
+ const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, {
1222
+ verificationPlanId: output.verification_plan_id,
1223
+ });
1224
+ const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
1225
+ const validationRatchetVerdictEffects = countValidationRatchetVerdictEffects(validationRatchetRisks);
1226
+ const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
1227
+ const failureFingerprint = createFailureFingerprintForVerify({
1228
+ verificationPlanId: output.verification_plan_id,
1229
+ report,
1230
+ results,
1231
+ riskCodes: riskCodesForFailureFingerprint({
1232
+ sourceAnchorRisks,
1233
+ scopeDiffRisks,
1234
+ validationRatchetRisks,
1235
+ reproEvidenceRisks,
1236
+ externalEvidenceRisks,
1237
+ results,
1238
+ }),
1239
+ });
1240
+ const repeatedFailureSummary = updateRepeatedFailureState({
1241
+ projectRoot,
1242
+ failureFingerprint,
1243
+ status: output.status,
1244
+ });
1245
+ const previousVerifyLatest = readPreviousVerifyLatestSummary(projectRoot);
1246
+ const finalRepeatedFailureRisks = createRepeatedFailureRisks({
1247
+ previousFailureFingerprint: previousVerifyLatest?.failure_fingerprint ?? null,
1248
+ previousStatus: previousVerifyLatest?.status ?? null,
1249
+ currentFailureFingerprint: failureFingerprint,
1250
+ currentStatus: output.status,
1251
+ currentSummary: repeatedFailureSummary,
1252
+ });
1253
+ const completionVerdict = createCompletionVerdictForResults({
1254
+ report,
1255
+ verificationPlanId: output.verification_plan_id,
1256
+ summary: output.summary,
1257
+ results,
1258
+ sourceAnchorRiskCount: sourceAnchorRisks.length,
1259
+ scopeDiffRiskCount: scopeDiffRisks.length,
1260
+ repeatedFailureRisks: finalRepeatedFailureRisks,
1261
+ validationRatchetRiskCount: validationRatchetRisks.length,
1262
+ validationRatchetContradictionCount: validationRatchetVerdictEffects.contradicted,
1263
+ reproEvidenceRiskCount: reproEvidenceRisks.length,
1264
+ reproEvidenceContradictionCount: reproEvidenceVerdictEffects.contradicted,
1265
+ reproEvidenceUnverifiedCount: reproEvidenceVerdictEffects.unverified,
1266
+ externalEvidenceRiskCount: externalEvidenceRisks.length,
1267
+ });
1268
+ const outputWithReceiptPaths = {
1269
+ ...output,
1270
+ completion_verdict: completionVerdict,
1271
+ failure_fingerprint: failureFingerprint,
1272
+ repeated_failure_summary: repeatedFailureSummary,
1273
+ results,
1274
+ evidence_model: createVerifyEvidenceModel({
1275
+ report,
1276
+ results,
1277
+ verificationPlanId: output.verification_plan_id,
1278
+ verdict: completionVerdict,
1279
+ sourceAnchorRisks,
1280
+ scopeDiffRisks,
1281
+ repeatedFailureRisks: finalRepeatedFailureRisks,
1282
+ validationRatchetRisks,
1283
+ reproEvidence,
1284
+ reproEvidenceRisks,
1285
+ externalChecks,
1286
+ externalEvidenceRisks,
1287
+ }),
1288
+ };
466
1289
  const manifest = {
467
1290
  schema_version: '1',
468
1291
  command: 'verify',
469
- reason: output.reason,
470
- reasons: output.reasons,
471
- plan_source: output.plan_source,
472
- status: output.status,
473
- summary: output.summary,
1292
+ reason: outputWithReceiptPaths.reason,
1293
+ reasons: outputWithReceiptPaths.reasons,
1294
+ plan_source: outputWithReceiptPaths.plan_source,
1295
+ verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1296
+ status: outputWithReceiptPaths.status,
1297
+ completion_verdict: outputWithReceiptPaths.completion_verdict,
1298
+ evidence_model: outputWithReceiptPaths.evidence_model,
1299
+ failure_fingerprint: outputWithReceiptPaths.failure_fingerprint,
1300
+ repeated_failure_summary: outputWithReceiptPaths.repeated_failure_summary,
1301
+ summary: outputWithReceiptPaths.summary,
1302
+ ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
1303
+ ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
474
1304
  receipts,
475
1305
  };
476
1306
  writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
@@ -478,27 +1308,93 @@ function writeVerifyRunReceipts(projectRoot, output) {
478
1308
  schema_version: '1',
479
1309
  command: 'verify',
480
1310
  kind: 'verify_run_summary',
481
- reason: output.reason,
482
- reasons: output.reasons,
483
- plan_source: output.plan_source,
484
- status: output.status,
485
- summary: output.summary,
1311
+ reason: outputWithReceiptPaths.reason,
1312
+ reasons: outputWithReceiptPaths.reasons,
1313
+ plan_source: outputWithReceiptPaths.plan_source,
1314
+ verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1315
+ status: outputWithReceiptPaths.status,
1316
+ completion_verdict: outputWithReceiptPaths.completion_verdict,
1317
+ evidence_model: outputWithReceiptPaths.evidence_model,
1318
+ failure_fingerprint: outputWithReceiptPaths.failure_fingerprint,
1319
+ repeated_failure_summary: outputWithReceiptPaths.repeated_failure_summary,
1320
+ summary: outputWithReceiptPaths.summary,
1321
+ ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
1322
+ ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
486
1323
  run_dir: toPosixPath(VERIFY_RUN_DIR),
487
- manifest_path: toPosixPath(path.join(VERIFY_RUN_DIR, 'manifest.json')),
1324
+ manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
488
1325
  };
489
1326
  writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
1327
+ return outputWithReceiptPaths;
490
1328
  }
491
- async function createVerifyOutput(input, planSource, projectRoot, lang) {
1329
+ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
492
1330
  const contract = readCommandContract(projectRoot);
493
1331
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
1332
+ const verificationPlanId = createVerificationPlanId(report, contract);
494
1333
  const scheduledIntents = new Set(report.schedule.entries.map((entry) => entry.intent));
495
1334
  const scheduledTestTargets = testTargetsByScheduledIntent(report);
1335
+ const sourceAnchorRisks = await readLocalSourceAnchorVerdictRisks(projectRoot, report.files);
1336
+ const scopeDiffRisks = createScopeDiffRisks(input.classificationReport);
1337
+ const validationRatchetRisks = createValidationRatchetRisks(input.classificationReport, projectRoot);
1338
+ const validationRatchetVerdictEffects = countValidationRatchetVerdictEffects(validationRatchetRisks);
1339
+ const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, { verificationPlanId });
1340
+ const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
1341
+ const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
496
1342
  const results = [];
497
1343
  for (const entry of report.schedule.entries) {
498
- results.push(await runVerificationIntent(entry.intent, lang, scheduledTestTargets.get(entry.intent) ?? []));
1344
+ results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
499
1345
  }
500
1346
  results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
501
1347
  const summary = summarizeResults(results);
1348
+ const status = getVerificationStatus(summary);
1349
+ const previousVerifyLatest = readPreviousVerifyLatestSummary(projectRoot);
1350
+ const failureFingerprint = createFailureFingerprintForVerify({
1351
+ verificationPlanId,
1352
+ report,
1353
+ results,
1354
+ riskCodes: riskCodesForFailureFingerprint({
1355
+ sourceAnchorRisks,
1356
+ scopeDiffRisks,
1357
+ validationRatchetRisks,
1358
+ reproEvidenceRisks,
1359
+ externalEvidenceRisks,
1360
+ results,
1361
+ }),
1362
+ });
1363
+ const repeatedFailureRisks = createRepeatedFailureRisks({
1364
+ previousFailureFingerprint: previousVerifyLatest?.failure_fingerprint ?? null,
1365
+ previousStatus: previousVerifyLatest?.status ?? null,
1366
+ currentFailureFingerprint: failureFingerprint,
1367
+ currentStatus: status,
1368
+ });
1369
+ const completionVerdict = createCompletionVerdictForResults({
1370
+ report,
1371
+ verificationPlanId,
1372
+ summary,
1373
+ results,
1374
+ sourceAnchorRiskCount: sourceAnchorRisks.length,
1375
+ scopeDiffRiskCount: scopeDiffRisks.length,
1376
+ repeatedFailureRisks,
1377
+ validationRatchetRiskCount: validationRatchetRisks.length,
1378
+ validationRatchetContradictionCount: validationRatchetVerdictEffects.contradicted,
1379
+ reproEvidenceRiskCount: reproEvidenceRisks.length,
1380
+ reproEvidenceContradictionCount: reproEvidenceVerdictEffects.contradicted,
1381
+ reproEvidenceUnverifiedCount: reproEvidenceVerdictEffects.unverified,
1382
+ externalEvidenceRiskCount: externalEvidenceRisks.length,
1383
+ });
1384
+ const evidenceModel = createVerifyEvidenceModel({
1385
+ report,
1386
+ results,
1387
+ verificationPlanId,
1388
+ verdict: completionVerdict,
1389
+ sourceAnchorRisks,
1390
+ scopeDiffRisks,
1391
+ repeatedFailureRisks,
1392
+ validationRatchetRisks,
1393
+ reproEvidence,
1394
+ reproEvidenceRisks,
1395
+ externalChecks,
1396
+ externalEvidenceRisks,
1397
+ });
502
1398
  const output = {
503
1399
  schema_version: VERIFY_SCHEMA_VERSION,
504
1400
  command: 'verify',
@@ -506,16 +1402,25 @@ async function createVerifyOutput(input, planSource, projectRoot, lang) {
506
1402
  reason: input.reasons.join(', '),
507
1403
  reasons: input.reasons,
508
1404
  plan_source: planSource,
509
- status: getVerificationStatus(summary),
1405
+ verification_plan_id: verificationPlanId,
1406
+ status,
1407
+ completion_verdict: completionVerdict,
1408
+ evidence_model: evidenceModel,
1409
+ failure_fingerprint: failureFingerprint,
1410
+ repeated_failure_summary: null,
510
1411
  summary,
1412
+ ...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
1413
+ ...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
1414
+ run_dir: toPosixPath(VERIFY_RUN_DIR),
1415
+ manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
511
1416
  results,
512
1417
  };
513
- writeVerifyRunReceipts(projectRoot, output);
514
- return output;
1418
+ return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks);
515
1419
  }
516
1420
  async function createPlanOnlyOutput(input, projectRoot) {
517
1421
  const contract = readCommandContract(projectRoot);
518
1422
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
1423
+ const verificationPlanId = createVerificationPlanId(report, contract);
519
1424
  const localSurfaceReadModels = await readLocalPathSurfaces(projectRoot, report.files);
520
1425
  const [firstEntry] = report.schedule.entries;
521
1426
  const requirements = report.requirements.map((requirement) => {
@@ -525,7 +1430,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
525
1430
  return surfaceReadModels.length > 0 ? { ...requirement, surfaceReadModels } : requirement;
526
1431
  });
527
1432
  if (!firstEntry) {
528
- return { ...report, requirements };
1433
+ return { ...report, verification_plan_id: verificationPlanId, requirements };
529
1434
  }
530
1435
  const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
531
1436
  const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
@@ -538,6 +1443,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
538
1443
  }
539
1444
  return {
540
1445
  ...report,
1446
+ verification_plan_id: verificationPlanId,
541
1447
  requirements,
542
1448
  schedule: {
543
1449
  ...report.schedule,
@@ -555,6 +1461,7 @@ function renderVerifyOutput(output, lang) {
555
1461
  `${t(lang, 'verify.label.reason')}: ${output.reason}`,
556
1462
  `${t(lang, 'verify.label.planSource')}: ${output.plan_source ?? t(lang, 'value.none')}`,
557
1463
  `${t(lang, 'verify.label.status')}: ${output.status}`,
1464
+ `completion verdict: ${output.completion_verdict.status} (${output.completion_verdict.primary_reason})`,
558
1465
  `matched: ${output.summary.matched}`,
559
1466
  `ran: ${output.summary.ran}`,
560
1467
  `passed: ${output.summary.passed}`,
@@ -579,17 +1486,28 @@ export async function runVerify(args, reporter, lang = 'en') {
579
1486
  if (parsed.error) {
580
1487
  const message = parsed.error === 'missing_reason_value'
581
1488
  ? t(lang, 'cli.error.missingValue', { option: '--reason' })
582
- : parsed.error === 'missing_from_plan_value'
583
- ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
584
- : parsed.error === 'missing_write_plan_value'
585
- ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
586
- : parsed.error.startsWith('unexpected:')
587
- ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
588
- : t(lang, 'cli.error.unknownOption', { option: parsed.error });
1489
+ : parsed.error === 'missing_from_classification_value'
1490
+ ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
1491
+ : parsed.error === 'missing_from_plan_value'
1492
+ ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
1493
+ : parsed.error === 'missing_write_plan_value'
1494
+ ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
1495
+ : parsed.error === 'missing_repro_evidence_value'
1496
+ ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
1497
+ : parsed.error === 'missing_external_evidence_value'
1498
+ ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1499
+ : parsed.error.startsWith('unexpected:')
1500
+ ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1501
+ : t(lang, 'cli.error.unknownOption', { option: parsed.error });
589
1502
  printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
590
1503
  return 1;
591
1504
  }
592
- const selectedInputCount = [parsed.reason, parsed.fromPlan, parsed.changed ? 'changed' : undefined].filter(Boolean).length;
1505
+ const selectedInputCount = [
1506
+ parsed.reason,
1507
+ parsed.fromClassification,
1508
+ parsed.fromPlan,
1509
+ parsed.changed ? 'changed' : undefined,
1510
+ ].filter(Boolean).length;
593
1511
  if (selectedInputCount > 1) {
594
1512
  printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf verify --help', getVerifyHelp(lang), lang);
595
1513
  return 1;
@@ -606,17 +1524,27 @@ export async function runVerify(args, reporter, lang = 'en') {
606
1524
  printUsageError(reporter, t(lang, 'verify.error.planOnlyJson'), 'mf verify --help', getVerifyHelp(lang), lang);
607
1525
  return 1;
608
1526
  }
1527
+ if (parsed.planOnly && parsed.reproEvidence) {
1528
+ printUsageError(reporter, t(lang, 'verify.error.reproEvidenceRequiresRun'), 'mf verify --help', getVerifyHelp(lang), lang);
1529
+ return 1;
1530
+ }
1531
+ if (parsed.planOnly && parsed.externalEvidence) {
1532
+ printUsageError(reporter, t(lang, 'verify.error.externalEvidenceRequiresRun'), 'mf verify --help', getVerifyHelp(lang), lang);
1533
+ return 1;
1534
+ }
609
1535
  const projectRoot = resolveMustflowRoot();
610
1536
  let input;
611
1537
  let changedPlan = null;
1538
+ let reproEvidence = null;
1539
+ let externalChecks = [];
612
1540
  try {
613
1541
  if (parsed.changed) {
614
1542
  const changedInput = createInputFromChanged(projectRoot);
615
1543
  input = changedInput.input;
616
1544
  changedPlan = changedInput.plan;
617
1545
  }
618
- else if (parsed.fromPlan) {
619
- input = readInputFromPlan(projectRoot, parsed.fromPlan);
1546
+ else if (parsed.fromClassification || parsed.fromPlan) {
1547
+ input = readInputFromPlan(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
620
1548
  }
621
1549
  else {
622
1550
  input = {
@@ -627,17 +1555,32 @@ export async function runVerify(args, reporter, lang = 'en') {
627
1555
  if (parsed.writePlan && changedPlan) {
628
1556
  writeChangedPlan(projectRoot, parsed.writePlan, changedPlan);
629
1557
  }
1558
+ if (parsed.reproEvidence) {
1559
+ reproEvidence = readReproEvidenceFile(projectRoot, parsed.reproEvidence);
1560
+ }
1561
+ if (parsed.externalEvidence) {
1562
+ externalChecks = readExternalEvidenceFile(projectRoot, parsed.externalEvidence);
1563
+ }
630
1564
  }
631
1565
  catch (error) {
632
1566
  const code = error instanceof Error ? error.message : 'invalid_plan_file';
633
- printUsageError(reporter, t(lang, planErrorMessageKey(code)), 'mf verify --help', getVerifyHelp(lang), lang);
1567
+ const message = code === 'invalid_repro_evidence_file'
1568
+ ? t(lang, 'verify.error.invalid_repro_evidence_file')
1569
+ : code === 'unsupported_repro_evidence_source'
1570
+ ? t(lang, 'verify.error.unsupported_repro_evidence_source')
1571
+ : code === 'invalid_external_evidence_file'
1572
+ ? t(lang, 'verify.error.invalid_external_evidence_file')
1573
+ : code === 'unsupported_external_evidence_source'
1574
+ ? t(lang, 'verify.error.unsupported_external_evidence_source')
1575
+ : t(lang, planErrorMessageKey(code));
1576
+ printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
634
1577
  return 1;
635
1578
  }
636
1579
  if (parsed.planOnly) {
637
1580
  reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
638
1581
  return 0;
639
1582
  }
640
- const output = await createVerifyOutput(input, parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang);
1583
+ const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks);
641
1584
  if (parsed.json) {
642
1585
  reporter.stdout(JSON.stringify(output, null, 2));
643
1586
  }