mustflow 1.31.0 → 2.11.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 (64) 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 +6 -0
  5. package/dist/cli/commands/index.js +5 -0
  6. package/dist/cli/commands/run.js +4 -1
  7. package/dist/cli/commands/verify.js +488 -43
  8. package/dist/cli/i18n/en.js +61 -10
  9. package/dist/cli/i18n/es.js +61 -10
  10. package/dist/cli/i18n/fr.js +61 -10
  11. package/dist/cli/i18n/hi.js +61 -10
  12. package/dist/cli/i18n/ko.js +61 -10
  13. package/dist/cli/i18n/zh.js +61 -10
  14. package/dist/cli/lib/dashboard-export.js +62 -12
  15. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  16. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  17. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  18. package/dist/cli/lib/dashboard-html/template.js +134 -0
  19. package/dist/cli/lib/dashboard-html/types.js +1 -0
  20. package/dist/cli/lib/dashboard-html.js +1 -1907
  21. package/dist/cli/lib/dashboard-locale.js +37 -0
  22. package/dist/cli/lib/local-index/constants.js +48 -0
  23. package/dist/cli/lib/local-index/index.js +2256 -0
  24. package/dist/cli/lib/local-index/sql.js +15 -0
  25. package/dist/cli/lib/local-index/types.js +1 -0
  26. package/dist/cli/lib/local-index.js +1 -1911
  27. package/dist/cli/lib/run-plan.js +76 -1
  28. package/dist/cli/lib/templates.js +18 -1
  29. package/dist/cli/lib/validation/command-intents.js +11 -0
  30. package/dist/cli/lib/validation/constants.js +238 -0
  31. package/dist/cli/lib/validation/index.js +1384 -0
  32. package/dist/cli/lib/validation/primitives.js +198 -0
  33. package/dist/cli/lib/validation/test-selection.js +95 -0
  34. package/dist/cli/lib/validation/types.js +1 -0
  35. package/dist/cli/lib/validation.js +1 -1770
  36. package/dist/core/check-issues.js +6 -0
  37. package/dist/core/completion-verdict.js +209 -0
  38. package/dist/core/contract-lint.js +221 -6
  39. package/dist/core/external-evidence.js +9 -0
  40. package/dist/core/public-json-contracts.js +21 -0
  41. package/dist/core/repeated-failure.js +17 -0
  42. package/dist/core/repro-evidence.js +53 -0
  43. package/dist/core/scope-risk.js +64 -0
  44. package/dist/core/skill-route-alignment.js +20 -0
  45. package/dist/core/source-anchor-status.js +4 -1
  46. package/dist/core/test-selection.js +3 -0
  47. package/dist/core/validation-ratchet.js +52 -0
  48. package/dist/core/verification-evidence.js +249 -0
  49. package/examples/README.md +12 -4
  50. package/package.json +1 -1
  51. package/schemas/README.md +13 -3
  52. package/schemas/change-verification-report.schema.json +16 -2
  53. package/schemas/commands.schema.json +4 -0
  54. package/schemas/contract-lint-report.schema.json +29 -0
  55. package/schemas/dashboard-export.schema.json +227 -0
  56. package/schemas/latest-run-pointer.schema.json +384 -0
  57. package/schemas/run-receipt.schema.json +4 -0
  58. package/schemas/test-selection.schema.json +81 -0
  59. package/schemas/verify-report.schema.json +361 -1
  60. package/schemas/verify-run-manifest.schema.json +410 -0
  61. package/templates/default/i18n.toml +1 -1
  62. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  63. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  64. 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 { createRepeatedFailureRisk } from '../../core/repeated-failure.js';
