mustflow 1.31.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +77 -2
  5. package/dist/cli/commands/explain-verify.js +11 -1
  6. package/dist/cli/commands/index.js +14 -0
  7. package/dist/cli/commands/run.js +4 -1
  8. package/dist/cli/commands/verify.js +986 -43
  9. package/dist/cli/i18n/en.js +61 -10
  10. package/dist/cli/i18n/es.js +61 -10
  11. package/dist/cli/i18n/fr.js +61 -10
  12. package/dist/cli/i18n/hi.js +61 -10
  13. package/dist/cli/i18n/ko.js +61 -10
  14. package/dist/cli/i18n/zh.js +61 -10
  15. package/dist/cli/lib/dashboard-export.js +62 -12
  16. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  17. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  18. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  19. package/dist/cli/lib/dashboard-html/template.js +134 -0
  20. package/dist/cli/lib/dashboard-html/types.js +1 -0
  21. package/dist/cli/lib/dashboard-html.js +1 -1907
  22. package/dist/cli/lib/dashboard-locale.js +37 -0
  23. package/dist/cli/lib/local-index/constants.js +48 -0
  24. package/dist/cli/lib/local-index/index.js +2951 -0
  25. package/dist/cli/lib/local-index/sql.js +15 -0
  26. package/dist/cli/lib/local-index/types.js +1 -0
  27. package/dist/cli/lib/local-index.js +1 -1911
  28. package/dist/cli/lib/run-plan.js +76 -1
  29. package/dist/cli/lib/templates.js +18 -1
  30. package/dist/cli/lib/validation/command-intents.js +11 -0
  31. package/dist/cli/lib/validation/constants.js +238 -0
  32. package/dist/cli/lib/validation/index.js +1384 -0
  33. package/dist/cli/lib/validation/primitives.js +198 -0
  34. package/dist/cli/lib/validation/test-selection.js +95 -0
  35. package/dist/cli/lib/validation/types.js +1 -0
  36. package/dist/cli/lib/validation.js +1 -1770
  37. package/dist/core/check-issues.js +6 -0
  38. package/dist/core/completion-verdict.js +341 -0
  39. package/dist/core/contract-lint.js +221 -6
  40. package/dist/core/external-evidence.js +9 -0
  41. package/dist/core/public-json-contracts.js +21 -0
  42. package/dist/core/repeated-failure.js +179 -0
  43. package/dist/core/repro-evidence.js +134 -0
  44. package/dist/core/scope-risk.js +64 -0
  45. package/dist/core/skill-route-alignment.js +20 -0
  46. package/dist/core/source-anchor-status.js +4 -1
  47. package/dist/core/test-selection.js +3 -0
  48. package/dist/core/validation-ratchet.js +196 -0
  49. package/dist/core/verification-evidence.js +249 -0
  50. package/examples/README.md +12 -4
  51. package/package.json +3 -3
  52. package/schemas/README.md +13 -3
  53. package/schemas/change-verification-report.schema.json +16 -2
  54. package/schemas/commands.schema.json +4 -0
  55. package/schemas/contract-lint-report.schema.json +29 -0
  56. package/schemas/dashboard-export.schema.json +310 -0
  57. package/schemas/explain-report.schema.json +173 -1
  58. package/schemas/latest-run-pointer.schema.json +601 -0
  59. package/schemas/run-receipt.schema.json +4 -0
  60. package/schemas/test-selection.schema.json +81 -0
  61. package/schemas/verify-report.schema.json +578 -1
  62. package/schemas/verify-run-manifest.schema.json +627 -0
  63. package/templates/default/i18n.toml +1 -1
  64. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  65. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  66. package/templates/default/manifest.toml +29 -2
