mustflow 1.30.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 (82) hide show
  1. package/README.md +35 -11
  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 +224 -48
  7. package/dist/cli/commands/upgrade.js +65 -0
  8. package/dist/cli/commands/verify.js +550 -33
  9. package/dist/cli/i18n/en.js +73 -10
  10. package/dist/cli/i18n/es.js +73 -10
  11. package/dist/cli/i18n/fr.js +73 -10
  12. package/dist/cli/i18n/hi.js +73 -10
  13. package/dist/cli/i18n/ko.js +73 -10
  14. package/dist/cli/i18n/zh.js +73 -10
  15. package/dist/cli/index.js +27 -46
  16. package/dist/cli/lib/command-registry.js +5 -0
  17. package/dist/cli/lib/dashboard-export.js +62 -12
  18. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  20. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  21. package/dist/cli/lib/dashboard-html/template.js +134 -0
  22. package/dist/cli/lib/dashboard-html/types.js +1 -0
  23. package/dist/cli/lib/dashboard-html.js +1 -1907
  24. package/dist/cli/lib/dashboard-locale.js +37 -0
  25. package/dist/cli/lib/local-index/constants.js +48 -0
  26. package/dist/cli/lib/local-index/index.js +2256 -0
  27. package/dist/cli/lib/local-index/sql.js +15 -0
  28. package/dist/cli/lib/local-index/types.js +1 -0
  29. package/dist/cli/lib/local-index.js +1 -1908
  30. package/dist/cli/lib/reporter.js +6 -0
  31. package/dist/cli/lib/run-plan.js +96 -4
  32. package/dist/cli/lib/templates.js +18 -1
  33. package/dist/cli/lib/validation/command-intents.js +11 -0
  34. package/dist/cli/lib/validation/constants.js +238 -0
  35. package/dist/cli/lib/validation/index.js +1384 -0
  36. package/dist/cli/lib/validation/primitives.js +198 -0
  37. package/dist/cli/lib/validation/test-selection.js +95 -0
  38. package/dist/cli/lib/validation/types.js +1 -0
  39. package/dist/cli/lib/validation.js +1 -1661
  40. package/dist/core/bounded-output.js +38 -0
  41. package/dist/core/change-classification.js +6 -2
  42. package/dist/core/change-verification.js +240 -6
  43. package/dist/core/check-issues.js +12 -0
  44. package/dist/core/command-contract-validation.js +20 -0
  45. package/dist/core/command-effects.js +13 -0
  46. package/dist/core/completion-verdict.js +209 -0
  47. package/dist/core/contract-lint.js +316 -7
  48. package/dist/core/dashboard-verification.js +8 -0
  49. package/dist/core/external-evidence.js +9 -0
  50. package/dist/core/public-json-contracts.js +28 -0
  51. package/dist/core/repeated-failure.js +17 -0
  52. package/dist/core/repro-evidence.js +53 -0
  53. package/dist/core/run-performance-history.js +307 -0
  54. package/dist/core/run-profile.js +87 -0
  55. package/dist/core/run-receipt.js +171 -4
  56. package/dist/core/run-write-drift.js +18 -2
  57. package/dist/core/scope-risk.js +64 -0
  58. package/dist/core/skill-route-alignment.js +110 -0
  59. package/dist/core/source-anchor-status.js +4 -1
  60. package/dist/core/test-selection.js +227 -0
  61. package/dist/core/validation-ratchet.js +52 -0
  62. package/dist/core/verification-decision-graph.js +67 -0
  63. package/dist/core/verification-evidence.js +249 -0
  64. package/dist/core/verification-scheduler.js +96 -2
  65. package/examples/README.md +12 -4
  66. package/package.json +1 -1
  67. package/schemas/README.md +18 -4
  68. package/schemas/change-verification-report.schema.json +169 -5
  69. package/schemas/commands.schema.json +51 -1
  70. package/schemas/contract-lint-report.schema.json +80 -0
  71. package/schemas/dashboard-export.schema.json +500 -0
  72. package/schemas/explain-report.schema.json +2 -0
  73. package/schemas/latest-run-pointer.schema.json +384 -0
  74. package/schemas/run-receipt.schema.json +113 -0
  75. package/schemas/test-selection.schema.json +81 -0
  76. package/schemas/verify-report.schema.json +361 -1
  77. package/schemas/verify-run-manifest.schema.json +410 -0
  78. package/templates/default/common/.mustflow/config/commands.toml +1 -1
  79. package/templates/default/i18n.toml +1 -1
  80. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  81. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  82. package/templates/default/manifest.toml +29 -2