10
+ import { createReproEvidenceRisks, } from '../../core/repro-evidence.js';
11
+ import { createVerifyEvidenceModel } from '../../core/verification-evidence.js';
12
+ import { createScopeDiffRisks } from '../../core/scope-risk.js';
13
+ import { 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,91 @@ 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 isReproEvidenceStatus(value) {
467
+ return value === 'present' || value === 'unavailable' || value === 'missing';
468
+ }
469
+ function readOptionalString(value) {
470
+ return typeof value === 'string' && value.length > 0 ? value : null;
471
+ }
472
+ function readReproEvidenceItem(value) {
473
+ if (!isPlainRecord(value)) {
474
+ return {
475
+ status: 'missing',
476
+ summary: null,
477
+ reason: null,
478
+ };
479
+ }
480
+ if (!isReproEvidenceStatus(value.status)) {
481
+ throw new Error('invalid_repro_evidence_file');
482
+ }
483
+ return {
484
+ status: value.status,
485
+ summary: readOptionalString(value.summary),
486
+ reason: readOptionalString(value.reason),
487
+ };
488
+ }
489
+ function readReproEvidenceFile(projectRoot, inputPath) {
490
+ let parsed;
491
+ const evidencePath = resolvePlanPath(projectRoot, inputPath);
492
+ try {
493
+ parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
494
+ }
495
+ catch {
496
+ throw new Error('invalid_repro_evidence_file');
497
+ }
498
+ if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'repro-evidence') {
499
+ throw new Error('unsupported_repro_evidence_source');
500
+ }
501
+ return {
502
+ source: 'repro_first_debug',
503
+ authority: 'claim_evidence',
504
+ reported_symptom: readOptionalString(parsed.reported_symptom),
505
+ expected_behavior: readOptionalString(parsed.expected_behavior),
506
+ observed_behavior: readOptionalString(parsed.observed_behavior),
507
+ original_reproduction: readReproEvidenceItem(parsed.original_reproduction),
508
+ evidence_before_fix: readReproEvidenceItem(parsed.evidence_before_fix),
509
+ evidence_after_fix: readReproEvidenceItem(parsed.evidence_after_fix),
510
+ regression_guard: readReproEvidenceItem(parsed.regression_guard),
511
+ };
512
+ }
513
+ function readExternalEvidenceFile(projectRoot, inputPath) {
514
+ let parsed;
515
+ const evidencePath = resolvePlanPath(projectRoot, inputPath);
516
+ try {
517
+ parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
518
+ }
519
+ catch {
520
+ throw new Error('invalid_external_evidence_file');
521
+ }
522
+ if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'external-evidence') {
523
+ throw new Error('unsupported_external_evidence_source');
524
+ }
525
+ if (!Array.isArray(parsed.checks)) {
526
+ throw new Error('invalid_external_evidence_file');
527
+ }
528
+ return parsed.checks.map((check) => {
529
+ if (!isPlainRecord(check) ||
530
+ typeof check.provider !== 'string' ||
531
+ check.provider.length === 0 ||
532
+ typeof check.name !== 'string' ||
533
+ check.name.length === 0 ||
534
+ !isExternalEvidenceStatus(check.status)) {
535
+ throw new Error('invalid_external_evidence_file');
536
+ }
537
+ return {
538
+ source: 'external_ci',
539
+ authority: 'supporting_only',
540
+ provider: check.provider,
541
+ name: check.name,
542
+ status: check.status,
543
+ url: readOptionalString(check.url),
544
+ summary: readOptionalString(check.summary),
545
+ };
546
+ });
547
+ }
310
548
  function createInputFromChanged(projectRoot) {
311
549
  const plan = createClassifyOutput(projectRoot, 'changed', []);
312
550
  return {
@@ -344,6 +582,9 @@ function skippedResult(candidate) {
344
582
  reason: candidate.reason,
345
583
  detail: candidate.detail,
346
584
  exit_code: null,
585
+ verification_plan_id: null,
586
+ receipt_path: null,
587
+ receipt_sha256: null,
347
588
  receipt: null,
348
589
  };
349
590
  }
@@ -383,7 +624,7 @@ function testTargetsByScheduledIntent(report) {
383
624
  candidate.appliedTestTargets.length > 0)
384
625
  .map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
385
626
  }
386
- async function runVerificationIntent(intent, lang, testTargets = []) {
627
+ async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
387
628
  const output = createBufferedOutput();
388
629
  const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
389
630
  writeLatestReceipt: false,
@@ -413,6 +654,9 @@ async function runVerificationIntent(intent, lang, testTargets = []) {
413
654
  reason: exitCode === 0 ? null : 'run_failed',
414
655
  detail: output.stderr().trim() || null,
415
656
  exit_code: exitCode,
657
+ verification_plan_id: verificationPlanId,
658
+ receipt_path: null,
659
+ receipt_sha256: null,
416
660
  receipt,
417
661
  };
418
662
  }
@@ -442,35 +686,141 @@ function getVerificationStatus(summary) {
442
686
  }
443
687
  return 'passed';
444
688
  }
