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,8 +1,11 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readPositiveInteger, readString, readStringArray, } from './config-loading.js';
2
- import { evaluateCommandIntentEligibility } from './command-intent-eligibility.js';
4
+ import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
3
5
  import { commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
4
6
  import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
5
7
  import { listChangeClassificationValidationReasons } from './change-classification.js';
8
+ import { parseSkillIndexRoutes } from './skill-route-alignment.js';
6
9
  const CONTRACT_LINT_SOURCE_FILES = [
7
10
  '.mustflow/config/commands.toml',
8
11
  '.mustflow/docs/agent-workflow.md',
@@ -41,9 +44,20 @@ const RELEASE_SENSITIVE_REASONS = new Set([
41
44
  'release_risk',
42
45
  'template_version_change',
43
46
  ]);
47
+ const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
48
+ const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
49
+ const CHANGE_CLASSIFICATION_SOURCE_PATH = 'src/core/change-classification.ts';
50
+ const AGENT_WORKFLOW_PATH = '.mustflow/docs/agent-workflow.md';
51
+ const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
52
+ const MAKEFILE_CANDIDATES = ['Makefile', 'makefile'];
53
+ const JUSTFILE_CANDIDATES = ['justfile', 'Justfile'];
44
54
  function uniqueSorted(values) {
45
55
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
46
56
  }
57
+ function intersectSorted(left, right) {
58
+ const rightSet = new Set(right);
59
+ return uniqueSorted(left.filter((value) => rightSet.has(value)));
60
+ }
47
61
  function readBoolean(intent, key) {
48
62
  const value = intent[key];
49
63
  return typeof value === 'boolean' ? value : null;
@@ -56,6 +70,195 @@ function writesAreValid(intent) {
56
70
  const value = intent.writes;
57
71
  return value === undefined || (Array.isArray(value) && value.every((entry) => typeof entry === 'string'));
58
72
  }
73
+ function readStringList(value) {
74
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
75
+ return null;
76
+ }
77
+ return [...value];
78
+ }
79
+ function normalizeCommandName(value) {
80
+ return path.basename(value).replace(/\.(?:cmd|exe)$/iu, '').toLowerCase();
81
+ }
82
+ function readPackageScriptReference(intent) {
83
+ const argv = readStringList(intent.argv);
84
+ if (!argv || argv.length < 3) {
85
+ return null;
86
+ }
87
+ const runner = normalizeCommandName(argv[0]);
88
+ if (!PACKAGE_SCRIPT_RUNNERS.has(runner) || argv[1] !== 'run') {
89
+ return null;
90
+ }
91
+ const scriptName = argv[2];
92
+ return scriptName && !scriptName.startsWith('-') ? scriptName : null;
93
+ }
94
+ function resolveIntentCwd(projectRoot, intent) {
95
+ const cwd = readString(intent, 'cwd') ?? '.';
96
+ const root = path.resolve(projectRoot);
97
+ const resolved = path.resolve(root, cwd);
98
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
99
+ return null;
100
+ }
101
+ return resolved;
102
+ }
103
+ function toProjectRelativePath(projectRoot, absolutePath) {
104
+ const relativePath = path.relative(projectRoot, absolutePath) || '.';
105
+ return relativePath.split(path.sep).join('/');
106
+ }
107
+ function readPackageScripts(projectRoot, intent) {
108
+ const intentCwd = resolveIntentCwd(projectRoot, intent);
109
+ if (!intentCwd) {
110
+ return null;
111
+ }
112
+ const packagePath = path.join(intentCwd, 'package.json');
113
+ if (!existsSync(packagePath)) {
114
+ return null;
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
118
+ if (!isRecord(parsed) || !isRecord(parsed.scripts)) {
119
+ return {
120
+ relativePath: toProjectRelativePath(projectRoot, packagePath),
121
+ scripts: new Set(),
122
+ };
123
+ }
124
+ return {
125
+ relativePath: toProjectRelativePath(projectRoot, packagePath),
126
+ scripts: new Set(Object.keys(parsed.scripts)),
127
+ };
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function readRootPackageScripts(projectRoot) {
134
+ const packagePath = path.join(projectRoot, 'package.json');
135
+ if (!existsSync(packagePath)) {
136
+ return [];
137
+ }
138
+ try {
139
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
140
+ if (!isRecord(parsed) || !isRecord(parsed.scripts)) {
141
+ return [];
142
+ }
143
+ return Object.entries(parsed.scripts)
144
+ .filter((entry) => typeof entry[1] === 'string')
145
+ .sort((left, right) => left[0].localeCompare(right[0]));
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ }
151
+ function readFirstExistingFile(projectRoot, candidates) {
152
+ for (const candidate of candidates) {
153
+ if (existsSync(path.join(projectRoot, candidate))) {
154
+ return candidate;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ function readMakeTargets(projectRoot) {
160
+ const relativePath = readFirstExistingFile(projectRoot, MAKEFILE_CANDIDATES);
161
+ if (!relativePath) {
162
+ return [];
163
+ }
164
+ const content = readFileSync(path.join(projectRoot, relativePath), 'utf8');
165
+ const targets = [];
166
+ for (const line of content.split(/\r?\n/u)) {
167
+ if (/^\s/u.test(line) || line.startsWith('#') || line.includes(':=')) {
168
+ continue;
169
+ }
170
+ const match = /^([A-Za-z0-9][A-Za-z0-9_-]*)\s*:/u.exec(line);
171
+ if (match) {
172
+ targets.push(match[1]);
173
+ }
174
+ }
175
+ return uniqueSorted(targets);
176
+ }
177
+ function readJustRecipes(projectRoot) {
178
+ const relativePath = readFirstExistingFile(projectRoot, JUSTFILE_CANDIDATES);
179
+ if (!relativePath) {
180
+ return [];
181
+ }
182
+ const content = readFileSync(path.join(projectRoot, relativePath), 'utf8');
183
+ const recipes = [];
184
+ for (const line of content.split(/\r?\n/u)) {
185
+ if (/^\s/u.test(line) || line.startsWith('#') || line.startsWith('set ') || line.includes(' := ')) {
186
+ continue;
187
+ }
188
+ const match = /^([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s+[^:]*)?:\s*(?:#.*)?$/u.exec(line);
189
+ if (match) {
190
+ recipes.push(match[1]);
191
+ }
192
+ }
193
+ return uniqueSorted(recipes);
194
+ }
195
+ function toTomlString(value) {
196
+ return JSON.stringify(value);
197
+ }
198
+ function normalizeIntentSegment(value) {
199
+ return value
200
+ .toLowerCase()
201
+ .replace(/[^a-z0-9]+/gu, '_')
202
+ .replace(/^_+|_+$/gu, '');
203
+ }
204
+ function uniqueSuggestionIntentName(baseName, usedIntentNames) {
205
+ let candidate = baseName;
206
+ let suffix = 2;
207
+ while (usedIntentNames.has(candidate)) {
208
+ candidate = `${baseName}_${suffix}`;
209
+ suffix += 1;
210
+ }
211
+ usedIntentNames.add(candidate);
212
+ return candidate;
213
+ }
214
+ function createUnknownIntentSnippet(intentName, description, reason) {
215
+ return [
216
+ `[intents.${intentName}]`,
217
+ 'status = "unknown"',
218
+ `description = ${toTomlString(description)}`,
219
+ `reason = ${toTomlString(reason)}`,
220
+ 'agent_action = "review_and_configure_before_run"',
221
+ ].join('\n');
222
+ }
223
+ function createSuggestion(usedIntentNames, sourceFile, sourceKind, sourceName, commandHint) {
224
+ const sourcePrefix = sourceKind.replace(/_script$/u, '').replace(/_target$/u, '').replace(/_recipe$/u, '');
225
+ const intentName = uniqueSuggestionIntentName(`suggest_${sourcePrefix}_${normalizeIntentSegment(sourceName) || 'command'}`, usedIntentNames);
226
+ const reason = `Suggested from ${sourceFile} entry "${sourceName}". Review before adding runnable command fields.`;
227
+ const description = `Review ${commandHint} for a possible command intent.`;
228
+ return {
229
+ sourceFile,
230
+ sourceKind,
231
+ sourceName,
232
+ commandHint,
233
+ suggestedIntent: intentName,
234
+ status: 'unknown',
235
+ reason,
236
+ snippet: createUnknownIntentSnippet(intentName, description, reason),
237
+ };
238
+ }
239
+ function suggestCommandContracts(projectRoot, existingIntentNames) {
240
+ if (!projectRoot) {
241
+ return [];
242
+ }
243
+ const usedIntentNames = new Set(existingIntentNames);
244
+ const suggestions = [];
245
+ for (const [scriptName] of readRootPackageScripts(projectRoot)) {
246
+ suggestions.push(createSuggestion(usedIntentNames, 'package.json', 'package_script', scriptName, `npm run ${scriptName}`));
247
+ }
248
+ const makefilePath = readFirstExistingFile(projectRoot, MAKEFILE_CANDIDATES);
249
+ if (makefilePath) {
250
+ for (const target of readMakeTargets(projectRoot)) {
251
+ suggestions.push(createSuggestion(usedIntentNames, makefilePath, 'make_target', target, `make ${target}`));
252
+ }
253
+ }
254
+ const justfilePath = readFirstExistingFile(projectRoot, JUSTFILE_CANDIDATES);
255
+ if (justfilePath) {
256
+ for (const recipe of readJustRecipes(projectRoot)) {
257
+ suggestions.push(createSuggestion(usedIntentNames, justfilePath, 'just_recipe', recipe, `just ${recipe}`));
258
+ }
259
+ }
260
+ return suggestions;
261
+ }
59
262
  function pushIssue(issues, severity, code, intent, message) {
60
263
  issues.push({ severity, code, intent, message });
61
264
  }
@@ -132,6 +335,25 @@ function lintIntent(name, value, issues) {
132
335
  }
133
336
  return value;
134
337
  }
338
+ function lintReferencedPackageScripts(projectRoot, intents, issues) {
339
+ if (!projectRoot) {
340
+ return;
341
+ }
342
+ for (const [name, intent] of intents) {
343
+ if (readString(intent, 'status') !== 'configured') {
344
+ continue;
345
+ }
346
+ const scriptName = readPackageScriptReference(intent);
347
+ if (!scriptName) {
348
+ continue;
349
+ }
350
+ const packageScripts = readPackageScripts(projectRoot, intent);
351
+ if (!packageScripts || packageScripts.scripts.has(scriptName)) {
352
+ continue;
353
+ }
354
+ pushIssue(issues, 'warning', 'referenced_package_script_missing', name, `Intent ${name} references package script "${scriptName}" in ${packageScripts.relativePath}, but that script is not declared.`);
355
+ }
356
+ }
135
357
  function collectRequiredAfterReasons(contract) {
136
358
  const reasonToIntents = new Map();
137
359
  for (const [name, intent] of Object.entries(contract.intents)) {
@@ -151,6 +373,87 @@ function collectRequiredAfterReasons(contract) {
151
373
  }
152
374
  return reasonToIntents;
153
375
  }
376
+ function readSkillPathsByIntent(projectRoot) {
377
+ const skillPathsByIntent = new Map();
378
+ if (!projectRoot) {
379
+ return skillPathsByIntent;
380
+ }
381
+ const skillIndexPath = path.join(projectRoot, ...SKILL_INDEX_PATH.split('/'));
382
+ if (!existsSync(skillIndexPath)) {
383
+ return skillPathsByIntent;
384
+ }
385
+ const routes = parseSkillIndexRoutes(readFileSync(skillIndexPath, 'utf8'));
386
+ for (const route of routes) {
387
+ for (const intent of route.commandIntents) {
388
+ skillPathsByIntent.set(intent, [...(skillPathsByIntent.get(intent) ?? []), route.skillPath]);
389
+ }
390
+ }
391
+ return skillPathsByIntent;
392
+ }
393
+ function classifyReasonSource(reason, knownClassificationReasons, documentedVerificationReasons) {
394
+ if (knownClassificationReasons.includes(reason)) {
395
+ return 'classification';
396
+ }
397
+ if (documentedVerificationReasons.includes(reason)) {
398
+ return 'documented';
399
+ }
400
+ return 'required_after';
401
+ }
402
+ function buildRelatedDocs(source, relatedSkills) {
403
+ const docs = [COMMANDS_CONFIG_PATH];
404
+ if (source === 'classification') {
405
+ docs.push(CHANGE_CLASSIFICATION_SOURCE_PATH);
406
+ }
407
+ if (source === 'documented') {
408
+ docs.push(AGENT_WORKFLOW_PATH);
409
+ }
410
+ if (relatedSkills.length > 0) {
411
+ docs.push(SKILL_INDEX_PATH);
412
+ }
413
+ return uniqueSorted(docs);
414
+ }
415
+ function buildCoverageMatrix(reasonToIntents, knownClassificationReasons, documentedVerificationReasons, skillPathsByIntent) {
416
+ const matrixReasons = uniqueSorted([
417
+ ...knownClassificationReasons,
418
+ ...documentedVerificationReasons,
419
+ ...reasonToIntents.keys(),
420
+ ]);
421
+ return matrixReasons.map((reason) => {
422
+ const candidates = [...(reasonToIntents.get(reason) ?? [])].sort((left, right) => left.name.localeCompare(right.name));
423
+ const source = classifyReasonSource(reason, knownClassificationReasons, documentedVerificationReasons);
424
+ const intentNames = candidates.map((candidate) => candidate.name);
425
+ const relatedSkills = uniqueSorted(intentNames.flatMap((intent) => skillPathsByIntent.get(intent) ?? []));
426
+ const gaps = [];
427
+ if (candidates.length === 0) {
428
+ gaps.push('missing_required_after');
429
+ }
430
+ else if (!candidates.some((candidate) => candidate.runnable)) {
431
+ gaps.push('no_runnable_intent');
432
+ }
433
+ if (source === 'required_after') {
434
+ gaps.push('unknown_reason');
435
+ }
436
+ if (intentNames.length > 0 && intersectSorted(intentNames, [...skillPathsByIntent.keys()]).length === 0) {
437
+ gaps.push('no_related_skill_route');
438
+ }
439
+ return {
440
+ reason,
441
+ source,
442
+ intents: candidates.map((candidate) => {
443
+ const eligibility = evaluateCommandIntentEligibility(candidate.name, candidate.intent);
444
+ return {
445
+ intent: candidate.name,
446
+ status: eligibility.code,
447
+ runnable: eligibility.ok,
448
+ detail: eligibility.detail,
449
+ };
450
+ }),
451
+ gaps: uniqueSorted(gaps),
452
+ relatedSkills,
453
+ relatedDocs: buildRelatedDocs(source, relatedSkills),
454
+ };
455
+ });
456
+ }
154
457
  function hasExplicitEffectMetadata(intent) {
155
458
  return Array.isArray(intent.effects) && intent.effects.some((effect) => isRecord(effect));
156
459
  }
@@ -200,6 +503,7 @@ function lintCoverage(contract, options, issues) {
200
503
  const documentedVerificationReasons = [...DOCUMENTED_VERIFICATION_REASONS];
201
504
  const knownReasons = new Set([...knownClassificationReasons, ...documentedVerificationReasons]);
202
505
  const reasonToIntents = collectRequiredAfterReasons(contract);
506
+ const skillPathsByIntent = readSkillPathsByIntent(options.projectRoot);
203
507
  const requiredAfterReasons = uniqueSorted(reasonToIntents.keys());
204
508
  const runnableReasons = uniqueSorted([...reasonToIntents.entries()]
205
509
  .filter(([, candidates]) => candidates.some((candidate) => candidate.runnable))
@@ -236,6 +540,7 @@ function lintCoverage(contract, options, issues) {
236
540
  documentedVerificationReasons,
237
541
  requiredAfterReasons,
238
542
  runnableReasons,
543
+ matrix: buildCoverageMatrix(reasonToIntents, knownClassificationReasons, documentedVerificationReasons, skillPathsByIntent),
239
544
  findings,
240
545
  };
241
546
  }
@@ -249,24 +554,28 @@ export function lintCommandContract(contract, options = {}) {
249
554
  const issues = [];
250
555
  const intentEntries = Object.entries(contract.intents);
251
556
  const intentTables = intentEntries
252
- .map(([name, value]) => lintIntent(name, value, issues))
253
- .filter((intent) => intent !== null);
557
+ .map(([name, value]) => [name, lintIntent(name, value, issues)])
558
+ .filter((entry) => entry[1] !== null);
559
+ lintReferencedPackageScripts(options.projectRoot, intentTables, issues);
560
+ const validIntents = intentTables.map(([, intent]) => intent);
254
561
  const coverage = options.coverage === true ? lintCoverage(contract, options, issues) : undefined;
562
+ const suggestions = options.suggest === true ? suggestCommandContracts(options.projectRoot, intentEntries.map(([name]) => name)) : undefined;
255
563
  const errors = issues.filter((issue) => issue.severity === 'error').length;
256
564
  const warnings = issues.length - errors;
257
565
  return {
258
566
  status: getStatus(errors, warnings),
259
567
  summary: {
260
568
  totalIntents: intentEntries.length,
261
- configured: intentTables.filter((intent) => readString(intent, 'status') === 'configured').length,
262
- runnable: intentTables.filter(configuredIntentIsRunnable).length,
263
- manualOnly: intentTables.filter((intent) => readString(intent, 'status') === 'manual_only').length,
264
- unknown: intentTables.filter((intent) => readString(intent, 'status') === 'unknown').length,
569
+ configured: validIntents.filter((intent) => readString(intent, 'status') === 'configured').length,
570
+ runnable: validIntents.filter(configuredIntentIsRunnable).length,
571
+ manualOnly: validIntents.filter((intent) => readString(intent, 'status') === 'manual_only').length,
572
+ unknown: validIntents.filter((intent) => readString(intent, 'status') === 'unknown').length,
265
573
  errors,
266
574
  warnings,
267
575
  },
268
576
  issues,
269
577
  sourceFiles: CONTRACT_LINT_SOURCE_FILES,
270
578
  coverage,
579
+ suggestions,
271
580
  };
272
581
  }
@@ -68,6 +68,11 @@ function createEmptyDashboardVerificationSnapshot(changedFiles) {
68
68
  skipped: [],
69
69
  schedule: {
70
70
  runner: 'serial_mf_run_receipts',
71
+ failurePolicy: {
72
+ mode: 'batch_boundary',
73
+ startedBatch: 'wait_for_completion',
74
+ nextBatch: 'stop_on_failure',
75
+ },
71
76
  batches: [],
72
77
  entries: [],
73
78
  notes: [],
@@ -104,6 +109,7 @@ export function createDashboardVerificationSnapshot(projectRoot, rawCommandContr
104
109
  skipped,
105
110
  schedule: {
106
111
  runner: verificationReport.schedule.runner,
112
+ failurePolicy: verificationReport.schedule.failurePolicy,
107
113
  batches: verificationReport.schedule.batches.map((batch) => ({
108
114
  index: batch.index,
109
115
  intents: batch.intents,
@@ -113,6 +119,8 @@ export function createDashboardVerificationSnapshot(projectRoot, rawCommandContr
113
119
  entries: verificationReport.schedule.entries.map((entry) => ({
114
120
  intent: entry.intent,
115
121
  command: `mf run ${entry.intent}`,
122
+ parallelEligible: entry.parallelEligible,
123
+ parallelReason: entry.parallelReason,
116
124
  locks: entry.locks,
117
125
  effects: entry.effects.map((effect) => ({
118
126
  access: effect.access,
@@ -0,0 +1,9 @@
1
+ export function createExternalEvidenceRisks(checks) {
2
+ return checks
3
+ .filter((check) => check.status !== 'passed')
4
+ .map((check) => ({
5
+ code: 'external_evidence_requires_review',
6
+ severity: 'medium',
7
+ detail: `External ${check.provider} check ${check.name} reported ${check.status}; review it as supporting evidence, not command authority.`,
8
+ }));
9
+ }
@@ -38,6 +38,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
38
38
  packaged: true,
39
39
  documented: true,
40
40
  },
41
+ {
42
+ id: 'test-selection',
43
+ schemaFile: 'test-selection.schema.json',
44
+ producer: 'parsed .mustflow/config/test-selection.toml',
45
+ packaged: true,
46
+ documented: true,
47
+ },
41
48
  {
42
49
  id: 'contract-lint-report',
43
50
  schemaFile: 'contract-lint-report.schema.json',
@@ -46,6 +53,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
46
53
  documented: true,
47
54
  installedCommand: ['mf', 'contract-lint', '--json'],
48
55
  },
56
+ {
57
+ id: 'dashboard-export',
58
+ schemaFile: 'dashboard-export.schema.json',
59
+ producer: 'mf dashboard --export-json <path>',
60
+ packaged: true,
61
+ documented: true,
62
+ },
49
63
  {
50
64
  id: 'classify-report',
51
65
  schemaFile: 'classify-report.schema.json',
@@ -71,6 +85,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
71
85
  installedCommand: ['mf', 'line-endings', 'check', '--json'],
72
86
  expectedExitCodes: [0, 1],
73
87
  },
88
+ {
89
+ id: 'latest-run-pointer',
90
+ schemaFile: 'latest-run-pointer.schema.json',
91
+ producer: '.mustflow/state/runs/latest.json when written by mf verify',
92
+ packaged: true,
93
+ documented: true,
94
+ },
74
95
  {
75
96
  id: 'handoff-validation-report',
76
97
  schemaFile: 'handoff-validation-report.schema.json',
@@ -111,6 +132,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
111
132
  documented: true,
112
133
  installedCommand: ['mf', 'verify', '--reason', 'schema_verify', '--json'],
113
134
  },
135
+ {
136
+ id: 'verify-run-manifest',
137
+ schemaFile: 'verify-run-manifest.schema.json',
138
+ producer: '.mustflow/state/runs/verify-latest/manifest.json',
139
+ packaged: true,
140
+ documented: true,
141
+ },
114
142
  {
115
143
  id: 'change-verification-report',
116
144
  schemaFile: 'change-verification-report.schema.json',
@@ -0,0 +1,17 @@
1
+ const UNRESOLVED_VERIFY_STATUSES = new Set(['failed', 'blocked', 'partial']);
2
+ export function createRepeatedFailureRisk(input) {
3
+ if (input.previousVerificationPlanId === null ||
4
+ input.previousStatus === null ||
5
+ input.previousVerificationPlanId !== input.currentVerificationPlanId ||
6
+ !UNRESOLVED_VERIFY_STATUSES.has(input.previousStatus) ||
7
+ !UNRESOLVED_VERIFY_STATUSES.has(input.currentStatus)) {
8
+ return null;
9
+ }
10
+ return {
11
+ code: 'repeated_verification_failure',
12
+ severity: 'high',
13
+ previous_status: input.previousStatus,
14
+ verification_plan_id: input.currentVerificationPlanId,
15
+ detail: 'The previous verify summary has the same verification_plan_id and an unresolved status; provide new evidence or a narrower hypothesis before marking the task complete.',
16
+ };
17
+ }
@@ -0,0 +1,53 @@
1
+ const TEXT_FIELD_LABELS = {
2
+ reported_symptom: 'reported symptom',
3
+ expected_behavior: 'expected behavior',
4
+ observed_behavior: 'observed behavior',
5
+ };
6
+ const ITEM_FIELD_LABELS = {
7
+ original_reproduction: 'original reproduction path',
8
+ evidence_before_fix: 'before-fix evidence',
9
+ evidence_after_fix: 'after-fix evidence',
10
+ regression_guard: 'regression guard',
11
+ };
12
+ export function createReproEvidenceRisks(report) {
13
+ if (!report) {
14
+ return [];
15
+ }
16
+ const risks = [];
17
+ for (const [field, label] of Object.entries(TEXT_FIELD_LABELS)) {
18
+ if (!report[field]) {
19
+ risks.push({
20
+ code: 'repro_evidence_missing',
21
+ severity: 'high',
22
+ detail: `Bug-fix repro evidence is missing ${label}; do not mark the task verified from command receipts alone.`,
23
+ });
24
+ }
25
+ }
26
+ for (const [field, label] of Object.entries(ITEM_FIELD_LABELS)) {
27
+ const item = report[field];
28
+ if (item.status === 'missing') {
29
+ risks.push({
30
+ code: 'repro_evidence_missing',
31
+ severity: 'high',
32
+ detail: `Bug-fix repro evidence is missing ${label}; rerun or explicitly mark it unavailable before claiming verification.`,
33
+ });
34
+ continue;
35
+ }
36
+ if (item.status === 'present' && !item.summary) {
37
+ risks.push({
38
+ code: 'repro_evidence_missing',
39
+ severity: 'high',
40
+ detail: `Bug-fix repro evidence marks ${label} present but does not summarize the evidence.`,
41
+ });
42
+ continue;
43
+ }
44
+ if (item.status === 'unavailable' && !item.reason) {
45
+ risks.push({
46
+ code: 'repro_evidence_missing',
47
+ severity: 'high',
48
+ detail: `Bug-fix repro evidence marks ${label} unavailable without explaining why.`,
49
+ });
50
+ }
51
+ }
52
+ return risks;
53
+ }