mustflow 2.22.17 → 2.22.47

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 (70) hide show
  1. package/README.md +6 -0
  2. package/dist/cli/commands/api.js +874 -0
  3. package/dist/cli/commands/dashboard.js +51 -4
  4. package/dist/cli/commands/explain.js +3 -2
  5. package/dist/cli/commands/help.js +0 -1
  6. package/dist/cli/commands/run.js +41 -4
  7. package/dist/cli/commands/verify.js +4 -43
  8. package/dist/cli/i18n/en.js +15 -0
  9. package/dist/cli/i18n/es.js +15 -0
  10. package/dist/cli/i18n/fr.js +15 -0
  11. package/dist/cli/i18n/hi.js +15 -0
  12. package/dist/cli/i18n/ko.js +15 -0
  13. package/dist/cli/i18n/zh.js +15 -0
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/lib/cli-output.js +1 -1
  16. package/dist/cli/lib/command-registry.js +6 -0
  17. package/dist/cli/lib/dashboard-html/client-script.js +9 -0
  18. package/dist/cli/lib/dashboard-html/styles.js +48 -1
  19. package/dist/cli/lib/doc-review-ledger.js +1 -1
  20. package/dist/cli/lib/local-index/index.js +324 -298
  21. package/dist/cli/lib/repo-map.js +19 -5
  22. package/dist/cli/lib/validation/index.js +6 -2
  23. package/dist/core/active-run-locks.js +36 -8
  24. package/dist/core/atomic-state-write.js +5 -20
  25. package/dist/core/change-verification.js +18 -2
  26. package/dist/core/contract-lint.js +3 -3
  27. package/dist/core/public-json-contracts.js +48 -0
  28. package/dist/core/repeated-failure.js +1 -1
  29. package/dist/core/run-write-drift.js +30 -17
  30. package/dist/core/safe-filesystem.js +54 -5
  31. package/dist/core/skill-route-explanation.js +2 -1
  32. package/dist/core/source-anchors.js +7 -3
  33. package/dist/core/validation-ratchet.js +61 -18
  34. package/dist/core/verification-decision-graph.js +8 -1
  35. package/dist/core/verification-plan-id.js +44 -0
  36. package/package.json +1 -1
  37. package/schemas/README.md +6 -0
  38. package/schemas/command-catalog.schema.json +158 -0
  39. package/schemas/diff-risk.schema.json +74 -0
  40. package/schemas/health.schema.json +45 -0
  41. package/schemas/latest-evidence.schema.json +95 -0
  42. package/schemas/verification-plan.schema.json +245 -0
  43. package/schemas/workspace-summary.schema.json +282 -0
  44. package/templates/default/i18n.toml +139 -1
  45. package/templates/default/locales/en/.mustflow/skills/INDEX.md +24 -1
  46. package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +212 -0
  47. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +184 -0
  48. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +194 -0
  49. package/templates/default/locales/en/.mustflow/skills/config-env-change/SKILL.md +189 -0
  50. package/templates/default/locales/en/.mustflow/skills/css-code-change/SKILL.md +199 -0
  51. package/templates/default/locales/en/.mustflow/skills/dart-code-change/SKILL.md +179 -0
  52. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +178 -0
  53. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +151 -0
  54. package/templates/default/locales/en/.mustflow/skills/elysia-code-change/SKILL.md +115 -0
  55. package/templates/default/locales/en/.mustflow/skills/file-path-cross-platform-change/SKILL.md +147 -0
  56. package/templates/default/locales/en/.mustflow/skills/flutter-code-change/SKILL.md +116 -0
  57. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +156 -0
  58. package/templates/default/locales/en/.mustflow/skills/hono-code-change/SKILL.md +117 -0
  59. package/templates/default/locales/en/.mustflow/skills/html-code-change/SKILL.md +173 -0
  60. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +149 -0
  61. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +154 -0
  62. package/templates/default/locales/en/.mustflow/skills/release-publish-change/SKILL.md +172 -0
  63. package/templates/default/locales/en/.mustflow/skills/routes.toml +138 -0
  64. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +154 -0
  65. package/templates/default/locales/en/.mustflow/skills/svelte-code-change/SKILL.md +186 -0
  66. package/templates/default/locales/en/.mustflow/skills/tailwind-code-change/SKILL.md +164 -0
  67. package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +185 -0
  68. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +184 -0
  69. package/templates/default/locales/en/.mustflow/skills/unocss-code-change/SKILL.md +186 -0
  70. package/templates/default/manifest.toml +158 -1