445
- function writeVerifyRunReceipts(projectRoot, output) {
689
+ function isVerificationStatus(value) {
690
+ return value === 'passed' || value === 'partial' || value === 'failed' || value === 'blocked';
691
+ }
692
+ function readPreviousVerifyLatestSummary(projectRoot) {
693
+ try {
694
+ const parsed = JSON.parse(readFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), 'utf8'));
695
+ if (parsed.command !== 'verify' ||
696
+ parsed.kind !== 'verify_run_summary' ||
697
+ typeof parsed.verification_plan_id !== 'string' ||
698
+ !isVerificationStatus(parsed.status)) {
699
+ return null;
700
+ }
701
+ return {
702
+ verification_plan_id: parsed.verification_plan_id,
703
+ status: parsed.status,
704
+ };
705
+ }
706
+ catch {
707
+ return null;
708
+ }
709
+ }
710
+ function hashTextSha256(content) {
711
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
712
+ }
713
+ function stableJson(value) {
714
+ if (Array.isArray(value)) {
715
+ return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
716
+ }
717
+ if (value && typeof value === 'object') {
718
+ const record = value;
719
+ return `{${Object.keys(record)
720
+ .sort((left, right) => left.localeCompare(right))
721
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
722
+ .join(',')}}`;
723
+ }
724
+ return JSON.stringify(value) ?? 'null';
725
+ }
726
+ function getCandidateIntentNames(report) {
727
+ return [...new Set(report.candidates.map((candidate) => candidate.intent).filter((intent) => Boolean(intent)))]
728
+ .sort((left, right) => left.localeCompare(right));
729
+ }
730
+ function createVerificationPlanId(report, contract) {
731
+ const relatedIntents = Object.fromEntries(getCandidateIntentNames(report).map((intent) => [intent, contract.intents[intent] ?? null]));
732
+ const fingerprintSource = {
733
+ schema_version: '1',
734
+ algorithm: 'mustflow.verify_plan_id.v1',
735
+ report: {
736
+ source: report.source,
737
+ files: report.files,
738
+ classification_summary: report.classification_summary,
739
+ requirements: report.requirements,
740
+ candidates: report.candidates,
741
+ gaps: report.gaps,
742
+ schedule: report.schedule,
743
+ test_selection: report.test_selection,
744
+ },
745
+ command_contract: {
746
+ defaults: contract.defaults,
747
+ resources: contract.resources,
748
+ intents: relatedIntents,
749
+ },
750
+ };
751
+ return hashTextSha256(stableJson(fingerprintSource));
752
+ }
753
+ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, repeatedFailureRisks, validationRatchetRisks, reproEvidence, externalChecks) {
446
754
  const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
447
755
  const intentDir = path.join(runDir, 'intents');
448
756
  const receipts = [];
757
+ const results = [];
449
758
  rmSync(runDir, { recursive: true, force: true });
450
759
  mkdirSync(intentDir, { recursive: true });
451
760
  for (const [index, result] of output.results.entries()) {
452
761
  let receiptPath = null;
762
+ let receiptSha256 = null;
763
+ let receipt = result.receipt;
453
764
  if (result.intent && result.receipt) {
454
765
  const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
455
766
  const absoluteReceiptPath = path.join(intentDir, fileName);
456
767
  receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
457
- writeFileSync(absoluteReceiptPath, `${JSON.stringify({ ...result.receipt, receipt_path: receiptPath }, null, 2)}\n`, 'utf8');
768
+ receipt = {
769
+ ...result.receipt,
770
+ verification_plan_id: output.verification_plan_id,
771
+ receipt_path: receiptPath,
772
+ };
773
+ const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
774
+ receiptSha256 = hashTextSha256(receiptContent);
775
+ writeFileSync(absoluteReceiptPath, receiptContent, 'utf8');
458
776
  }
459
777
  receipts.push({
460
778
  intent: result.intent,
461
779
  status: result.status,
462
780
  skipped: result.skipped,
781
+ verification_plan_id: result.skipped ? null : output.verification_plan_id,
463
782
  receipt_path: receiptPath,
783
+ receipt_sha256: receiptSha256,
784
+ });
785
+ results.push({
786
+ ...result,
787
+ verification_plan_id: result.skipped ? null : output.verification_plan_id,
788
+ receipt_path: receiptPath,
789
+ receipt_sha256: receiptSha256,
790
+ receipt,
464
791
  });
465
792
  }
793
+ const outputWithReceiptPaths = {
794
+ ...output,
795
+ results,
796
+ evidence_model: createVerifyEvidenceModel({
797
+ report,
798
+ results,
799
+ verificationPlanId: output.verification_plan_id,
800
+ verdict: output.completion_verdict,
801
+ sourceAnchorRisks,
802
+ scopeDiffRisks,
803
+ repeatedFailureRisks,
804
+ validationRatchetRisks,
805
+ reproEvidence,
806
+ reproEvidenceRisks: createReproEvidenceRisks(reproEvidence),
807
+ externalChecks,
808
+ externalEvidenceRisks: createExternalEvidenceRisks(externalChecks),
809
+ }),
810
+ };
466
811
  const manifest = {
467
812
  schema_version: '1',
468
813
  command: 'verify',
469
- reason: output.reason,
470
- reasons: output.reasons,
471
- plan_source: output.plan_source,
472
- status: output.status,
473
- summary: output.summary,
814
+ reason: outputWithReceiptPaths.reason,
815
+ reasons: outputWithReceiptPaths.reasons,
816
+ plan_source: outputWithReceiptPaths.plan_source,
817
+ verification_plan_id: outputWithReceiptPaths.verification_plan_id,
818
+ status: outputWithReceiptPaths.status,
819
+ completion_verdict: outputWithReceiptPaths.completion_verdict,
820
+ evidence_model: outputWithReceiptPaths.evidence_model,
821
+ summary: outputWithReceiptPaths.summary,
822
+ ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
823
+ ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
474
824
  receipts,
475
825
  };
476
826
  writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
@@ -478,27 +828,77 @@ function writeVerifyRunReceipts(projectRoot, output) {
478
828
  schema_version: '1',
479
829
  command: 'verify',
480
830
  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,
831
+ reason: outputWithReceiptPaths.reason,
832
+ reasons: outputWithReceiptPaths.reasons,
833
+ plan_source: outputWithReceiptPaths.plan_source,
834
+ verification_plan_id: outputWithReceiptPaths.verification_plan_id,
835
+ status: outputWithReceiptPaths.status,
836
+ completion_verdict: outputWithReceiptPaths.completion_verdict,
837
+ evidence_model: outputWithReceiptPaths.evidence_model,
838
+ summary: outputWithReceiptPaths.summary,
839
+ ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
840
+ ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
486
841
  run_dir: toPosixPath(VERIFY_RUN_DIR),
487
- manifest_path: toPosixPath(path.join(VERIFY_RUN_DIR, 'manifest.json')),
842
+ manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
488
843
  };
489
844
  writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
845
+ return outputWithReceiptPaths;
490
846
  }