package/README.md CHANGED
@@ -14,10 +14,24 @@ The core concept is straightforward: place `AGENTS.md` at the project root and k
14
14
  - Security: [SECURITY.md](https://github.com/0disoft/mustflow/blob/main/SECURITY.md)
15
15
  - Changelog: [CHANGELOG.md](https://github.com/0disoft/mustflow/blob/main/CHANGELOG.md)
16
16
 
17
+ ## Choose your path
18
+
19
+ - Use mustflow in your repository: start with [Quick start](#quick-start), then review the [no-guessing workflow](#no-guessing-workflow) and [`examples/minimal-js/`](examples/minimal-js/).
20
+ - Contribute to mustflow: read [CONTRIBUTING.md](CONTRIBUTING.md), then run only configured command intents from [`.mustflow/config/commands.toml`](.mustflow/config/commands.toml).
21
+ - Build an AI coding tool or agent harness: use `AGENTS.md` and `mf context --json` for repository context, then consume JSON output and schemas from `mf classify`, `mf verify`, `mf run`, `mf dashboard`, and [`schemas/`](schemas/).
22
+
17
23
  ## No-guessing workflow
18
24
 
19
25
  The initial mustflow path is deliberately narrow.
20
26
 
27
+ Authority stays narrow:
28
+
29
+ - Current user instructions define the task goal unless they are unsafe.
30
+ - Host safety, sandbox, and approval gates still apply.
31
+ - Repository work rules come from the nearest `AGENTS.md` and `.mustflow/config/*.toml`.
32
+ - Command execution authority comes only from `.mustflow/config/commands.toml`.
33
+ - Skills, context files, preferences, generated maps, search results, cache, and state files guide or explain work. They do not grant command permission.
34
+
21
35
  ```sh
22
36
  npm install -D mustflow
23
37
  npx mf init --yes
@@ -27,9 +41,9 @@ npx mf check --strict
27
41
  After changes to code, templates, schemas, or documentation, classify the changed paths and review the verification plan before running any commands.
28
42
 
29
43
  ```sh
30
- npx mf classify --changed --json > .mustflow/state/change-plan.json
31
- npx mf verify --from-plan .mustflow/state/change-plan.json --plan-only --json
32
- npx mf verify --from-plan .mustflow/state/change-plan.json --json
44
+ npx mf classify --changed --write .mustflow/state/change-classification.json
45
+ npx mf verify --from-classification .mustflow/state/change-classification.json --plan-only --json
46
+ npx mf verify --from-classification .mustflow/state/change-classification.json --json
33
47
  ```
34
48
 
35
49
  The plan is based on change classification and the `required_after` metadata in `.mustflow/config/commands.toml`. A command runs only if its declared intent is configured, one-shot, agent-allowed, closed-stdin, bounded by a timeout, and backed by an explicit command source.
@@ -239,9 +253,9 @@ If a project already has optional root Markdown files such as `README.md`, `PROJ
239
253
  npx mf init --yes
240
254
  npx mf doctor
241
255
  npx mf check --strict
242
- npx mf classify --changed --json > .mustflow/state/change-plan.json
243
- npx mf verify --from-plan .mustflow/state/change-plan.json --plan-only --json
244
- npx mf verify --from-plan .mustflow/state/change-plan.json --json
256
+ npx mf classify --changed --write .mustflow/state/change-classification.json
257
+ npx mf verify --from-classification .mustflow/state/change-classification.json --plan-only --json
258
+ npx mf verify --from-classification .mustflow/state/change-classification.json --json
245
259
  ```
246
260
 
247
261
  Create the optional local search index if search capabilities are needed. Run the normal command
@@ -294,8 +308,8 @@ mf run mustflow_update_apply
294
308
  | `mf check` | Validate mustflow files, TOML configuration, and skill document shape. |
295
309
  | `mf check --strict` | Run additional safety checks for document identity, authority/lifecycle metadata, skill index/body alignment, skill metadata, command boundaries, version-source discovery, retention policy, output limits, raw logs, and secret-like context. |
296
310
  | `mf adapters status` | Inspect existing host-specific instruction and adapter files without generating adapter files or granting command authority. |
297
- | `mf classify --changed` | Classify changed paths, public surfaces, and validation reasons without modifying files. |
298
- | `mf contract-lint` | Inspect `.mustflow/config/commands.toml` for command-contract errors and warnings without running commands. |
311
+ | `mf classify --changed` | Classify changed paths, public surfaces, and validation reasons. Add `--write <path>` to save the classification report. |
312
+ | `mf contract-lint` | Inspect `.mustflow/config/commands.toml` for command-contract errors and warnings without running commands. Add `--suggest` to print non-runnable candidate snippets from existing command files. |
299
313
  | `mf doctor` | Inspect the current mustflow root without writing files. |
300
314
  | `mf docs review list` | Show documents still waiting for prose review after agent edits. |
301
315
  | `mf docs review add <path>` | Add or refresh a document review queue entry. |
@@ -356,7 +370,7 @@ npx mf init --product-source-locale en --product-locale ko-KR
356
370
  npx mf init --set git.auto_commit=true
357
371
  ```
358
372
 
359
- - `--profile`: Project profile. The default is `minimal`. Profiles also select the installed skill surface: `minimal` installs core everyday coding skills, while `oss`, `team`, `product`, and `library` add opt-in skill groups without removing optional skill files from the package.
373
+ - `--profile`: Project profile. The default is `minimal`. Profiles also select the installed skill surface: `minimal` installs core everyday coding skills, `patterns` adds architecture-pattern procedures, and `oss`, `team`, `product`, and `library` add opt-in skill groups without removing optional skill files from the package.
360
374
  - `--locale`: Installed mustflow document language. The default template currently supports `en`, `ko`, `zh`, `es`, `fr`, and `hi`. The default template includes localized documents for all these locales.
361
375
  - `--agent-lang`: Default language for final agent reports.
362
376
  - `--interactive`: Choose init settings via prompts.
@@ -1,3 +1,5 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { createChangeClassificationReport, } from '../../core/change-classification.js';
2
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
3
5
  import { readGitChangedFiles } from '../lib/git-changes.js';
@@ -10,10 +12,15 @@ export function getClassifyHelp(lang = 'en') {
10
12
  summary: t(lang, 'classify.help.summary'),
11
13
  options: [
12
14
  { label: '--changed', description: t(lang, 'classify.help.option.changed') },
15
+ { label: '--write <path>', description: t(lang, 'classify.help.option.write') },
13
16
  { label: '--json', description: t(lang, 'cli.option.json') },
14
17
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
15
18
  ],
16
- examples: ['mf classify --changed', 'mf classify README.md schemas/verify-report.schema.json --json'],
19
+ examples: [
20
+ 'mf classify --changed',
21
+ 'mf classify --changed --write .mustflow/state/change-classification.json',
22
+ 'mf classify README.md schemas/verify-report.schema.json --json',
23
+ ],
17
24
  exitCodes: [
18
25
  { label: '0', description: t(lang, 'classify.help.exit.ok') },
19
26
  { label: '1', description: t(lang, 'cli.common.invalidInput') },
@@ -24,7 +31,9 @@ function parseClassifyArgs(args) {
24
31
  const paths = [];
25
32
  let json = false;
26
33
  let changed = false;
27
- for (const arg of args) {
34
+ let writePath;
35
+ for (let index = 0; index < args.length; index += 1) {
36
+ const arg = args[index];
28
37
  if (arg === '--json') {
29
38
  json = true;
30
39
  continue;
@@ -33,12 +42,29 @@ function parseClassifyArgs(args) {
33
42
  changed = true;
34
43
  continue;
35
44
  }
45
+ if (arg === '--write') {
46
+ const value = args[index + 1];
47
+ if (!value || value.startsWith('-')) {
48
+ return { json, changed, writePath, paths, error: 'missing_write_value' };
49
+ }
50
+ writePath = value;
51
+ index += 1;
52
+ continue;
53
+ }
54
+ if (arg.startsWith('--write=')) {
55
+ const value = arg.slice('--write='.length);
56
+ if (value.length === 0) {
57
+ return { json, changed, writePath, paths, error: 'missing_write_value' };
58
+ }
59
+ writePath = value;
60
+ continue;
61
+ }
36
62
  if (arg.startsWith('-')) {
37
- return { json, changed, paths, error: arg };
63
+ return { json, changed, writePath, paths, error: arg };
38
64
  }
39
65
  paths.push(arg);
40
66
  }
41
- return { json, changed, paths };
67
+ return { json, changed, writePath, paths };
42
68
  }
43
69
  export function createClassifyOutput(projectRoot, source, paths) {
44
70
  const files = source === 'changed' ? readGitChangedFiles(projectRoot) : paths;
@@ -75,6 +101,19 @@ function renderClassifyOutput(output, lang) {
75
101
  }
76
102
  return lines.join('\n');
77
103
  }
104
+ function resolveWritePath(projectRoot, inputPath) {
105
+ const resolved = path.resolve(projectRoot, inputPath);
106
+ const relative = path.relative(projectRoot, resolved);
107
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
108
+ throw new Error('write_path_outside_root');
109
+ }
110
+ return resolved;
111
+ }
112
+ function writeClassifyOutput(projectRoot, inputPath, output) {
113
+ const outputPath = resolveWritePath(projectRoot, inputPath);
114
+ mkdirSync(path.dirname(outputPath), { recursive: true });
115
+ writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
116
+ }
78
117
  export function runClassify(args, reporter, lang = 'en') {
79
118
  if (args.includes('--help') || args.includes('-h')) {
80
119
  reporter.stdout(getClassifyHelp(lang));
@@ -82,7 +121,10 @@ export function runClassify(args, reporter, lang = 'en') {
82
121
  }
83
122
  const parsed = parseClassifyArgs(args);
84
123
  if (parsed.error) {
85
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: parsed.error }), 'mf classify --help', getClassifyHelp(lang), lang);
124
+ const message = parsed.error === 'missing_write_value'
125
+ ? t(lang, 'cli.error.missingValue', { option: '--write' })
126
+ : t(lang, 'cli.error.unknownOption', { option: parsed.error });
127
+ printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
86
128
  return 1;
87
129
  }
88
130
  if (parsed.changed && parsed.paths.length > 0) {
@@ -93,7 +135,20 @@ export function runClassify(args, reporter, lang = 'en') {
93
135
  printUsageError(reporter, t(lang, 'classify.error.missingInput'), 'mf classify --help', getClassifyHelp(lang), lang);
94
136
  return 1;
95
137
  }
96
- const output = createClassifyOutput(resolveMustflowRoot(), parsed.changed ? 'changed' : 'paths', parsed.paths);
138
+ const projectRoot = resolveMustflowRoot();
139
+ const output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
140
+ if (parsed.writePath) {
141
+ try {
142
+ writeClassifyOutput(projectRoot, parsed.writePath, output);
143
+ }
144
+ catch (error) {
145
+ const message = error instanceof Error && error.message === 'write_path_outside_root'
146
+ ? t(lang, 'classify.error.write_path_outside_root')
147
+ : t(lang, 'cli.common.invalidInput');
148
+ printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
149
+ return 1;
150
+ }
151
+ }
97
152
  if (parsed.json) {
98
153
  reporter.stdout(JSON.stringify(output, null, 2));
99
154
  return 0;
@@ -14,10 +14,11 @@ export function getContractLintHelp(lang = 'en') {
14
14
  summary: t(lang, 'contractLint.help.summary'),
15
15
  options: [
16
16
  { label: '--coverage', description: t(lang, 'contractLint.help.option.coverage') },
17
+ { label: '--suggest', description: t(lang, 'contractLint.help.option.suggest') },
17
18
  { label: '--json', description: t(lang, 'cli.option.json') },
18
19
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
19
20
  ],
20
- examples: ['mf contract-lint', 'mf contract-lint --coverage', 'mf contract-lint --coverage --json'],
21
+ examples: ['mf contract-lint', 'mf contract-lint --coverage', 'mf contract-lint --suggest', 'mf contract-lint --coverage --json'],
21
22
  exitCodes: [
22
23
  { label: '0', description: t(lang, 'contractLint.help.exit.ok') },
23
24
  { label: '1', description: t(lang, 'contractLint.help.exit.fail') },
@@ -32,13 +33,14 @@ function readPreferences(projectRoot) {
32
33
  const preferences = readTomlFile(preferencesPath);
33
34
  return isRecord(preferences) ? preferences : undefined;
34
35
  }
35
- function createContractLintOutput(projectRoot, coverage) {
36
+ function createContractLintOutput(projectRoot, coverage, suggest) {
36
37
  return {
37
38
  schema_version: CONTRACT_LINT_SCHEMA_VERSION,
38
39
  command: 'contract-lint',
39
40
  mustflow_root: projectRoot,
40
41
  report: lintCommandContract(readCommandContract(projectRoot), {
41
42
  coverage,
43
+ suggest,
42
44
  projectRoot,
43
45
  releaseVersioningEnabled: releaseVersioningIsEnabled(readPreferences(projectRoot)),
44
46
  }),
@@ -63,6 +65,13 @@ function renderContractLintOutput(output, lang) {
63
65
  if (output.report.coverage) {
64
66
  lines.push('', t(lang, 'contractLint.label.coverage'), `${t(lang, 'contractLint.label.classificationReasons')}: ${output.report.coverage.knownClassificationReasons.length}`, `${t(lang, 'contractLint.label.requiredAfterReasons')}: ${output.report.coverage.requiredAfterReasons.length}`, `${t(lang, 'contractLint.label.runnableReasons')}: ${output.report.coverage.runnableReasons.length}`, `${t(lang, 'contractLint.label.coverageFindings')}: ${output.report.coverage.findings.length}`);
65
67
  }
68
+ if (output.report.suggestions) {
69
+ lines.push('', t(lang, 'contractLint.label.suggestions'));
70
+ for (const suggestion of output.report.suggestions) {
71
+ lines.push(`- ${suggestion.sourceFile}:${suggestion.sourceName} -> ${suggestion.suggestedIntent}`);
72
+ lines.push(...suggestion.snippet.split('\n').map((line) => ` ${line}`));
73
+ }
74
+ }
66
75
  if (output.report.issues.length > 0) {
67
76
  lines.push('', t(lang, 'contractLint.label.issues'));
68
77
  for (const issue of output.report.issues) {
@@ -77,13 +86,13 @@ export function runContractLint(args, reporter, lang = 'en') {
77
86
  reporter.stdout(getContractLintHelp(lang));
78
87
  return 0;
79
88
  }
80
- const supported = new Set(['--coverage', '--json']);
89
+ const supported = new Set(['--coverage', '--suggest', '--json']);
81
90
  const unsupported = args.filter((arg) => !supported.has(arg));
82
91
  if (unsupported.length > 0) {
83
92
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf contract-lint --help', getContractLintHelp(lang), lang);
84
93
  return 1;
85
94
  }
86
- const output = createContractLintOutput(resolveMustflowRoot(), args.includes('--coverage'));
95
+ const output = createContractLintOutput(resolveMustflowRoot(), args.includes('--coverage'), args.includes('--suggest'));
87
96
  if (args.includes('--json')) {
88
97
  reporter.stdout(JSON.stringify(output, null, 2));
89
98
  }
@@ -15,7 +15,7 @@ import { isRecord, readCommandContract, readPositiveInteger, readString, readStr
15
15
  import { readDashboardPreferences, updateDashboardPreferences, } from '../lib/dashboard-preferences.js';
16
16
  import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
17
17
  import { inspectManifestLock } from '../lib/manifest-lock.js';
18
- import { readLocalCommandEffectGraphs } from '../lib/local-index.js';
18
+ import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
19
19
  import { readPackageMetadata } from '../lib/package-info.js';
20
20
  import { t } from '../lib/i18n.js';
21
21
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -25,6 +25,7 @@ const DEFAULT_DASHBOARD_HOST = '127.0.0.1';
25
25
  const DEFAULT_DASHBOARD_PORT = 0;
26
26
  const MAX_REQUEST_BYTES = 64 * 1024;
27
27
  const LOCAL_DASHBOARD_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
28
+ const DOC_REVIEW_BULK_PAYLOAD_FIELDS = ['paths', 'documents', 'entries'];
28
29
  const RELEASE_FILE_PATTERNS = [
29
30
  /^package\.json$/u,
30
31
  /^bun\.lockb?$/u,
@@ -296,6 +297,11 @@ function readDocReviewPayload(value) {
296
297
  throw new Error('Request body must be a JSON object.');
297
298
  }
298
299
  const payload = value;
300
+ for (const field of DOC_REVIEW_BULK_PAYLOAD_FIELDS) {
301
+ if (field in payload) {
302
+ throw new Error('Bulk documentation review updates require a separate confirmed flow.');
303
+ }
304
+ }
299
305
  const status = readRequiredStringField(payload, 'status');
300
306
  if (status !== 'approved' && status !== 'needs_human' && status !== 'ignored') {
301
307
  throw new Error('status must be approved, needs_human, or ignored.');
@@ -377,6 +383,70 @@ function toDashboardCommandEffectGraph(graph) {
377
383
  })),
378
384
  };
379
385
  }
386
+ function toDashboardVerificationReadModel(readModel) {
387
+ return {
388
+ source: readModel.source,
389
+ authority: readModel.authority,
390
+ command_authority: readModel.commandAuthority,
391
+ grants_command_authority: readModel.grantsCommandAuthority,
392
+ status: readModel.status,
393
+ database_path: readModel.databasePath,
394
+ index_fresh: readModel.indexFresh,
395
+ stale_paths: readModel.stalePaths,
396
+ plan_id: readModel.planId,
397
+ refresh_hint: readModel.refreshHint,
398
+ uncovered_criteria: readModel.uncoveredCriteria.map((criterion) => ({
399
+ criterion_id: criterion.criterionId,
400
+ source: criterion.source,
401
+ reason: criterion.reason,
402
+ surface: criterion.surface,
403
+ path_hash: criterion.pathHash,
404
+ coverage_status: criterion.coverageStatus,
405
+ receipt_count: criterion.receiptCount,
406
+ gap_count: criterion.gapCount,
407
+ risk_count: criterion.riskCount,
408
+ })),
409
+ severe_risks: readModel.severeRisks.map((risk) => ({
410
+ source_path: risk.sourcePath,
411
+ ordinal: risk.ordinal,
412
+ code: risk.code,
413
+ severity: risk.severity,
414
+ detail_hash: risk.detailHash,
415
+ })),
416
+ non_passing_receipts: readModel.nonPassingReceipts.map((receipt) => ({
417
+ receipt_hash: receipt.receiptHash,
418
+ plan_id: receipt.planId,
419
+ intent: receipt.intent,
420
+ status: receipt.status,
421
+ command_fingerprint: receipt.commandFingerprint,
422
+ contract_fingerprint: receipt.contractFingerprint,
423
+ current_state_hash: receipt.currentStateHash,
424
+ write_drift_status: receipt.writeDriftStatus,
425
+ })),
426
+ repeated_failure_fingerprints: readModel.repeatedFailureFingerprints.map((fingerprint) => ({
427
+ source_path: fingerprint.sourcePath,
428
+ fingerprint: fingerprint.fingerprint,
429
+ verification_plan_id: fingerprint.verificationPlanId,
430
+ status: fingerprint.status,
431
+ failed_intents: fingerprint.failedIntents,
432
+ primary_reason: fingerprint.primaryReason,
433
+ failed_intents_hash: fingerprint.failedIntentsHash,
434
+ risk_codes_hash: fingerprint.riskCodesHash,
435
+ affected_surfaces_hash: fingerprint.affectedSurfacesHash,
436
+ seen_count: fingerprint.seenCount,
437
+ requires_new_evidence: fingerprint.requiresNewEvidence,
438
+ })),
439
+ validation_weakening_signals: readModel.validationWeakeningSignals.map((signal) => ({
440
+ signal_id: signal.signalId,
441
+ plan_id: signal.planId,
442
+ code: signal.code,
443
+ severity: signal.severity,
444
+ path_hash: signal.pathHash,
445
+ before_hash: signal.beforeHash,
446
+ after_hash: signal.afterHash,
447
+ })),
448
+ };
449
+ }
380
450
  async function readCommandEffectGraphMap(projectRoot, intentNames) {
381
451
  if (intentNames.length === 0) {
382
452
  return { graphs: new Map() };
@@ -614,6 +684,8 @@ async function renderStatusResponse(projectRoot) {
614
684
  const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
615
685
  const gitChangedFiles = readGitChangedFiles(projectRoot);
616
686
  const packageMetadata = readPackageMetadata();
687
+ const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
688
+ const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
617
689
  return {
618
690
  schema_version: '1',
619
691
  command: 'dashboard status',
@@ -635,7 +707,10 @@ async function renderStatusResponse(projectRoot) {
635
707
  issues: manifest.issues,
636
708
  runnable_intents: context.command_contract.runnable_intents,
637
709
  command_contract: commandContract,
638
- verification: createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles),
710
+ verification: {
711
+ ...verification,
712
+ read_model: toDashboardVerificationReadModel(readModel),
713
+ },
639
714
  latest_run: context.latest_run,
640
715
  active_review_documents: activeDocuments.length,
641
716
  };
@@ -4,7 +4,7 @@ import { createVerificationDecisionGraph, } from '../../core/verification-decisi
4
4
  import { createVerificationPlan, } from '../../core/verification-plan.js';
5
5
  import { createVerificationSchedule } from '../../core/verification-scheduler.js';
6
6
  import { t } from '../lib/i18n.js';
7
- import { readLocalCommandEffectGraphs, } from '../lib/local-index.js';
7
+ import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
8
8
  import { planErrorMessageKey, readInputFromPlan } from './verify.js';
9
9
  export function parseExplainVerifyArgs(args) {
10
10
  let reason;
@@ -104,6 +104,7 @@ export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons
104
104
  ...new Set(plans.flatMap((plan) => plan.candidates.map((candidate) => candidate.intent).filter((intent) => intent.length > 0))),
105
105
  ];
106
106
  const graphsByIntent = intentNames.length > 0 ? await readLocalCommandEffectGraphs(projectRoot, intentNames) : new Map();
107
+ const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
107
108
  const requirements = plans.map((plan) => {
108
109
  const candidates = plan.candidates.map((candidate) => {
109
110
  const command = candidate.intent ? explainCommandIntent(contract, candidate.intent).intent : null;
@@ -166,6 +167,7 @@ export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons
166
167
  skippedCount,
167
168
  requirements,
168
169
  decisionGraph,
170
+ readModel,
169
171
  },
170
172
  },
171
173
  };
@@ -182,7 +184,15 @@ export function renderVerifyExplainDecision(decision, lang) {
182
184
  `- skipped: ${verification.skippedCount}`,
183
185
  `- decision_graph_nodes: ${verification.decisionGraph.summary.nodeCount}`,
184
186
  `- decision_graph_gaps: ${verification.decisionGraph.summary.gapCount}`,
187
+ `- read_model: ${verification.readModel.status}`,
188
+ `- read_model_plan_id: ${verification.readModel.planId ?? t(lang, 'value.none')}`,
185
189
  ];
190
+ if (verification.readModel.refreshHint) {
191
+ lines.push(`- read_model_refresh_hint: ${verification.readModel.refreshHint}`);
192
+ }
193
+ if (verification.readModel.status === 'fresh') {
194
+ lines.push(`- uncovered_criteria: ${verification.readModel.uncoveredCriteria.length}`, `- severe_risks: ${verification.readModel.severeRisks.length}`, `- non_passing_receipts: ${verification.readModel.nonPassingReceipts.length}`, `- repeated_failures_requiring_new_evidence: ${verification.readModel.repeatedFailureFingerprints.length}`, `- validation_weakening_signals: ${verification.readModel.validationWeakeningSignals.length}`);
195
+ }
186
196
  for (const requirement of verification.requirements) {
187
197
  lines.push(`- required_after: ${requirement.reason}`);
188
198
  if (requirement.matchingIntents.length > 0) {
@@ -42,7 +42,21 @@ function renderIndexSummary(result, lang) {
42
42
  `skill_routes: ${result.skill_route_count}`,
43
43
  `${t(lang, 'label.commandIntents')}: ${result.command_intent_count}`,
44
44
  `command_effects: ${result.command_effect_count}`,
45
+ `verification_evidence_summaries: ${result.verification_evidence_summary_count}`,
46
+ `verification_plans: ${result.verification_plan_count}`,
47
+ `acceptance_criteria: ${result.acceptance_criteria_count}`,
48
+ `criterion_coverage: ${result.criterion_coverage_count}`,
49
+ `verification_receipt_summaries: ${result.verification_receipt_summary_count}`,
50
+ `command_receipt_summaries: ${result.command_receipt_summary_count}`,
51
+ `verification_coverage_states: ${result.verification_coverage_state_count}`,
52
+ `verification_risk_signals: ${result.verification_risk_signal_count}`,
53
+ `validation_ratchet_signals: ${result.validation_ratchet_signal_count}`,
54
+ `completion_verdict_summaries: ${result.completion_verdict_summary_count}`,
55
+ `repro_routes: ${result.repro_route_count}`,
56
+ `repro_observations: ${result.repro_observation_count}`,
57
+ `failure_fingerprints: ${result.failure_fingerprint_count}`,
45
58
  `source_anchors: ${result.source_anchor_count}`,
59
+ `source_anchor_risk_signals: ${result.source_anchor_risk_signal_count}`,
46
60
  `index_mode: ${result.index_mode}`,
47
61
  `reused_existing: ${result.reused_existing ? 'yes' : 'no'}`,
48
62
  `${t(lang, 'label.wroteFiles')}: ${result.wrote_files ? 'yes' : 'no'}`,
@@ -367,6 +367,9 @@ function reportRunPlanFailure(plan, reporter, lang) {
367
367
  message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
368
368
  break;
369
369
  }
370
+ if (plan.suggestedIntentSnippet) {
371
+ message = `${message}\n\n${t(lang, 'run.label.suggestedIntentSnippet')}:\n${plan.suggestedIntentSnippet}`;
372
+ }
370
373
  reporter.stderr(renderCliError(message, 'mf help commands', lang));
371
374
  }
372
375
  export function getRunHelp(lang = 'en') {
@@ -439,7 +442,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
439
442
  reporter.stdout(JSON.stringify(createRunPreview(plan, previewMode), null, 2));
440
443
  }
441
444
  else {
442
- reporter.stdout(renderRunPreviewText(plan, previewMode));
445
+ reporter.stdout(renderRunPreviewText(plan, previewMode, lang));
443
446
  }
444
447
  });
445
448
  profiler.writeLatest({