@@ -1,14 +1,25 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { createHash } from 'node:crypto';
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';
20
+ const VERIFY_RUN_DIR = path.join('.mustflow', 'state', 'runs', 'verify-latest');
21
+ const VERIFY_MANIFEST_PATH = path.join(VERIFY_RUN_DIR, 'manifest.json');
22
+ const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
12
23
  function createBufferedOutput() {
13
24
  const stdout = [];
14
25
  const stderr = [];
@@ -31,13 +42,16 @@ function createBufferedOutput() {
31
42
  }
32
43
  export function getVerifyHelp(lang = 'en') {
33
44
  return renderHelp({
34
- 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]',
35
46
  summary: t(lang, 'verify.help.summary'),
36
47
  options: [
37
48
  { label: '--reason <event>', description: t(lang, 'verify.help.option.reason') },
49
+ { label: '--from-classification <path>', description: t(lang, 'verify.help.option.fromClassification') },
38
50
  { label: '--from-plan <path>', description: t(lang, 'verify.help.option.fromPlan') },
39
51
  { label: '--changed', description: t(lang, 'verify.help.option.changed') },
40
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') },
41
55
  { label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
42
56
  { label: '--json', description: t(lang, 'cli.option.json') },
43
57
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
@@ -46,9 +60,9 @@ export function getVerifyHelp(lang = 'en') {
46
60
  'mf verify --reason code_change',
47
61
  'mf verify --reason docs_change --json',
48
62
  'mf verify --reason docs_change --plan-only --json',
49
- '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',
50
65
  'mf verify --changed --plan-only --json',
51
- 'mf verify --changed --write-plan .mustflow/state/change-plan.json --json',
52
66
  'mf verify --reason mustflow_docs_change',
53
67
  ],
54
68
  exitCodes: [
@@ -59,8 +73,11 @@ export function getVerifyHelp(lang = 'en') {
59
73
  }
60
74
  function parseVerifyArgs(args) {
61
75
  let reason;
76
+ let fromClassification;
62
77
  let fromPlan;
63
78
  let writePlan;
79
+ let reproEvidence;
80
+ let externalEvidence;
64
81
  let json = false;
65
82
  let planOnly = false;
66
83
  let changed = false;
@@ -90,21 +107,86 @@ function parseVerifyArgs(args) {
90
107
  if (arg === '--from-plan') {
91
108
  const value = args[index + 1];
92
109
  if (!value || value.startsWith('-')) {
93
- return { json, planOnly, changed, reason, fromPlan, error: 'missing_from_plan_value' };
110
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
94
111
  }
95
112
  fromPlan = value;
96
113
  index += 1;
97
114
  continue;
98
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
+ }
99
133
  if (arg === '--write-plan') {
100
134
  const value = args[index + 1];
101
135
  if (!value || value.startsWith('-')) {
102
- 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
+ };
103
146
  }
104
147
  writePlan = value;
105
148
  index += 1;
106
149
  continue;
107
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
+ }
108
190
  if (arg.startsWith('--reason=')) {
109
191
  const value = arg.slice('--reason='.length);
110
192
  if (value.length === 0) {
@@ -116,29 +198,109 @@ function parseVerifyArgs(args) {
116
198
  if (arg.startsWith('--from-plan=')) {
117
199
  const value = arg.slice('--from-plan='.length);
118
200
  if (value.length === 0) {
119
- return { json, planOnly, changed, reason, fromPlan, error: 'missing_from_plan_value' };
201
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
120
202
  }
121
203
  fromPlan = value;
122
204
  continue;
123
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
+ }
124
222
  if (arg.startsWith('--write-plan=')) {
125
223
  const value = arg.slice('--write-plan='.length);
126
224
  if (value.length === 0) {
127
- 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
+ };
128
235
  }
129
236
  writePlan = value;
130
237
  continue;
131
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
+ }
132
276
  if (arg.startsWith('-')) {
133
- return { json, planOnly, changed, reason, fromPlan, writePlan, error: arg };
277
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence, error: arg };
134
278
  }
135
- 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
+ };
136
291
  }
137
- return { json, planOnly, changed, reason, fromPlan, writePlan };
292
+ return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence };
138
293
  }
139
294
  function uniqueStrings(values) {
140
295
  return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
141
296
  }
297
+ function toPosixPath(value) {
298
+ return value.split(path.sep).join('/');
299
+ }
300
+ function sanitizeIntentFilePart(value) {
301
+ const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
302
+ return sanitized.length > 0 ? sanitized.slice(0, 80) : 'intent';
303
+ }
142
304
  function readStringArray(value) {
143
305
  if (!Array.isArray(value)) {
144
306
  return [];
@@ -298,6 +460,91 @@ export function readInputFromPlan(projectRoot, inputPath) {
298
460
  classificationReport,
299
461
  };
300
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
+ }
301
548
  function createInputFromChanged(projectRoot) {
302
549
  const plan = createClassifyOutput(projectRoot, 'changed', []);
303
550
  return {
@@ -335,6 +582,9 @@ function skippedResult(candidate) {
335
582
  reason: candidate.reason,
336
583
  detail: candidate.detail,
337
584
  exit_code: null,
585
+ verification_plan_id: null,
586
+ receipt_path: null,
587
+ receipt_sha256: null,
338
588
  receipt: null,
339
589
  };
340
590
  }
@@ -343,13 +593,17 @@ function candidateResultKey(candidate) {
343
593
  ? `intent:${candidate.intent}`
344
594
  : `missing:${candidate.reason}:${candidate.skipReason ?? ''}:${candidate.detail ?? ''}`;
345
595
  }
346
- function createSkippedResults(candidates, scheduledIntents) {
596
+ function createSkippedResults(candidates, scheduledIntents, gaps) {
347
597
  const seen = new Set();
348
598
  const results = [];
599
+ const activeGapReasons = new Set(gaps.map((gap) => gap.reason));
349
600
  for (const candidate of candidates) {
350
601
  if (candidate.status === 'runnable' || (candidate.intent && scheduledIntents.has(candidate.intent))) {
351
602
  continue;
352
603
  }
604
+ if (candidate.candidateState === 'gap' && !activeGapReasons.has(candidate.reason)) {
605
+ continue;
606
+ }
353
607
  const key = candidateResultKey(candidate);
354
608
  if (seen.has(key)) {
355
609
  continue;
@@ -363,9 +617,19 @@ function createSkippedResults(candidates, scheduledIntents) {
363
617
  }
364
618
  return results;
365
619
  }
366
- async function runVerificationIntent(intent, lang) {
620
+ function testTargetsByScheduledIntent(report) {
621
+ return new Map(report.test_selection.selected
622
+ .filter((candidate) => candidate.status === 'runnable' &&
623
+ candidate.testTargetsApplied &&
624
+ candidate.appliedTestTargets.length > 0)
625
+ .map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
626
+ }
627
+ async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
367
628
  const output = createBufferedOutput();
368
- const exitCode = await runRun([intent, '--json'], output.reporter, lang);
629
+ const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
630
+ writeLatestReceipt: false,
631
+ testTargets,
632
+ });
369
633
  const rawStdout = output.stdout().trim();
370
634
  let receipt = null;
371
635
  let status = exitCode === 0 ? 'passed' : 'failed';
@@ -390,6 +654,9 @@ async function runVerificationIntent(intent, lang) {
390
654
  reason: exitCode === 0 ? null : 'run_failed',
391
655
  detail: output.stderr().trim() || null,
392
656
  exit_code: exitCode,
657
+ verification_plan_id: verificationPlanId,
658
+ receipt_path: null,
659
+ receipt_sha256: null,
393
660
  receipt,
394
661
  };
395
662
  }
@@ -419,31 +686,243 @@ function getVerificationStatus(summary) {
419
686
  }
420
687
  return 'passed';
421
688
  }
422
- async function createVerifyOutput(input, planSource, projectRoot, lang) {
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) {
754
+ const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
755
+ const intentDir = path.join(runDir, 'intents');
756
+ const receipts = [];
757
+ const results = [];
758
+ rmSync(runDir, { recursive: true, force: true });
759
+ mkdirSync(intentDir, { recursive: true });
760
+ for (const [index, result] of output.results.entries()) {
761
+ let receiptPath = null;
762
+ let receiptSha256 = null;
763
+ let receipt = result.receipt;
764
+ if (result.intent && result.receipt) {
765
+ const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
766
+ const absoluteReceiptPath = path.join(intentDir, fileName);
767
+ receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
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');
776
+ }
777
+ receipts.push({
778
+ intent: result.intent,
779
+ status: result.status,
780
+ skipped: result.skipped,
781
+ verification_plan_id: result.skipped ? null : output.verification_plan_id,
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,
791
+ });
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
+ };
811
+ const manifest = {
812
+ schema_version: '1',
813
+ command: 'verify',
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 } : {}),
824
+ receipts,
825
+ };
826
+ writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
827
+ const latest = {
828
+ schema_version: '1',
829
+ command: 'verify',
830
+ kind: 'verify_run_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 } : {}),
841
+ run_dir: toPosixPath(VERIFY_RUN_DIR),
842
+ manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
843
+ };
844
+ writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
845
+ return outputWithReceiptPaths;
846
+ }
847
+ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
423
848
  const contract = readCommandContract(projectRoot);
424
849
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
850
+ const verificationPlanId = createVerificationPlanId(report, contract);
425
851
  const scheduledIntents = new Set(report.schedule.entries.map((entry) => entry.intent));
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);
426
858
  const results = [];
427
859
  for (const entry of report.schedule.entries) {
428
- results.push(await runVerificationIntent(entry.intent, lang));
860
+ results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
429
861
  }
430
- results.push(...createSkippedResults(report.candidates, scheduledIntents));
862
+ results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
431
863
  const summary = summarizeResults(results);
432
- return {
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
+ });
902
+ const output = {
433
903
  schema_version: VERIFY_SCHEMA_VERSION,
434
904
  command: 'verify',
435
905
  mustflow_root: projectRoot,
436
906
  reason: input.reasons.join(', '),
437
907
  reasons: input.reasons,
438
908
  plan_source: planSource,
439
- status: getVerificationStatus(summary),
909
+ verification_plan_id: verificationPlanId,
910
+ status,
911
+ completion_verdict: completionVerdict,
912
+ evidence_model: evidenceModel,
440
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),
441
918
  results,
442
919
  };
920
+ return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, repeatedFailureRisks, validationRatchetRisks, reproEvidence, externalChecks);
443
921
  }