491
- async function createVerifyOutput(input, planSource, projectRoot, lang) {
847
+ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
492
848
  const contract = readCommandContract(projectRoot);
493
849
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
850
+ const verificationPlanId = createVerificationPlanId(report, contract);
494
851
  const scheduledIntents = new Set(report.schedule.entries.map((entry) => entry.intent));
495
852
  const scheduledTestTargets = testTargetsByScheduledIntent(report);
853
+ const sourceAnchorRisks = await readLocalSourceAnchorVerdictRisks(projectRoot, report.files);
854
+ const scopeDiffRisks = createScopeDiffRisks(input.classificationReport);
855
+ const validationRatchetRisks = createValidationRatchetRisks(input.classificationReport, projectRoot);
856
+ const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence);
857
+ const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
496
858
  const results = [];
497
859
  for (const entry of report.schedule.entries) {
498
- results.push(await runVerificationIntent(entry.intent, lang, scheduledTestTargets.get(entry.intent) ?? []));
860
+ results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
499
861
  }
500
862
  results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
501
863
  const summary = summarizeResults(results);
864
+ const status = getVerificationStatus(summary);
865
+ const previousVerifyLatest = readPreviousVerifyLatestSummary(projectRoot);
866
+ const repeatedFailureRisk = createRepeatedFailureRisk({
867
+ previousVerificationPlanId: previousVerifyLatest?.verification_plan_id ?? null,
868
+ previousStatus: previousVerifyLatest?.status ?? null,
869
+ currentVerificationPlanId: verificationPlanId,
870
+ currentStatus: status,
871
+ });
872
+ const repeatedFailureRisks = repeatedFailureRisk ? [repeatedFailureRisk] : [];
873
+ const completionVerdict = createVerifyCompletionVerdict({
874
+ verificationPlanId,
875
+ matchedIntents: summary.matched,
876
+ ranIntents: summary.ran,
877
+ passedIntents: summary.passed,
878
+ failedIntents: summary.failed,
879
+ skippedIntents: summary.skipped,
880
+ receiptCount: results.filter((result) => result.receipt !== null).length,
881
+ sourceAnchorRiskCount: sourceAnchorRisks.length,
882
+ scopeDiffRiskCount: scopeDiffRisks.length,
883
+ repeatedFailureCount: repeatedFailureRisks.length,
884
+ validationRatchetRiskCount: validationRatchetRisks.length,
885
+ reproEvidenceRiskCount: reproEvidenceRisks.length,
886
+ externalEvidenceRiskCount: externalEvidenceRisks.length,
887
+ });
888
+ const evidenceModel = createVerifyEvidenceModel({
889
+ report,
890
+ results,
891
+ verificationPlanId,
892
+ verdict: completionVerdict,
893
+ sourceAnchorRisks,
894
+ scopeDiffRisks,
895
+ repeatedFailureRisks,
896
+ validationRatchetRisks,
897
+ reproEvidence,
898
+ reproEvidenceRisks,
899
+ externalChecks,
900
+ externalEvidenceRisks,
901
+ });
502
902
  const output = {
503
903
  schema_version: VERIFY_SCHEMA_VERSION,
504
904
  command: 'verify',
@@ -506,16 +906,23 @@ async function createVerifyOutput(input, planSource, projectRoot, lang) {
506
906
  reason: input.reasons.join(', '),
507
907
  reasons: input.reasons,
508
908
  plan_source: planSource,
509
- status: getVerificationStatus(summary),
909
+ verification_plan_id: verificationPlanId,
910
+ status,
911
+ completion_verdict: completionVerdict,
912
+ evidence_model: evidenceModel,
510
913
  summary,
914
+ ...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
915
+ ...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
916
+ run_dir: toPosixPath(VERIFY_RUN_DIR),
917
+ manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
511
918
  results,
512
919
  };
513
- writeVerifyRunReceipts(projectRoot, output);
514
- return output;
920
+ return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, repeatedFailureRisks, validationRatchetRisks, reproEvidence, externalChecks);
515
921
  }