@@ -0,0 +1,874 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createClassifyOutput } from './classify.js';
4
+ import { createChangeVerificationReport, } from '../../core/change-verification.js';
5
+ import { readUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
6
+ import { createVerificationPlanId } from '../../core/verification-plan-id.js';
7
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
8
+ import { getAgentContext } from '../lib/agent-context.js';
9
+ import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
10
+ import { readGitChangedFiles } from '../lib/git-changes.js';
11
+ import { t } from '../lib/i18n.js';
12
+ import { resolveMustflowRoot } from '../lib/project-root.js';
13
+ import { createRunPlan } from '../lib/run-plan.js';
14
+ import { checkMustflowProjectReport } from '../lib/validation.js';
15
+ const API_WORKSPACE_SUMMARY_SCHEMA_VERSION = '1';
16
+ const API_COMMAND_CATALOG_SCHEMA_VERSION = '1';
17
+ const API_VERIFICATION_PLAN_SCHEMA_VERSION = '1';
18
+ const API_LATEST_EVIDENCE_SCHEMA_VERSION = '1';
19
+ const API_DIFF_RISK_SCHEMA_VERSION = '1';
20
+ const API_HEALTH_SCHEMA_VERSION = '1';
21
+ const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
22
+ const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
23
+ const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
24
+ export function getApiHelp(lang = 'en') {
25
+ return renderHelp({
26
+ usage: 'mf api <action> [options]',
27
+ summary: t(lang, 'api.help.summary'),
28
+ commands: [
29
+ {
30
+ label: 'workspace-summary',
31
+ description: t(lang, 'api.help.action.workspaceSummary'),
32
+ },
33
+ {
34
+ label: 'command-catalog',
35
+ description: t(lang, 'api.help.action.commandCatalog'),
36
+ },
37
+ {
38
+ label: 'verification-plan',
39
+ description: t(lang, 'api.help.action.verificationPlan'),
40
+ },
41
+ {
42
+ label: 'latest-evidence',
43
+ description: t(lang, 'api.help.action.latestEvidence'),
44
+ },
45
+ {
46
+ label: 'diff-risk',
47
+ description: t(lang, 'api.help.action.diffRisk'),
48
+ },
49
+ {
50
+ label: 'health',
51
+ description: t(lang, 'api.help.action.health'),
52
+ },
53
+ ],
54
+ options: [
55
+ { label: '--changed', description: t(lang, 'classify.help.option.changed') },
56
+ { label: '--json', description: t(lang, 'cli.option.json') },
57
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
58
+ ],
59
+ examples: [
60
+ 'mf api workspace-summary --json',
61
+ 'mf api command-catalog --json',
62
+ 'mf api verification-plan --changed --json',
63
+ 'mf api latest-evidence --json',
64
+ 'mf api diff-risk --changed --json',
65
+ 'mf api health --json',
66
+ ],
67
+ exitCodes: [
68
+ { label: '0', description: t(lang, 'api.help.exit.ok') },
69
+ { label: '1', description: t(lang, 'cli.common.invalidInput') },
70
+ ],
71
+ }, lang);
72
+ }
73
+ function summarizeCheck(projectRoot) {
74
+ const report = checkMustflowProjectReport(projectRoot, { strict: false });
75
+ return {
76
+ ok: report.issues.length === 0,
77
+ issue_count: report.issues.length,
78
+ warning_count: report.warnings.length,
79
+ issues: report.issues,
80
+ warnings: report.warnings,
81
+ };
82
+ }
83
+ function summarizeReadOrder(context) {
84
+ return {
85
+ required: context.read_order.map((entry) => entry.path),
86
+ optional: context.optional_read_order.map((entry) => entry.path),
87
+ missing_required: context.read_order.filter((entry) => !entry.exists).map((entry) => entry.path),
88
+ missing_optional: context.optional_read_order.filter((entry) => !entry.exists).map((entry) => entry.path),
89
+ };
90
+ }
91
+ function summarizeCommandSurface(projectRoot, context) {
92
+ const catalog = createCommandCatalogOutputForRoot(projectRoot);
93
+ const totalIntents = catalog.command_contract.parse_error === null ? catalog.command_contract.total_intents : context.command_contract.intents.length;
94
+ const runnableIntents = catalog.command_contract.parse_error === null
95
+ ? catalog.intents.filter((intent) => intent.runnable).map((intent) => intent.name)
96
+ : context.command_contract.runnable_intents;
97
+ const runnableCount = runnableIntents.length;
98
+ return {
99
+ path: context.command_contract.path,
100
+ exists: context.command_contract.exists,
101
+ total_intents: totalIntents,
102
+ runnable_intents: runnableIntents,
103
+ runnable_count: runnableCount,
104
+ non_runnable_count: Math.max(0, totalIntents - runnableCount),
105
+ };
106
+ }
107
+ function pathExists(projectRoot, relativePath) {
108
+ const resolved = path.resolve(projectRoot, ...relativePath.split('/'));
109
+ const root = path.resolve(projectRoot);
110
+ const relative = path.relative(root, resolved);
111
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
112
+ return false;
113
+ }
114
+ return existsSync(resolved);
115
+ }
116
+ function summarizeSkillSurface(projectRoot) {
117
+ return {
118
+ index_path: '.mustflow/skills/INDEX.md',
119
+ index_exists: pathExists(projectRoot, '.mustflow/skills/INDEX.md'),
120
+ routes_path: '.mustflow/skills/routes.toml',
121
+ routes_exists: pathExists(projectRoot, '.mustflow/skills/routes.toml'),
122
+ };
123
+ }
124
+ function summarizeGitState(projectRoot) {
125
+ const result = readGitChangedFiles(projectRoot);
126
+ if (!result.ok) {
127
+ return {
128
+ status: 'unavailable',
129
+ changed_file_count: null,
130
+ changed_files: [],
131
+ error: result.message,
132
+ };
133
+ }
134
+ return {
135
+ status: 'available',
136
+ changed_file_count: result.files.length,
137
+ changed_files: result.files,
138
+ error: null,
139
+ };
140
+ }
141
+ function createRecommendedReadSurfaces(context) {
142
+ const surfaces = [
143
+ ...context.read_order.filter((entry) => entry.exists).map((entry) => entry.path),
144
+ ...context.optional_read_order.filter((entry) => entry.exists).map((entry) => entry.path),
145
+ ];
146
+ return [...new Set(surfaces)];
147
+ }
148
+ function createRecommendedNextCommands(output) {
149
+ if (!output.installed) {
150
+ return ['mf init --dry-run', 'mf init --yes'];
151
+ }
152
+ if (!output.check.ok) {
153
+ return ['mf check', 'mf status --json', 'mf update --dry-run'];
154
+ }
155
+ const commands = ['mf context --json', 'mf doctor --json', 'mf check --strict'];
156
+ if (output.git_state.status === 'available' && output.git_state.changed_file_count !== null && output.git_state.changed_file_count > 0) {
157
+ commands.push('mf classify --changed --json', 'mf verify --changed --plan-only --json');
158
+ }
159
+ return commands;
160
+ }
161
+ function isSafeIntentName(value) {
162
+ return /^[A-Za-z0-9_-]+$/u.test(value);
163
+ }
164
+ function commandForIntent(kind, intentName) {
165
+ if (!isSafeIntentName(intentName)) {
166
+ return null;
167
+ }
168
+ return kind === 'run' ? `mf run ${intentName}` : `mf run ${intentName} --dry-run --json`;
169
+ }
170
+ function acceptsTestTargets(intent) {
171
+ return isRecord(intent) && isRecord(intent.selection) && intent.selection.accepts_test_targets === true;
172
+ }
173
+ function readIntentStrings(intent, key) {
174
+ return intent ? (readStringArray(intent, key) ?? []) : [];
175
+ }
176
+ function toNullableBoolean(value) {
177
+ return typeof value === 'boolean' ? value : null;
178
+ }
179
+ function readIntentBoolean(intent, key) {
180
+ const value = intent?.[key];
181
+ return typeof value === 'boolean' ? value : null;
182
+ }
183
+ function getIntentMode(intent) {
184
+ if (!intent) {
185
+ return null;
186
+ }
187
+ if (readStringArray(intent, 'argv')) {
188
+ return 'argv';
189
+ }
190
+ return intent.mode === 'shell' && readString(intent, 'cmd') ? 'shell' : null;
191
+ }
192
+ function createCommandCatalogIntentFromError(contract, name, error) {
193
+ const rawIntent = isRecord(contract.intents[name]) ? contract.intents[name] : undefined;
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ return {
196
+ name,
197
+ description: rawIntent ? readString(rawIntent, 'description') ?? null : null,
198
+ kind: rawIntent ? readString(rawIntent, 'kind') ?? null : null,
199
+ status: rawIntent ? readString(rawIntent, 'status') ?? null : null,
200
+ lifecycle: rawIntent ? readString(rawIntent, 'lifecycle') ?? null : null,
201
+ run_policy: rawIntent ? readString(rawIntent, 'run_policy') ?? null : null,
202
+ runnable: false,
203
+ reason_code: 'catalog_error',
204
+ detail: message,
205
+ run_command: null,
206
+ preview_command: commandForIntent('preview', name),
207
+ mode: getIntentMode(rawIntent),
208
+ cwd: null,
209
+ timeout_seconds: rawIntent ? readPositiveInteger(rawIntent, 'timeout_seconds') ?? null : null,
210
+ kill_after_seconds: null,
211
+ max_output_bytes: null,
212
+ success_exit_codes: null,
213
+ required_after: readIntentStrings(rawIntent, 'required_after'),
214
+ writes: readIntentStrings(rawIntent, 'writes'),
215
+ effect_count: Array.isArray(rawIntent?.effects) ? rawIntent.effects.length : 0,
216
+ network: readIntentBoolean(rawIntent, 'network'),
217
+ destructive: readIntentBoolean(rawIntent, 'destructive'),
218
+ accepts_test_targets: acceptsTestTargets(rawIntent),
219
+ env_policy: rawIntent ? readString(rawIntent, 'env_policy') ?? null : null,
220
+ env_allowlist: readIntentStrings(rawIntent, 'env_allowlist'),
221
+ precondition_count: 0,
222
+ active_lock_conflict_count: 0,
223
+ stale_active_lock_count: 0,
224
+ agent_action: rawIntent ? readString(rawIntent, 'agent_action') ?? null : null,
225
+ manual_start_hint: rawIntent ? readString(rawIntent, 'manual_start_hint') ?? null : null,
226
+ health_check_url: rawIntent ? readString(rawIntent, 'health_check_url') ?? null : null,
227
+ stop_instruction: rawIntent ? readString(rawIntent, 'stop_instruction') ?? null : null,
228
+ related_oneshot_checks: readIntentStrings(rawIntent, 'related_oneshot_checks'),
229
+ };
230
+ }
231
+ function createCommandCatalogIntent(projectRoot, contract, name) {
232
+ let plan;
233
+ try {
234
+ plan = createRunPlan(projectRoot, contract, name);
235
+ }
236
+ catch (error) {
237
+ return createCommandCatalogIntentFromError(contract, name, error);
238
+ }
239
+ const rawIntent = isRecord(plan.intent) ? plan.intent : undefined;
240
+ return {
241
+ name,
242
+ description: rawIntent ? readString(rawIntent, 'description') ?? null : null,
243
+ kind: plan.kind,
244
+ status: plan.intentStatus,
245
+ lifecycle: plan.lifecycle,
246
+ run_policy: plan.runPolicy,
247
+ runnable: plan.ok,
248
+ reason_code: plan.reasonCode,
249
+ detail: plan.detail,
250
+ run_command: plan.ok ? commandForIntent('run', name) : null,
251
+ preview_command: commandForIntent('preview', name),
252
+ mode: plan.mode,
253
+ cwd: plan.relativeCwd,
254
+ timeout_seconds: plan.timeoutSeconds,
255
+ kill_after_seconds: plan.killAfterSeconds,
256
+ max_output_bytes: plan.maxOutputBytes,
257
+ success_exit_codes: plan.successExitCodes,
258
+ required_after: readIntentStrings(rawIntent, 'required_after'),
259
+ writes: plan.writes ?? [],
260
+ effect_count: plan.effects?.length ?? 0,
261
+ network: toNullableBoolean(plan.network),
262
+ destructive: toNullableBoolean(plan.destructive),
263
+ accepts_test_targets: acceptsTestTargets(rawIntent),
264
+ env_policy: plan.envPolicy,
265
+ env_allowlist: plan.envAllowlist,
266
+ precondition_count: plan.preconditions.length,
267
+ active_lock_conflict_count: plan.activeLockConflicts.length,
268
+ stale_active_lock_count: plan.staleActiveLocks.length,
269
+ agent_action: rawIntent ? readString(rawIntent, 'agent_action') ?? null : null,
270
+ manual_start_hint: plan.manualStartHint,
271
+ health_check_url: plan.healthCheckUrl,
272
+ stop_instruction: plan.stopInstruction,
273
+ related_oneshot_checks: plan.relatedOneshotChecks,
274
+ };
275
+ }
276
+ function createEmptyCommandCatalogContract(exists, parseError) {
277
+ return {
278
+ path: COMMANDS_RELATIVE_PATH,
279
+ exists,
280
+ parse_error: parseError,
281
+ total_intents: 0,
282
+ runnable_count: 0,
283
+ blocked_count: 0,
284
+ };
285
+ }
286
+ function createCommandCatalogOutputForRoot(mustflowRoot) {
287
+ const executionPolicy = {
288
+ command_authority: COMMANDS_RELATIVE_PATH,
289
+ run_entrypoint: 'mf run <intent>',
290
+ preview_entrypoint: 'mf run <intent> --dry-run --json',
291
+ direct_commands_allowed: false,
292
+ requires_configured_oneshot_agent_allowed: true,
293
+ };
294
+ if (!pathExists(mustflowRoot, COMMANDS_RELATIVE_PATH)) {
295
+ return {
296
+ schema_version: API_COMMAND_CATALOG_SCHEMA_VERSION,
297
+ command: 'api command-catalog',
298
+ mustflow_root: mustflowRoot,
299
+ command_contract: createEmptyCommandCatalogContract(false, null),
300
+ execution_policy: executionPolicy,
301
+ intents: [],
302
+ issues: [`${COMMANDS_RELATIVE_PATH} is missing.`],
303
+ };
304
+ }
305
+ let contract;
306
+ try {
307
+ contract = readCommandContract(mustflowRoot);
308
+ }
309
+ catch (error) {
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ return {
312
+ schema_version: API_COMMAND_CATALOG_SCHEMA_VERSION,
313
+ command: 'api command-catalog',
314
+ mustflow_root: mustflowRoot,
315
+ command_contract: createEmptyCommandCatalogContract(true, message),
316
+ execution_policy: executionPolicy,
317
+ intents: [],
318
+ issues: [message],
319
+ };
320
+ }
321
+ const intents = Object.keys(contract.intents)
322
+ .sort((left, right) => left.localeCompare(right))
323
+ .map((name) => createCommandCatalogIntent(mustflowRoot, contract, name));
324
+ const runnableCount = intents.filter((intent) => intent.runnable).length;
325
+ return {
326
+ schema_version: API_COMMAND_CATALOG_SCHEMA_VERSION,
327
+ command: 'api command-catalog',
328
+ mustflow_root: mustflowRoot,
329
+ command_contract: {
330
+ path: COMMANDS_RELATIVE_PATH,
331
+ exists: true,
332
+ parse_error: null,
333
+ total_intents: intents.length,
334
+ runnable_count: runnableCount,
335
+ blocked_count: Math.max(0, intents.length - runnableCount),
336
+ },
337
+ execution_policy: executionPolicy,
338
+ intents,
339
+ issues: [],
340
+ };
341
+ }
342
+ function createCommandCatalogOutput() {
343
+ return createCommandCatalogOutputForRoot(resolveMustflowRoot());
344
+ }
345
+ function createWorkspaceSummaryOutput() {
346
+ const mustflowRoot = resolveMustflowRoot();
347
+ const context = getAgentContext(mustflowRoot);
348
+ const check = summarizeCheck(mustflowRoot);
349
+ const gitState = summarizeGitState(mustflowRoot);
350
+ const partialOutput = {
351
+ installed: context.installed,
352
+ check,
353
+ git_state: gitState,
354
+ };
355
+ return {
356
+ schema_version: API_WORKSPACE_SUMMARY_SCHEMA_VERSION,
357
+ command: 'api workspace-summary',
358
+ mustflow_root: context.mustflow_root,
359
+ installed: context.installed,
360
+ manifest_lock: context.manifest_lock,
361
+ template: context.template,
362
+ check,
363
+ read_order: summarizeReadOrder(context),
364
+ command_surface: summarizeCommandSurface(mustflowRoot, context),
365
+ skill_surface: summarizeSkillSurface(mustflowRoot),
366
+ git_state: gitState,
367
+ latest_run: context.latest_run,
368
+ effective_policy: context.effective_policy,
369
+ state_policy: context.state_policy,
370
+ blocked_actions: context.blocked_actions,
371
+ recommended_read_surfaces: createRecommendedReadSurfaces(context),
372
+ recommended_next_commands: createRecommendedNextCommands(partialOutput),
373
+ issues: context.issues,
374
+ };
375
+ }
376
+ function createVerificationPlanExecutionPolicy() {
377
+ return {
378
+ plan_command: 'mf verify --changed --plan-only --json',
379
+ run_command: 'mf verify --changed --json',
380
+ direct_commands_allowed: false,
381
+ selected_intents_run_via: 'mf run <intent>',
382
+ };
383
+ }
384
+ function createVerificationPlanSource() {
385
+ return {
386
+ kind: 'changed',
387
+ command: 'mf verify --changed --plan-only --json',
388
+ };
389
+ }
390
+ function toVerificationPlanClassification(output) {
391
+ return {
392
+ source: 'changed',
393
+ file_count: output.summary.fileCount,
394
+ files: output.files,
395
+ validation_reasons: output.summary.validationReasons,
396
+ public_surface_count: output.summary.publicSurfaceCount,
397
+ update_policies: output.summary.updatePolicies,
398
+ drift_checks: output.summary.driftChecks,
399
+ };
400
+ }
401
+ function toVerificationPlanRequirement(requirement) {
402
+ return {
403
+ reason: requirement.reason,
404
+ file_count: requirement.files.length,
405
+ files: requirement.files,
406
+ surfaces: requirement.surfaces,
407
+ affected_contracts: requirement.affectedContracts,
408
+ drift_checks: requirement.driftChecks,
409
+ update_policies: requirement.updatePolicies,
410
+ };
411
+ }
412
+ function toVerificationPlanSchedule(report) {
413
+ return {
414
+ runner: report.schedule.runner,
415
+ entry_count: report.schedule.entries.length,
416
+ batch_count: report.schedule.batches.length,
417
+ selected_intents: report.schedule.entries.map((entry) => entry.intent),
418
+ entries: report.schedule.entries.map((entry) => ({
419
+ intent: entry.intent,
420
+ run_command: commandForIntent('run', entry.intent),
421
+ preview_command: commandForIntent('preview', entry.intent),
422
+ parallel_eligible: entry.parallelEligible,
423
+ parallel_reason: entry.parallelReason,
424
+ locks: entry.locks,
425
+ conflict_count: entry.conflicts.length,
426
+ })),
427
+ batches: report.schedule.batches.map((batch) => ({
428
+ index: batch.index,
429
+ intents: batch.intents,
430
+ locks: batch.locks,
431
+ })),
432
+ notes: report.schedule.notes,
433
+ };
434
+ }
435
+ function toVerificationPlanTestSelection(report) {
436
+ return {
437
+ status: report.test_selection.status,
438
+ candidate_count: report.test_selection.matches.length,
439
+ selected_count: report.test_selection.selected.length,
440
+ note: report.test_selection.notes[0] ?? null,
441
+ };
442
+ }
443
+ function createUnavailableVerificationPlanOutput(mustflowRoot, classification, issue) {
444
+ return {
445
+ schema_version: API_VERIFICATION_PLAN_SCHEMA_VERSION,
446
+ command: 'api verification-plan',
447
+ mustflow_root: mustflowRoot,
448
+ source: createVerificationPlanSource(),
449
+ status: 'unavailable',
450
+ verification_plan_id: null,
451
+ classification: classification ? toVerificationPlanClassification(classification) : null,
452
+ requirements: [],
453
+ candidates: [],
454
+ gaps: [],
455
+ schedule: null,
456
+ test_selection: null,
457
+ execution_policy: createVerificationPlanExecutionPolicy(),
458
+ issues: [issue],
459
+ };
460
+ }
461
+ function createVerificationPlanOutput() {
462
+ const mustflowRoot = resolveMustflowRoot();
463
+ let classification = null;
464
+ try {
465
+ classification = createClassifyOutput(mustflowRoot, 'changed', []);
466
+ }
467
+ catch (error) {
468
+ const message = error instanceof Error ? error.message : String(error);
469
+ return createUnavailableVerificationPlanOutput(mustflowRoot, null, message);
470
+ }
471
+ let contract;
472
+ try {
473
+ contract = readCommandContract(mustflowRoot);
474
+ }
475
+ catch (error) {
476
+ const message = error instanceof Error ? error.message : String(error);
477
+ return createUnavailableVerificationPlanOutput(mustflowRoot, classification, message);
478
+ }
479
+ let report;
480
+ try {
481
+ report = createChangeVerificationReport(classification, contract, mustflowRoot);
482
+ }
483
+ catch (error) {
484
+ const message = error instanceof Error ? error.message : String(error);
485
+ return createUnavailableVerificationPlanOutput(mustflowRoot, classification, message);
486
+ }
487
+ return {
488
+ schema_version: API_VERIFICATION_PLAN_SCHEMA_VERSION,
489
+ command: 'api verification-plan',
490
+ mustflow_root: mustflowRoot,
491
+ source: createVerificationPlanSource(),
492
+ status: 'available',
493
+ verification_plan_id: createVerificationPlanId(report, contract),
494
+ classification: toVerificationPlanClassification(classification),
495
+ requirements: report.requirements.map(toVerificationPlanRequirement),
496
+ candidates: report.candidates.map((candidate) => ({
497
+ reason: candidate.reason,
498
+ intent: candidate.intent,
499
+ status: candidate.status,
500
+ selection_state: candidate.selectionState,
501
+ eligibility_state: candidate.eligibilityState,
502
+ candidate_state: candidate.candidateState,
503
+ skip_reason: candidate.skipReason,
504
+ detail: candidate.detail,
505
+ })),
506
+ gaps: report.gaps.map((gap) => ({
507
+ reason: gap.reason,
508
+ files: gap.files,
509
+ surfaces: gap.surfaces,
510
+ detail: gap.detail,
511
+ })),
512
+ schedule: toVerificationPlanSchedule(report),
513
+ test_selection: toVerificationPlanTestSelection(report),
514
+ execution_policy: createVerificationPlanExecutionPolicy(),
515
+ issues: [],
516
+ };
517
+ }
518
+ function readJsonInsideRoot(projectRoot, relativePath) {
519
+ return JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...relativePath.split('/')), {
520
+ maxBytes: MUSTFLOW_JSON_MAX_BYTES,
521
+ }));
522
+ }
523
+ function readLatestEvidenceManifest(projectRoot, latest) {
524
+ if (latest.kind !== 'verify_run_summary' || !latest.manifest_path) {
525
+ return {
526
+ status: 'not_applicable',
527
+ path: null,
528
+ receipt_count: null,
529
+ error: null,
530
+ };
531
+ }
532
+ try {
533
+ const manifest = readJsonInsideRoot(projectRoot, latest.manifest_path);
534
+ const receiptCount = isRecord(manifest) && Array.isArray(manifest.receipts) ? manifest.receipts.length : null;
535
+ return {
536
+ status: receiptCount === null ? 'unavailable' : 'available',
537
+ path: latest.manifest_path,
538
+ receipt_count: receiptCount,
539
+ error: receiptCount === null ? 'Verify manifest is not a recognized manifest object.' : null,
540
+ };
541
+ }
542
+ catch (error) {
543
+ return {
544
+ status: 'unavailable',
545
+ path: latest.manifest_path,
546
+ receipt_count: null,
547
+ error: error instanceof Error ? error.message : String(error),
548
+ };
549
+ }
550
+ }
551
+ function latestEvidenceSummaryFromParsed(parsed) {
552
+ if (!isRecord(parsed)) {
553
+ return {
554
+ exists: true,
555
+ path: LATEST_RUN_RELATIVE_PATH,
556
+ kind: 'unknown',
557
+ status: null,
558
+ intent: null,
559
+ reason: null,
560
+ verification_plan_id: null,
561
+ completion_verdict_status: null,
562
+ manifest_path: null,
563
+ receipt_path: null,
564
+ run_dir: null,
565
+ exit_code: null,
566
+ timed_out: null,
567
+ duration_ms: null,
568
+ summary: null,
569
+ };
570
+ }
571
+ const command = readString(parsed, 'command');
572
+ const kind = command === 'run'
573
+ ? 'run_receipt'
574
+ : command === 'verify' && readString(parsed, 'kind') === 'verify_run_summary'
575
+ ? 'verify_run_summary'
576
+ : 'unknown';
577
+ const completionVerdict = isRecord(parsed.completion_verdict) ? parsed.completion_verdict : null;
578
+ return {
579
+ exists: true,
580
+ path: LATEST_RUN_RELATIVE_PATH,
581
+ kind,
582
+ status: readString(parsed, 'status') ?? null,
583
+ intent: readString(parsed, 'intent') ?? null,
584
+ reason: readString(parsed, 'reason') ?? null,
585
+ verification_plan_id: readString(parsed, 'verification_plan_id') ?? null,
586
+ completion_verdict_status: completionVerdict ? readString(completionVerdict, 'status') ?? null : null,
587
+ manifest_path: readString(parsed, 'manifest_path') ?? null,
588
+ receipt_path: readString(parsed, 'receipt_path') ?? null,
589
+ run_dir: readString(parsed, 'run_dir') ?? null,
590
+ exit_code: typeof parsed.exit_code === 'number' ? parsed.exit_code : null,
591
+ timed_out: typeof parsed.timed_out === 'boolean' ? parsed.timed_out : null,
592
+ duration_ms: typeof parsed.duration_ms === 'number' ? parsed.duration_ms : null,
593
+ summary: isRecord(parsed.summary) ? parsed.summary : null,
594
+ };
595
+ }
596
+ function createLatestEvidenceOutput() {
597
+ const mustflowRoot = resolveMustflowRoot();
598
+ const issues = [];
599
+ let latest;
600
+ try {
601
+ latest = latestEvidenceSummaryFromParsed(readJsonInsideRoot(mustflowRoot, LATEST_RUN_RELATIVE_PATH));
602
+ }
603
+ catch (error) {
604
+ const message = error instanceof Error ? error.message : String(error);
605
+ latest = {
606
+ exists: false,
607
+ path: LATEST_RUN_RELATIVE_PATH,
608
+ kind: null,
609
+ status: null,
610
+ intent: null,
611
+ reason: null,
612
+ verification_plan_id: null,
613
+ completion_verdict_status: null,
614
+ manifest_path: null,
615
+ receipt_path: null,
616
+ run_dir: null,
617
+ exit_code: null,
618
+ timed_out: null,
619
+ duration_ms: null,
620
+ summary: null,
621
+ };
622
+ if (!message.includes('ENOENT')) {
623
+ issues.push(message);
624
+ }
625
+ }
626
+ return {
627
+ schema_version: API_LATEST_EVIDENCE_SCHEMA_VERSION,
628
+ command: 'api latest-evidence',
629
+ mustflow_root: mustflowRoot,
630
+ latest,
631
+ manifest: readLatestEvidenceManifest(mustflowRoot, latest),
632
+ policy: {
633
+ raw_output_included: false,
634
+ hidden_reasoning_included: false,
635
+ max_bytes: MUSTFLOW_JSON_MAX_BYTES,
636
+ },
637
+ issues,
638
+ };
639
+ }
640
+ function getRiskLevel(classification, report) {
641
+ if (!classification || !report) {
642
+ return 'unknown';
643
+ }
644
+ if (classification.summary.fileCount === 0) {
645
+ return 'none';
646
+ }
647
+ const reasons = new Set(classification.summary.validationReasons);
648
+ if (report.gaps.length > 0 ||
649
+ classification.summary.publicSurfaceCount > 0 ||
650
+ reasons.has('release_risk') ||
651
+ reasons.has('package_metadata_change') ||
652
+ reasons.has('mustflow_config_change') ||
653
+ reasons.has('mustflow_docs_change')) {
654
+ return 'high';
655
+ }
656
+ if (reasons.has('code_change') || reasons.has('behavior_change') || reasons.has('test_change') || reasons.has('unknown_change')) {
657
+ return 'medium';
658
+ }
659
+ return 'low';
660
+ }
661
+ function createDiffRiskOutput() {
662
+ const mustflowRoot = resolveMustflowRoot();
663
+ let classification = null;
664
+ try {
665
+ classification = createClassifyOutput(mustflowRoot, 'changed', []);
666
+ }
667
+ catch (error) {
668
+ const message = error instanceof Error ? error.message : String(error);
669
+ return {
670
+ schema_version: API_DIFF_RISK_SCHEMA_VERSION,
671
+ command: 'api diff-risk',
672
+ mustflow_root: mustflowRoot,
673
+ source: createVerificationPlanSource(),
674
+ status: 'unavailable',
675
+ risk_level: 'unknown',
676
+ changed_file_count: 0,
677
+ changed_files: [],
678
+ public_surface_count: 0,
679
+ validation_reasons: [],
680
+ update_policies: [],
681
+ drift_checks: [],
682
+ required_verification: [],
683
+ gap_count: 0,
684
+ gaps: [],
685
+ recommended_commands: [],
686
+ issues: [message],
687
+ };
688
+ }
689
+ let report = null;
690
+ const issues = [];
691
+ try {
692
+ report = createChangeVerificationReport(classification, readCommandContract(mustflowRoot), mustflowRoot);
693
+ }
694
+ catch (error) {
695
+ issues.push(error instanceof Error ? error.message : String(error));
696
+ }
697
+ const requiredVerification = report ? report.schedule.entries.map((entry) => entry.intent) : [];
698
+ return {
699
+ schema_version: API_DIFF_RISK_SCHEMA_VERSION,
700
+ command: 'api diff-risk',
701
+ mustflow_root: mustflowRoot,
702
+ source: createVerificationPlanSource(),
703
+ status: report ? 'available' : 'unavailable',
704
+ risk_level: getRiskLevel(classification, report),
705
+ changed_file_count: classification.summary.fileCount,
706
+ changed_files: classification.files,
707
+ public_surface_count: classification.summary.publicSurfaceCount,
708
+ validation_reasons: classification.summary.validationReasons,
709
+ update_policies: classification.summary.updatePolicies,
710
+ drift_checks: classification.summary.driftChecks,
711
+ required_verification: requiredVerification,
712
+ gap_count: report?.gaps.length ?? 0,
713
+ gaps: report?.gaps.map((gap) => ({
714
+ reason: gap.reason,
715
+ files: gap.files,
716
+ surfaces: gap.surfaces,
717
+ detail: gap.detail,
718
+ })) ?? [],
719
+ recommended_commands: [
720
+ 'mf api verification-plan --changed --json',
721
+ ...(requiredVerification.length > 0 ? ['mf verify --changed --json'] : []),
722
+ ],
723
+ issues,
724
+ };
725
+ }
726
+ function createHealthOutput() {
727
+ const workspace = createWorkspaceSummaryOutput();
728
+ const catalog = createCommandCatalogOutputForRoot(workspace.mustflow_root);
729
+ const blockers = [
730
+ ...workspace.check.issues,
731
+ ...catalog.issues,
732
+ ];
733
+ const warnings = [
734
+ ...workspace.check.warnings,
735
+ ...(workspace.git_state.status === 'unavailable' && workspace.git_state.error ? [workspace.git_state.error] : []),
736
+ ];
737
+ const commandContractOk = catalog.command_contract.exists && catalog.command_contract.parse_error === null;
738
+ const status = blockers.length > 0 || !workspace.installed || !commandContractOk
739
+ ? 'blocked'
740
+ : warnings.length > 0
741
+ ? 'degraded'
742
+ : 'ok';
743
+ return {
744
+ schema_version: API_HEALTH_SCHEMA_VERSION,
745
+ command: 'api health',
746
+ mustflow_root: workspace.mustflow_root,
747
+ status,
748
+ installed: workspace.installed,
749
+ check_ok: workspace.check.ok,
750
+ command_contract_ok: commandContractOk,
751
+ runnable_count: catalog.command_contract.runnable_count,
752
+ git_status: workspace.git_state.status,
753
+ changed_file_count: workspace.git_state.changed_file_count,
754
+ latest_run_exists: workspace.latest_run.exists,
755
+ blockers,
756
+ warnings,
757
+ recommended_next_commands: workspace.recommended_next_commands,
758
+ };
759
+ }
760
+ function validateJsonOnlyAction(action, args, reporter, lang) {
761
+ if (args.includes('--help') || args.includes('-h')) {
762
+ reporter.stdout(getApiHelp(lang));
763
+ return false;
764
+ }
765
+ const supported = new Set(['--json']);
766
+ const unsupported = args.filter((arg) => !supported.has(arg));
767
+ if (unsupported.length > 0) {
768
+ printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf api --help', getApiHelp(lang), lang);
769
+ return false;
770
+ }
771
+ if (!args.includes('--json')) {
772
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
773
+ return false;
774
+ }
775
+ return true;
776
+ }
777
+ function runWorkspaceSummary(args, reporter, lang) {
778
+ if (!validateJsonOnlyAction('workspace-summary', args, reporter, lang)) {
779
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
780
+ }
781
+ reporter.stdout(JSON.stringify(createWorkspaceSummaryOutput(), null, 2));
782
+ return 0;
783
+ }
784
+ function runCommandCatalog(args, reporter, lang) {
785
+ if (!validateJsonOnlyAction('command-catalog', args, reporter, lang)) {
786
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
787
+ }
788
+ reporter.stdout(JSON.stringify(createCommandCatalogOutput(), null, 2));
789
+ return 0;
790
+ }
791
+ function validateChangedJsonAction(action, args, reporter, lang) {
792
+ if (args.includes('--help') || args.includes('-h')) {
793
+ reporter.stdout(getApiHelp(lang));
794
+ return false;
795
+ }
796
+ const supported = new Set(['--changed', '--json']);
797
+ const unsupported = args.filter((arg) => !supported.has(arg));
798
+ if (unsupported.length > 0) {
799
+ printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf api --help', getApiHelp(lang), lang);
800
+ return false;
801
+ }
802
+ if (!args.includes('--json')) {
803
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
804
+ return false;
805
+ }
806
+ if (!args.includes('--changed')) {
807
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresChanged', { action }), 'mf api --help', getApiHelp(lang), lang);
808
+ return false;
809
+ }
810
+ return true;
811
+ }
812
+ function runVerificationPlan(args, reporter, lang) {
813
+ if (!validateChangedJsonAction('verification-plan', args, reporter, lang)) {
814
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
815
+ }
816
+ reporter.stdout(JSON.stringify(createVerificationPlanOutput(), null, 2));
817
+ return 0;
818
+ }
819
+ function runLatestEvidence(args, reporter, lang) {
820
+ if (!validateJsonOnlyAction('latest-evidence', args, reporter, lang)) {
821
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
822
+ }
823
+ reporter.stdout(JSON.stringify(createLatestEvidenceOutput(), null, 2));
824
+ return 0;
825
+ }
826
+ function runDiffRisk(args, reporter, lang) {
827
+ if (!validateChangedJsonAction('diff-risk', args, reporter, lang)) {
828
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
829
+ }
830
+ reporter.stdout(JSON.stringify(createDiffRiskOutput(), null, 2));
831
+ return 0;
832
+ }
833
+ function runHealth(args, reporter, lang) {
834
+ if (!validateJsonOnlyAction('health', args, reporter, lang)) {
835
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
836
+ }
837
+ reporter.stdout(JSON.stringify(createHealthOutput(), null, 2));
838
+ return 0;
839
+ }
840
+ export function runApi(args, reporter, lang = 'en') {
841
+ if (args.includes('--help') || args.includes('-h')) {
842
+ reporter.stdout(getApiHelp(lang));
843
+ return 0;
844
+ }
845
+ const [action, ...rest] = args;
846
+ if (!action) {
847
+ printUsageError(reporter, t(lang, 'api.error.missingAction'), 'mf api --help', getApiHelp(lang), lang);
848
+ return 1;
849
+ }
850
+ if (action.startsWith('-')) {
851
+ printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: action }), 'mf api --help', getApiHelp(lang), lang);
852
+ return 1;
853
+ }
854
+ if (action === 'workspace-summary') {
855
+ return runWorkspaceSummary(rest, reporter, lang);
856
+ }
857
+ if (action === 'command-catalog') {
858
+ return runCommandCatalog(rest, reporter, lang);
859
+ }
860
+ if (action === 'verification-plan') {
861
+ return runVerificationPlan(rest, reporter, lang);
862
+ }
863
+ if (action === 'latest-evidence') {
864
+ return runLatestEvidence(rest, reporter, lang);
865
+ }
866
+ if (action === 'diff-risk') {
867
+ return runDiffRisk(rest, reporter, lang);
868
+ }
869
+ if (action === 'health') {
870
+ return runHealth(rest, reporter, lang);
871
+ }
872
+ printUsageError(reporter, t(lang, 'api.error.unknownAction', { action }), 'mf api --help', getApiHelp(lang), lang);
873
+ return 1;
874
+ }