444
922
  async function createPlanOnlyOutput(input, projectRoot) {
445
923
  const contract = readCommandContract(projectRoot);
446
924
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
925
+ const verificationPlanId = createVerificationPlanId(report, contract);
447
926
  const localSurfaceReadModels = await readLocalPathSurfaces(projectRoot, report.files);
448
927
  const [firstEntry] = report.schedule.entries;
449
928
  const requirements = report.requirements.map((requirement) => {
@@ -453,7 +932,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
453
932
  return surfaceReadModels.length > 0 ? { ...requirement, surfaceReadModels } : requirement;
454
933
  });
455
934
  if (!firstEntry) {
456
- return { ...report, requirements };
935
+ return { ...report, verification_plan_id: verificationPlanId, requirements };
457
936
  }
458
937
  const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
459
938
  const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
@@ -466,6 +945,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
466
945
  }
467
946
  return {
468
947
  ...report,
948
+ verification_plan_id: verificationPlanId,
469
949
  requirements,
470
950
  schedule: {
471
951
  ...report.schedule,
@@ -483,6 +963,7 @@ function renderVerifyOutput(output, lang) {
483
963
  `${t(lang, 'verify.label.reason')}: ${output.reason}`,
484
964
  `${t(lang, 'verify.label.planSource')}: ${output.plan_source ?? t(lang, 'value.none')}`,
485
965
  `${t(lang, 'verify.label.status')}: ${output.status}`,
966
+ `completion verdict: ${output.completion_verdict.status} (${output.completion_verdict.primary_reason})`,
486
967
  `matched: ${output.summary.matched}`,
487
968
  `ran: ${output.summary.ran}`,
488
969
  `passed: ${output.summary.passed}`,
@@ -507,17 +988,28 @@ export async function runVerify(args, reporter, lang = 'en') {
507
988
  if (parsed.error) {
508
989
  const message = parsed.error === 'missing_reason_value'
509
990
  ? t(lang, 'cli.error.missingValue', { option: '--reason' })
510
- : parsed.error === 'missing_from_plan_value'
511
- ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
512
- : parsed.error === 'missing_write_plan_value'
513
- ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
514
- : parsed.error.startsWith('unexpected:')
515
- ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
516
- : 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 });
517
1004
  printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
518
1005
  return 1;
519
1006
  }
520
- 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;
521
1013
  if (selectedInputCount > 1) {
522
1014
  printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf verify --help', getVerifyHelp(lang), lang);
523
1015
  return 1;
@@ -534,17 +1026,27 @@ export async function runVerify(args, reporter, lang = 'en') {
534
1026
  printUsageError(reporter, t(lang, 'verify.error.planOnlyJson'), 'mf verify --help', getVerifyHelp(lang), lang);
535
1027
  return 1;
536
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
+ }
537
1037
  const projectRoot = resolveMustflowRoot();
538
1038
  let input;
539
1039
  let changedPlan = null;
1040
+ let reproEvidence = null;
1041
+ let externalChecks = [];
540
1042
  try {
541
1043
  if (parsed.changed) {
542
1044
  const changedInput = createInputFromChanged(projectRoot);
543
1045
  input = changedInput.input;
544
1046
  changedPlan = changedInput.plan;
545
1047
  }
546
- else if (parsed.fromPlan) {
547
- input = readInputFromPlan(projectRoot, parsed.fromPlan);
1048
+ else if (parsed.fromClassification || parsed.fromPlan) {
1049
+ input = readInputFromPlan(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
548
1050
  }
549
1051
  else {
550
1052
  input = {
@@ -555,17 +1057,32 @@ export async function runVerify(args, reporter, lang = 'en') {
555
1057
  if (parsed.writePlan && changedPlan) {
556
1058
  writeChangedPlan(projectRoot, parsed.writePlan, changedPlan);
557
1059
  }
1060
+ if (parsed.reproEvidence) {
1061
+ reproEvidence = readReproEvidenceFile(projectRoot, parsed.reproEvidence);
1062
+ }
1063
+ if (parsed.externalEvidence) {
1064
+ externalChecks = readExternalEvidenceFile(projectRoot, parsed.externalEvidence);
1065
+ }
558
1066
  }
559
1067
  catch (error) {
560
1068
  const code = error instanceof Error ? error.message : 'invalid_plan_file';
561
- 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);
562
1079
  return 1;
563
1080
  }
564
1081
  if (parsed.planOnly) {
565
1082
  reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
566
1083
  return 0;
567
1084
  }
568
- 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);
569
1086
  if (parsed.json) {
570
1087
  reporter.stdout(JSON.stringify(output, null, 2));
571
1088
  }