516
922
  async function createPlanOnlyOutput(input, projectRoot) {
517
923
  const contract = readCommandContract(projectRoot);
518
924
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
925
+ const verificationPlanId = createVerificationPlanId(report, contract);
519
926
  const localSurfaceReadModels = await readLocalPathSurfaces(projectRoot, report.files);
520
927
  const [firstEntry] = report.schedule.entries;
521
928
  const requirements = report.requirements.map((requirement) => {
@@ -525,7 +932,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
525
932
  return surfaceReadModels.length > 0 ? { ...requirement, surfaceReadModels } : requirement;
526
933
  });
527
934
  if (!firstEntry) {
528
- return { ...report, requirements };
935
+ return { ...report, verification_plan_id: verificationPlanId, requirements };
529
936
  }
530
937
  const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
531
938
  const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
@@ -538,6 +945,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
538
945
  }
539
946
  return {
540
947
  ...report,
948
+ verification_plan_id: verificationPlanId,
541
949
  requirements,
542
950
  schedule: {
543
951
  ...report.schedule,
@@ -555,6 +963,7 @@ function renderVerifyOutput(output, lang) {
555
963
  `${t(lang, 'verify.label.reason')}: ${output.reason}`,
556
964
  `${t(lang, 'verify.label.planSource')}: ${output.plan_source ?? t(lang, 'value.none')}`,
557
965
  `${t(lang, 'verify.label.status')}: ${output.status}`,
966
+ `completion verdict: ${output.completion_verdict.status} (${output.completion_verdict.primary_reason})`,
558
967
  `matched: ${output.summary.matched}`,
559
968
  `ran: ${output.summary.ran}`,
560
969
  `passed: ${output.summary.passed}`,
@@ -579,17 +988,28 @@ export async function runVerify(args, reporter, lang = 'en') {
579
988
  if (parsed.error) {
580
989
  const message = parsed.error === 'missing_reason_value'
581
990
  ? 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 });
991
+ : parsed.error === 'missing_from_classification_value'
992
+ ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
993
+ : parsed.error === 'missing_from_plan_value'
994
+ ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
995
+ : parsed.error === 'missing_write_plan_value'
996
+ ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
997
+ : parsed.error === 'missing_repro_evidence_value'
998
+ ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
999
+ : parsed.error === 'missing_external_evidence_value'
1000
+ ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1001
+ : parsed.error.startsWith('unexpected:')
1002
+ ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1003
+ : t(lang, 'cli.error.unknownOption', { option: parsed.error });
589
1004
  printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
590
1005
  return 1;
591
1006
  }
592
- const selectedInputCount = [parsed.reason, parsed.fromPlan, parsed.changed ? 'changed' : undefined].filter(Boolean).length;
1007
+ const selectedInputCount = [
1008
+ parsed.reason,
1009
+ parsed.fromClassification,
1010
+ parsed.fromPlan,
1011
+ parsed.changed ? 'changed' : undefined,
1012
+ ].filter(Boolean).length;
593
1013
  if (selectedInputCount > 1) {
594
1014
  printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf verify --help', getVerifyHelp(lang), lang);
595
1015
  return 1;
@@ -606,17 +1026,27 @@ export async function runVerify(args, reporter, lang = 'en') {
606
1026
  printUsageError(reporter, t(lang, 'verify.error.planOnlyJson'), 'mf verify --help', getVerifyHelp(lang), lang);
607
1027
  return 1;
608
1028
  }
1029
+ if (parsed.planOnly && parsed.reproEvidence) {
1030
+ printUsageError(reporter, t(lang, 'verify.error.reproEvidenceRequiresRun'), 'mf verify --help', getVerifyHelp(lang), lang);
1031
+ return 1;
1032
+ }
1033
+ if (parsed.planOnly && parsed.externalEvidence) {
1034
+ printUsageError(reporter, t(lang, 'verify.error.externalEvidenceRequiresRun'), 'mf verify --help', getVerifyHelp(lang), lang);
1035
+ return 1;
1036
+ }
609
1037
  const projectRoot = resolveMustflowRoot();
610
1038
  let input;
611
1039
  let changedPlan = null;
1040
+ let reproEvidence = null;
1041
+ let externalChecks = [];
612
1042
  try {
613
1043
  if (parsed.changed) {
614
1044
  const changedInput = createInputFromChanged(projectRoot);
615
1045
  input = changedInput.input;
616
1046
  changedPlan = changedInput.plan;
617
1047
  }
618
- else if (parsed.fromPlan) {
619
- input = readInputFromPlan(projectRoot, parsed.fromPlan);
1048
+ else if (parsed.fromClassification || parsed.fromPlan) {
1049
+ input = readInputFromPlan(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
620
1050
  }
621
1051
  else {
622
1052
  input = {
@@ -627,17 +1057,32 @@ export async function runVerify(args, reporter, lang = 'en') {
627
1057
  if (parsed.writePlan && changedPlan) {
628
1058
  writeChangedPlan(projectRoot, parsed.writePlan, changedPlan);
629
1059
  }
1060
+ if (parsed.reproEvidence) {
1061
+ reproEvidence = readReproEvidenceFile(projectRoot, parsed.reproEvidence);
1062
+ }
1063
+ if (parsed.externalEvidence) {
1064
+ externalChecks = readExternalEvidenceFile(projectRoot, parsed.externalEvidence);
1065
+ }
630
1066
  }
631
1067
  catch (error) {
632
1068
  const code = error instanceof Error ? error.message : 'invalid_plan_file';
633
- printUsageError(reporter, t(lang, planErrorMessageKey(code)), 'mf verify --help', getVerifyHelp(lang), lang);
1069
+ const message = code === 'invalid_repro_evidence_file'
1070
+ ? t(lang, 'verify.error.invalid_repro_evidence_file')
1071
+ : code === 'unsupported_repro_evidence_source'
1072
+ ? t(lang, 'verify.error.unsupported_repro_evidence_source')
1073
+ : code === 'invalid_external_evidence_file'
1074
+ ? t(lang, 'verify.error.invalid_external_evidence_file')
1075
+ : code === 'unsupported_external_evidence_source'
1076
+ ? t(lang, 'verify.error.unsupported_external_evidence_source')
1077
+ : t(lang, planErrorMessageKey(code));
1078
+ printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
634
1079
  return 1;
635
1080
  }
636
1081
  if (parsed.planOnly) {
637
1082
  reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
638
1083
  return 0;
639
1084
  }
640
- const output = await createVerifyOutput(input, parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang);
1085
+ const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks);
641
1086
  if (parsed.json) {
642
1087
  reporter.stdout(JSON.stringify(output, null, 2));
643
1088
  }