mustflow 2.85.4 → 2.99.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 (78) hide show
  1. package/dist/cli/commands/script-pack.js +10 -0
  2. package/dist/cli/i18n/en.js +183 -0
  3. package/dist/cli/i18n/es.js +183 -0
  4. package/dist/cli/i18n/fr.js +183 -0
  5. package/dist/cli/i18n/hi.js +183 -0
  6. package/dist/cli/i18n/ko.js +183 -0
  7. package/dist/cli/i18n/zh.js +183 -0
  8. package/dist/cli/lib/script-pack-registry.js +284 -1
  9. package/dist/cli/script-packs/code-change-impact.js +6 -0
  10. package/dist/cli/script-packs/code-import-cycle.js +193 -0
  11. package/dist/cli/script-packs/docs-link-integrity.js +145 -0
  12. package/dist/cli/script-packs/repo-approval-gate.js +100 -0
  13. package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
  14. package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
  15. package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
  16. package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
  17. package/dist/cli/script-packs/repo-version-source.js +92 -0
  18. package/dist/cli/script-packs/test-performance-report.js +247 -0
  19. package/dist/cli/script-packs/test-regression-selector.js +167 -0
  20. package/dist/core/change-impact.js +23 -51
  21. package/dist/core/change-surface-classification.js +198 -0
  22. package/dist/core/docs-link-integrity.js +443 -0
  23. package/dist/core/import-cycle.js +152 -0
  24. package/dist/core/public-json-contracts.js +116 -0
  25. package/dist/core/repo-approval-gate.js +116 -0
  26. package/dist/core/repo-git-ignore-audit.js +302 -0
  27. package/dist/core/repo-manifest-lock-drift.js +321 -0
  28. package/dist/core/repo-merge-conflict-scan.js +335 -0
  29. package/dist/core/repo-version-source.js +82 -0
  30. package/dist/core/script-pack-suggestions.js +77 -1
  31. package/dist/core/skill-route-audit.js +354 -0
  32. package/dist/core/test-performance-report.js +697 -0
  33. package/dist/core/test-regression-selector.js +335 -0
  34. package/package.json +1 -1
  35. package/schemas/README.md +40 -2
  36. package/schemas/change-impact-report.schema.json +35 -1
  37. package/schemas/import-cycle-report.schema.json +157 -0
  38. package/schemas/link-integrity-report.schema.json +176 -0
  39. package/schemas/repo-approval-gate-report.schema.json +115 -0
  40. package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
  41. package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
  42. package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
  43. package/schemas/repo-version-source-report.schema.json +127 -0
  44. package/schemas/skill-route-audit-report.schema.json +144 -0
  45. package/schemas/test-performance-report.schema.json +319 -0
  46. package/schemas/test-regression-selector-report.schema.json +187 -0
  47. package/templates/default/i18n.toml +66 -18
  48. package/templates/default/locales/en/.mustflow/skills/INDEX.md +45 -8
  49. package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
  50. package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
  51. package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
  52. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
  53. package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
  54. package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
  55. package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
  56. package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
  57. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
  58. package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
  59. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
  60. package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
  61. package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
  62. package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
  63. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
  64. package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
  65. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
  66. package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
  67. package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
  68. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
  69. package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
  70. package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
  71. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
  72. package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
  73. package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
  74. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +9 -0
  75. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +14 -9
  76. package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
  77. package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
  78. package/templates/default/manifest.toml +64 -1
@@ -65,6 +65,35 @@ export const SCRIPT_PACKS = [
65
65
  reportSchemaFile: 'dependency-graph-report.schema.json',
66
66
  loadRunner: async () => (await import('../script-packs/code-dependency-graph.js')).runCodeDependencyGraphScript,
67
67
  },
68
+ {
69
+ packId: 'code',
70
+ id: 'import-cycle',
71
+ ref: scriptRef('code', 'import-cycle'),
72
+ usage: 'mf script-pack run code/import-cycle check <path...> [options]',
73
+ summaryKey: 'scriptPack.script.codeImportCycle.summary',
74
+ actions: ['check'],
75
+ useWhen: [
76
+ 'Detect bounded relative TypeScript and JavaScript import cycles before changing module boundaries.',
77
+ 'Review exact cycle paths and import line evidence after dependency graph orientation finds cycle hints.',
78
+ ],
79
+ phases: ['before_change', 'after_change', 'review'],
80
+ readOnly: true,
81
+ mutates: false,
82
+ network: false,
83
+ inputs: ['path', 'max_files', 'max_file_bytes', 'max_depth', 'max_nodes', 'max_edges', 'max_cycles'],
84
+ outputs: ['human_summary', 'json_report', 'import_cycles', 'cycle_edge_evidence'],
85
+ relatedSkills: [
86
+ 'change-blast-radius-review',
87
+ 'codebase-orientation',
88
+ 'javascript-code-change',
89
+ 'module-boundary-review',
90
+ 'typescript-code-change',
91
+ ],
92
+ riskLevel: 'low',
93
+ cost: 'low',
94
+ reportSchemaFile: 'import-cycle-report.schema.json',
95
+ loadRunner: async () => (await import('../script-packs/code-import-cycle.js')).runCodeImportCycleScript,
96
+ },
68
97
  {
69
98
  packId: 'code',
70
99
  id: 'change-impact',
@@ -81,7 +110,7 @@ export const SCRIPT_PACKS = [
81
110
  mutates: false,
82
111
  network: false,
83
112
  inputs: ['path', 'base_ref', 'head_ref', 'max_files', 'max_impacts', 'max_file_bytes'],
84
- outputs: ['human_summary', 'json_report', 'changed_files', 'impact_candidates', 'verification_hints'],
113
+ outputs: ['human_summary', 'json_report', 'changed_files', 'impact_candidates', 'script_hints', 'verification_hints'],
85
114
  relatedSkills: [
86
115
  'change-blast-radius-review',
87
116
  'completion-evidence-gate',
@@ -252,6 +281,87 @@ export const SCRIPT_PACKS = [
252
281
  reportSchemaFile: 'reference-drift-report.schema.json',
253
282
  loadRunner: async () => (await import('../script-packs/docs-reference-drift.js')).runDocsReferenceDriftScript,
254
283
  },
284
+ {
285
+ packId: 'docs',
286
+ id: 'link-integrity',
287
+ ref: scriptRef('docs', 'link-integrity'),
288
+ usage: 'mf script-pack run docs/link-integrity check [path...] [options]',
289
+ summaryKey: 'scriptPack.script.linkIntegrity.summary',
290
+ actions: ['check'],
291
+ useWhen: [
292
+ 'Check Markdown and MDX inline links for local file and anchor drift without fetching external URLs.',
293
+ 'Review docs after moving, renaming, or retitling documentation files before claiming navigation links still resolve.',
294
+ ],
295
+ phases: ['after_change', 'review'],
296
+ readOnly: true,
297
+ mutates: false,
298
+ network: false,
299
+ inputs: ['path', 'max_files', 'max_file_bytes'],
300
+ outputs: ['human_summary', 'json_report', 'link_integrity', 'missing_link_findings'],
301
+ relatedSkills: [
302
+ 'cli-output-contract-review',
303
+ 'docs-prose-review',
304
+ 'docs-update',
305
+ 'public-json-contract-change',
306
+ 'readme-authoring',
307
+ ],
308
+ riskLevel: 'low',
309
+ cost: 'low',
310
+ reportSchemaFile: 'link-integrity-report.schema.json',
311
+ loadRunner: async () => (await import('../script-packs/docs-link-integrity.js')).runDocsLinkIntegrityScript,
312
+ },
313
+ ],
314
+ },
315
+ {
316
+ id: 'test',
317
+ summaryKey: 'scriptPack.pack.test.summary',
318
+ scripts: [
319
+ {
320
+ packId: 'test',
321
+ id: 'performance-report',
322
+ ref: scriptRef('test', 'performance-report'),
323
+ usage: 'mf script-pack run test/performance-report summarize [options]',
324
+ summaryKey: 'scriptPack.script.testPerformanceReport.summary',
325
+ actions: ['summarize'],
326
+ useWhen: [
327
+ 'Summarize retained mf run performance evidence, selected-test fallbacks, slow intents, timeout pressure, and phase bottlenecks.',
328
+ 'Review local verification-loop performance before changing test scheduling, profiling, caching, or command-contract timeout policy.',
329
+ ],
330
+ phases: ['after_change', 'review'],
331
+ readOnly: true,
332
+ mutates: false,
333
+ network: false,
334
+ inputs: ['max_samples', 'max_intents', 'max_test_files', 'max_findings', 'slow_ms', 'timeout_ratio', 'phase_ms'],
335
+ outputs: ['human_summary', 'json_report', 'performance_samples', 'intent_timings', 'phase_timings', 'test_file_timings'],
336
+ relatedSkills: ['test-suite-performance-review', 'test-maintenance', 'completion-evidence-gate'],
337
+ riskLevel: 'low',
338
+ cost: 'low',
339
+ reportSchemaFile: 'test-performance-report.schema.json',
340
+ loadRunner: async () => (await import('../script-packs/test-performance-report.js')).runTestPerformanceReportScript,
341
+ },
342
+ {
343
+ packId: 'test',
344
+ id: 'regression-selector',
345
+ ref: scriptRef('test', 'regression-selector'),
346
+ usage: 'mf script-pack run test/regression-selector select [path...] [options]',
347
+ summaryKey: 'scriptPack.script.testRegressionSelector.summary',
348
+ actions: ['select'],
349
+ useWhen: [
350
+ 'Select likely regression tests from changed files while reporting unsafe surfaces that require fallback verification.',
351
+ 'Review source, test, package, schema, workflow, and config changes before relying on a related-test shortcut.',
352
+ ],
353
+ phases: ['after_change', 'review'],
354
+ readOnly: true,
355
+ mutates: false,
356
+ network: false,
357
+ inputs: ['path', 'base_ref', 'head_ref', 'max_files', 'max_tests'],
358
+ outputs: ['human_summary', 'json_report', 'changed_files', 'selected_tests', 'fallbacks'],
359
+ relatedSkills: ['test-suite-performance-review', 'test-maintenance', 'change-blast-radius-review'],
360
+ riskLevel: 'low',
361
+ cost: 'low',
362
+ reportSchemaFile: 'test-regression-selector-report.schema.json',
363
+ loadRunner: async () => (await import('../script-packs/test-regression-selector.js')).runTestRegressionSelectorScript,
364
+ },
255
365
  ],
256
366
  },
257
367
  {
@@ -372,6 +482,179 @@ export const SCRIPT_PACKS = [
372
482
  reportSchemaFile: 'generated-boundary-report.schema.json',
373
483
  loadRunner: async () => (await import('../script-packs/repo-generated-boundary.js')).runRepoGeneratedBoundaryScript,
374
484
  },
485
+ {
486
+ packId: 'repo',
487
+ id: 'merge-conflict-scan',
488
+ ref: scriptRef('repo', 'merge-conflict-scan'),
489
+ usage: 'mf script-pack run repo/merge-conflict-scan check [path...] [options]',
490
+ summaryKey: 'scriptPack.script.mergeConflictScan.summary',
491
+ actions: ['check'],
492
+ useWhen: [
493
+ 'Scan changed files or explicit paths for Git merge conflict markers without printing surrounding file content.',
494
+ 'Review dirty worktrees before final verification, packaging, commit preparation, or handoff to avoid shipping unresolved conflict markers.',
495
+ ],
496
+ phases: ['after_change', 'review'],
497
+ readOnly: true,
498
+ mutates: false,
499
+ network: false,
500
+ inputs: ['path', 'max_files', 'max_file_bytes'],
501
+ outputs: ['human_summary', 'json_report', 'merge_conflict_markers', 'marker_findings'],
502
+ relatedSkills: [
503
+ 'cli-output-contract-review',
504
+ 'completion-evidence-gate',
505
+ 'public-json-contract-change',
506
+ 'test-maintenance',
507
+ 'typescript-code-change',
508
+ ],
509
+ riskLevel: 'low',
510
+ cost: 'low',
511
+ reportSchemaFile: 'repo-merge-conflict-scan-report.schema.json',
512
+ loadRunner: async () => (await import('../script-packs/repo-merge-conflict-scan.js')).runRepoMergeConflictScanScript,
513
+ },
514
+ {
515
+ packId: 'repo',
516
+ id: 'git-ignore-audit',
517
+ ref: scriptRef('repo', 'git-ignore-audit'),
518
+ usage: 'mf script-pack run repo/git-ignore-audit audit [path...] [options]',
519
+ summaryKey: 'scriptPack.script.gitIgnoreAudit.summary',
520
+ actions: ['audit'],
521
+ useWhen: [
522
+ 'Inspect explicit paths or changed files against .gitignore, .git/info/exclude, core.excludesFile, and config visibility evidence.',
523
+ 'Review ignored, untracked, missing, and tracked-but-ignored paths before assuming Git visibility, package contents, or generated output coverage.',
524
+ ],
525
+ phases: ['before_change', 'after_change', 'review'],
526
+ readOnly: true,
527
+ mutates: false,
528
+ network: false,
529
+ inputs: ['path', 'max_paths'],
530
+ outputs: ['human_summary', 'json_report', 'ignore_sources', 'ignored_path_evidence'],
531
+ relatedSkills: [
532
+ 'cli-output-contract-review',
533
+ 'completion-evidence-gate',
534
+ 'file-path-cross-platform-change',
535
+ 'public-json-contract-change',
536
+ 'test-maintenance',
537
+ ],
538
+ riskLevel: 'low',
539
+ cost: 'low',
540
+ reportSchemaFile: 'repo-git-ignore-audit-report.schema.json',
541
+ loadRunner: async () => (await import('../script-packs/repo-git-ignore-audit.js')).runRepoGitIgnoreAuditScript,
542
+ },
543
+ {
544
+ packId: 'repo',
545
+ id: 'manifest-lock-drift',
546
+ ref: scriptRef('repo', 'manifest-lock-drift'),
547
+ usage: 'mf script-pack run repo/manifest-lock-drift check [path...] [options]',
548
+ summaryKey: 'scriptPack.script.manifestLockDrift.summary',
549
+ actions: ['check'],
550
+ useWhen: [
551
+ 'Compare .mustflow/config/manifest.lock.toml entries with the current repository files without rewriting the lock.',
552
+ 'Review missing, unsafe, unreadable, invalid-hash, and content-hash mismatch evidence before trusting template update or install state.',
553
+ ],
554
+ phases: ['before_change', 'after_change', 'review'],
555
+ readOnly: true,
556
+ mutates: false,
557
+ network: false,
558
+ inputs: ['path', 'max_entries'],
559
+ outputs: ['human_summary', 'json_report', 'manifest_lock_entries', 'hash_mismatch_findings'],
560
+ relatedSkills: [
561
+ 'cli-output-contract-review',
562
+ 'completion-evidence-gate',
563
+ 'contract-sync-check',
564
+ 'file-path-cross-platform-change',
565
+ 'public-json-contract-change',
566
+ 'template-install-surface-sync',
567
+ ],
568
+ riskLevel: 'low',
569
+ cost: 'low',
570
+ reportSchemaFile: 'repo-manifest-lock-drift-report.schema.json',
571
+ loadRunner: async () => (await import('../script-packs/repo-manifest-lock-drift.js')).runRepoManifestLockDriftScript,
572
+ },
573
+ {
574
+ packId: 'repo',
575
+ id: 'skill-route-audit',
576
+ ref: scriptRef('repo', 'skill-route-audit'),
577
+ usage: 'mf script-pack run repo/skill-route-audit audit [options]',
578
+ summaryKey: 'scriptPack.script.skillRouteAudit.summary',
579
+ actions: ['audit'],
580
+ useWhen: [
581
+ 'Audit source skill files, route metadata, skill index entries, default template skill copies, manifest profiles, and i18n metadata for drift.',
582
+ 'Review mustflow skill-route surfaces after adding, refreshing, or synchronizing skills before relying on installed template coverage.',
583
+ ],
584
+ phases: ['before_change', 'after_change', 'review'],
585
+ readOnly: true,
586
+ mutates: false,
587
+ network: false,
588
+ inputs: [],
589
+ outputs: ['human_summary', 'json_report', 'skill_route_findings', 'template_drift', 'manifest_drift', 'i18n_drift'],
590
+ relatedSkills: [
591
+ 'contract-sync-check',
592
+ 'public-json-contract-change',
593
+ 'skill-authoring',
594
+ 'skill-refresh',
595
+ 'template-install-surface-sync',
596
+ ],
597
+ riskLevel: 'low',
598
+ cost: 'low',
599
+ reportSchemaFile: 'skill-route-audit-report.schema.json',
600
+ loadRunner: async () => (await import('../script-packs/repo-skill-route-audit.js')).runRepoSkillRouteAuditScript,
601
+ },
602
+ {
603
+ packId: 'repo',
604
+ id: 'version-source',
605
+ ref: scriptRef('repo', 'version-source'),
606
+ usage: 'mf script-pack run repo/version-source inspect [options]',
607
+ summaryKey: 'scriptPack.script.versionSource.summary',
608
+ actions: ['inspect'],
609
+ useWhen: [
610
+ 'Inspect detected version source files before editing package, template, release, or versioning metadata.',
611
+ 'Review whether enabled release versioning preferences have at least one detected version source.',
612
+ ],
613
+ phases: ['before_change', 'after_change', 'review'],
614
+ readOnly: true,
615
+ mutates: false,
616
+ network: false,
617
+ inputs: [],
618
+ outputs: ['human_summary', 'json_report', 'version_sources', 'versioning_findings'],
619
+ relatedSkills: [
620
+ 'completion-evidence-gate',
621
+ 'public-json-contract-change',
622
+ 'release-publish-change',
623
+ 'version-freshness-check',
624
+ ],
625
+ riskLevel: 'low',
626
+ cost: 'low',
627
+ reportSchemaFile: 'repo-version-source-report.schema.json',
628
+ loadRunner: async () => (await import('../script-packs/repo-version-source.js')).runRepoVersionSourceScript,
629
+ },
630
+ {
631
+ packId: 'repo',
632
+ id: 'approval-gate',
633
+ ref: scriptRef('repo', 'approval-gate'),
634
+ usage: 'mf script-pack run repo/approval-gate check --action <type> [options]',
635
+ summaryKey: 'scriptPack.script.approvalGate.summary',
636
+ actions: ['check'],
637
+ useWhen: [
638
+ 'Check planned action types against .mustflow/config/mustflow.toml [approval].required_for before proceeding.',
639
+ 'Review whether git, dependency, network, database, destructive, secret, release, or cross-repository actions require human approval.',
640
+ ],
641
+ phases: ['before_change', 'review'],
642
+ readOnly: true,
643
+ mutates: false,
644
+ network: false,
645
+ inputs: ['action_type'],
646
+ outputs: ['human_summary', 'json_report', 'approval_decisions', 'approval_findings'],
647
+ relatedSkills: [
648
+ 'command-contract-authoring',
649
+ 'completion-evidence-gate',
650
+ 'public-json-contract-change',
651
+ 'release-publish-change',
652
+ ],
653
+ riskLevel: 'low',
654
+ cost: 'low',
655
+ reportSchemaFile: 'repo-approval-gate-report.schema.json',
656
+ loadRunner: async () => (await import('../script-packs/repo-approval-gate.js')).runRepoApprovalGateScript,
657
+ },
375
658
  {
376
659
  packId: 'repo',
377
660
  id: 'related-files',
@@ -126,6 +126,12 @@ function renderChangeImpactSummary(report, lang) {
126
126
  lines.push(t(lang, 'changeImpact.label.scriptHints'));
127
127
  for (const hint of report.script_hints) {
128
128
  lines.push(`- ${hint.script_ref}: ${hint.command}`);
129
+ if (hint.related_intents && hint.related_intents.length > 0) {
130
+ lines.push(` related intents: ${hint.related_intents.join(', ')}`);
131
+ }
132
+ if (hint.expected_fallback_reasons && hint.expected_fallback_reasons.length > 0) {
133
+ lines.push(` expected fallback reasons: ${hint.expected_fallback_reasons.join(', ')}`);
134
+ }
129
135
  }
130
136
  }
131
137
  if (report.verification_hints.length > 0) {
@@ -0,0 +1,193 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { IMPORT_CYCLE_SCRIPT_REF, inspectImportCycles } from '../../core/import-cycle.js';
6
+ const IMPORT_CYCLE_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--max-files', kind: 'string' },
9
+ { name: '--max-file-bytes', kind: 'string' },
10
+ { name: '--max-depth', kind: 'string' },
11
+ { name: '--max-nodes', kind: 'string' },
12
+ { name: '--max-edges', kind: 'string' },
13
+ { name: '--max-cycles', kind: 'string' },
14
+ ];
15
+ function parsePositiveInteger(value, option, lang) {
16
+ if (value === null) {
17
+ return { value: null };
18
+ }
19
+ if (!/^[1-9]\d*$/u.test(value)) {
20
+ return { value: null, error: t(lang, 'importCycle.error.invalidPositiveInteger', { option, value }) };
21
+ }
22
+ const parsed = Number(value);
23
+ if (!Number.isSafeInteger(parsed)) {
24
+ return { value: null, error: t(lang, 'importCycle.error.invalidPositiveInteger', { option, value }) };
25
+ }
26
+ return { value: parsed };
27
+ }
28
+ export function getCodeImportCycleHelp(lang = 'en') {
29
+ return renderHelp({
30
+ usage: 'mf script-pack run code/import-cycle check <path...> [options]',
31
+ summary: t(lang, 'importCycle.help.summary'),
32
+ options: [
33
+ { label: '--max-depth <count>', description: t(lang, 'importCycle.help.option.maxDepth') },
34
+ { label: '--max-files <count>', description: t(lang, 'importCycle.help.option.maxFiles') },
35
+ { label: '--max-file-bytes <bytes>', description: t(lang, 'importCycle.help.option.maxFileBytes') },
36
+ { label: '--max-nodes <count>', description: t(lang, 'importCycle.help.option.maxNodes') },
37
+ { label: '--max-edges <count>', description: t(lang, 'importCycle.help.option.maxEdges') },
38
+ { label: '--max-cycles <count>', description: t(lang, 'importCycle.help.option.maxCycles') },
39
+ { label: '--json', description: t(lang, 'cli.option.json') },
40
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
41
+ ],
42
+ examples: [
43
+ 'mf script-pack run code/import-cycle check src --json',
44
+ 'mf script-pack run code/import-cycle check src/core --max-depth 30 --json',
45
+ 'mf script-pack run code/import-cycle check src tests --max-cycles 20 --json',
46
+ ],
47
+ exitCodes: [
48
+ { label: '0', description: t(lang, 'importCycle.help.exit.ok') },
49
+ { label: '1', description: t(lang, 'importCycle.help.exit.fail') },
50
+ ],
51
+ }, lang);
52
+ }
53
+ function parseImportCycleOptions(args, lang) {
54
+ const [action, ...rest] = args;
55
+ const parsed = parseCliOptions(rest, IMPORT_CYCLE_OPTIONS, { allowPositionals: true });
56
+ const json = hasParsedCliOption(parsed, '--json');
57
+ const maxFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-files'), '--max-files', lang);
58
+ const maxFileBytes = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-file-bytes'), '--max-file-bytes', lang);
59
+ const maxDepth = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-depth'), '--max-depth', lang);
60
+ const maxNodes = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-nodes'), '--max-nodes', lang);
61
+ const maxEdges = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-edges'), '--max-edges', lang);
62
+ const maxCycles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-cycles'), '--max-cycles', lang);
63
+ const positiveOptions = [maxFiles, maxFileBytes, maxDepth, maxNodes, maxEdges, maxCycles];
64
+ if (action !== 'check') {
65
+ return {
66
+ action: 'check',
67
+ json,
68
+ paths: parsed.positionals,
69
+ maxFiles: maxFiles.value,
70
+ maxFileBytes: maxFileBytes.value,
71
+ maxDepth: maxDepth.value,
72
+ maxNodes: maxNodes.value,
73
+ maxEdges: maxEdges.value,
74
+ maxCycles: maxCycles.value,
75
+ error: action ? t(lang, 'importCycle.error.unknownAction', { action }) : t(lang, 'importCycle.error.missingAction'),
76
+ };
77
+ }
78
+ if (parsed.error) {
79
+ return {
80
+ action,
81
+ json,
82
+ paths: parsed.positionals,
83
+ maxFiles: maxFiles.value,
84
+ maxFileBytes: maxFileBytes.value,
85
+ maxDepth: maxDepth.value,
86
+ maxNodes: maxNodes.value,
87
+ maxEdges: maxEdges.value,
88
+ maxCycles: maxCycles.value,
89
+ error: formatCliOptionParseError(parsed.error, lang),
90
+ };
91
+ }
92
+ for (const candidate of positiveOptions) {
93
+ if (candidate.error) {
94
+ return {
95
+ action,
96
+ json,
97
+ paths: parsed.positionals,
98
+ maxFiles: maxFiles.value,
99
+ maxFileBytes: maxFileBytes.value,
100
+ maxDepth: maxDepth.value,
101
+ maxNodes: maxNodes.value,
102
+ maxEdges: maxEdges.value,
103
+ maxCycles: maxCycles.value,
104
+ error: candidate.error,
105
+ };
106
+ }
107
+ }
108
+ if (parsed.positionals.length === 0) {
109
+ return {
110
+ action,
111
+ json,
112
+ paths: parsed.positionals,
113
+ maxFiles: maxFiles.value,
114
+ maxFileBytes: maxFileBytes.value,
115
+ maxDepth: maxDepth.value,
116
+ maxNodes: maxNodes.value,
117
+ maxEdges: maxEdges.value,
118
+ maxCycles: maxCycles.value,
119
+ error: t(lang, 'importCycle.error.missingPath'),
120
+ };
121
+ }
122
+ return {
123
+ action,
124
+ json,
125
+ paths: parsed.positionals,
126
+ maxFiles: maxFiles.value,
127
+ maxFileBytes: maxFileBytes.value,
128
+ maxDepth: maxDepth.value,
129
+ maxNodes: maxNodes.value,
130
+ maxEdges: maxEdges.value,
131
+ maxCycles: maxCycles.value,
132
+ };
133
+ }
134
+ function renderImportCycleSummary(report, lang) {
135
+ const lines = [
136
+ t(lang, 'importCycle.title'),
137
+ `${t(lang, 'scriptPack.label.script')}: ${IMPORT_CYCLE_SCRIPT_REF}`,
138
+ `${t(lang, 'label.status')}: ${report.status}`,
139
+ `${t(lang, 'importCycle.label.targets')}: ${report.targets.length}`,
140
+ `${t(lang, 'importCycle.label.nodes')}: ${report.graph.node_count}`,
141
+ `${t(lang, 'importCycle.label.edges')}: ${report.graph.edge_count}`,
142
+ `${t(lang, 'importCycle.label.cycles')}: ${report.cycles.length}`,
143
+ `${t(lang, 'importCycle.label.truncated')}: ${report.truncated ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
144
+ ];
145
+ if (report.cycles.length > 0) {
146
+ lines.push(t(lang, 'importCycle.label.cycleList'));
147
+ for (const cycle of report.cycles) {
148
+ lines.push(`- ${cycle.cycle_id}: ${cycle.paths.join(' -> ')}`);
149
+ for (const edge of cycle.edges) {
150
+ lines.push(` - ${edge.source_path}:${edge.line} -> ${edge.target_path} (${edge.kind}, ${edge.specifier})`);
151
+ }
152
+ }
153
+ }
154
+ if (report.findings.length > 0) {
155
+ lines.push(t(lang, 'importCycle.label.findings'));
156
+ for (const finding of report.findings) {
157
+ lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
158
+ }
159
+ }
160
+ if (report.issues.length > 0) {
161
+ lines.push(t(lang, 'importCycle.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
162
+ }
163
+ if (report.cycles.length === 0 && report.findings.length === 0 && report.issues.length === 0) {
164
+ lines.push(t(lang, 'importCycle.clean'));
165
+ }
166
+ return lines.join('\n');
167
+ }
168
+ export function runCodeImportCycleScript(args, reporter, lang = 'en') {
169
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
170
+ reporter.stdout(getCodeImportCycleHelp(lang));
171
+ return 0;
172
+ }
173
+ const options = parseImportCycleOptions(args, lang);
174
+ if (options.error) {
175
+ printUsageError(reporter, options.error, 'mf script-pack run code/import-cycle --help', getCodeImportCycleHelp(lang), lang);
176
+ return 1;
177
+ }
178
+ const report = inspectImportCycles(resolveMustflowRoot(), {
179
+ paths: options.paths,
180
+ maxFiles: options.maxFiles ?? undefined,
181
+ maxFileBytes: options.maxFileBytes ?? undefined,
182
+ maxDepth: options.maxDepth ?? undefined,
183
+ maxNodes: options.maxNodes ?? undefined,
184
+ maxEdges: options.maxEdges ?? undefined,
185
+ maxCycles: options.maxCycles ?? undefined,
186
+ });
187
+ if (options.json) {
188
+ reporter.stdout(JSON.stringify(report, null, 2));
189
+ return report.ok ? 0 : 1;
190
+ }
191
+ reporter.stdout(renderImportCycleSummary(report, lang));
192
+ return report.ok ? 0 : 1;
193
+ }
@@ -0,0 +1,145 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { checkLinkIntegrity, LINK_INTEGRITY_SCRIPT_REF, } from '../../core/docs-link-integrity.js';
6
+ const LINK_INTEGRITY_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--max-files', kind: 'string' },
9
+ { name: '--max-file-bytes', kind: 'string' },
10
+ ];
11
+ function parsePositiveInteger(value, option, lang) {
12
+ if (value === null) {
13
+ return { value: null };
14
+ }
15
+ if (!/^[1-9]\d*$/u.test(value)) {
16
+ return { value: null, error: t(lang, 'linkIntegrity.error.invalidPositiveInteger', { option, value }) };
17
+ }
18
+ const parsed = Number(value);
19
+ if (!Number.isSafeInteger(parsed)) {
20
+ return { value: null, error: t(lang, 'linkIntegrity.error.invalidPositiveInteger', { option, value }) };
21
+ }
22
+ return { value: parsed };
23
+ }
24
+ export function getDocsLinkIntegrityHelp(lang = 'en') {
25
+ return renderHelp({
26
+ usage: 'mf script-pack run docs/link-integrity check [path...] [options]',
27
+ summary: t(lang, 'linkIntegrity.help.summary'),
28
+ options: [
29
+ { label: '--max-files <count>', description: t(lang, 'linkIntegrity.help.option.maxFiles') },
30
+ { label: '--max-file-bytes <bytes>', description: t(lang, 'linkIntegrity.help.option.maxFileBytes') },
31
+ { label: '--json', description: t(lang, 'cli.option.json') },
32
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
33
+ ],
34
+ examples: [
35
+ 'mf script-pack run docs/link-integrity check --json',
36
+ 'mf script-pack run docs/link-integrity check README.md schemas/README.md --json',
37
+ 'mf script-pack run docs/link-integrity check docs-site/src/content/docs --max-files 80 --json',
38
+ ],
39
+ exitCodes: [
40
+ { label: '0', description: t(lang, 'linkIntegrity.help.exit.ok') },
41
+ { label: '1', description: t(lang, 'linkIntegrity.help.exit.fail') },
42
+ ],
43
+ }, lang);
44
+ }
45
+ function parseLinkIntegrityOptions(args, lang) {
46
+ const [action, ...rest] = args;
47
+ const parsed = parseCliOptions(rest, LINK_INTEGRITY_OPTIONS, { allowPositionals: true });
48
+ const json = hasParsedCliOption(parsed, '--json');
49
+ const maxFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-files'), '--max-files', lang);
50
+ const maxFileBytes = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-file-bytes'), '--max-file-bytes', lang);
51
+ if (action !== 'check') {
52
+ return {
53
+ action: 'check',
54
+ json,
55
+ paths: parsed.positionals,
56
+ maxFiles: maxFiles.value,
57
+ maxFileBytes: maxFileBytes.value,
58
+ error: action
59
+ ? t(lang, 'linkIntegrity.error.unknownAction', { action })
60
+ : t(lang, 'linkIntegrity.error.missingAction'),
61
+ };
62
+ }
63
+ if (parsed.error) {
64
+ return {
65
+ action,
66
+ json,
67
+ paths: parsed.positionals,
68
+ maxFiles: maxFiles.value,
69
+ maxFileBytes: maxFileBytes.value,
70
+ error: formatCliOptionParseError(parsed.error, lang),
71
+ };
72
+ }
73
+ for (const candidate of [maxFiles, maxFileBytes]) {
74
+ if (candidate.error) {
75
+ return {
76
+ action,
77
+ json,
78
+ paths: parsed.positionals,
79
+ maxFiles: maxFiles.value,
80
+ maxFileBytes: maxFileBytes.value,
81
+ error: candidate.error,
82
+ };
83
+ }
84
+ }
85
+ return {
86
+ action,
87
+ json,
88
+ paths: parsed.positionals,
89
+ maxFiles: maxFiles.value,
90
+ maxFileBytes: maxFileBytes.value,
91
+ };
92
+ }
93
+ function renderLinkIntegritySummary(report, lang) {
94
+ const lines = [
95
+ t(lang, 'linkIntegrity.title'),
96
+ `${t(lang, 'scriptPack.label.script')}: ${LINK_INTEGRITY_SCRIPT_REF}`,
97
+ `${t(lang, 'label.status')}: ${report.status}`,
98
+ `${t(lang, 'linkIntegrity.label.files')}: ${report.files.length}`,
99
+ `${t(lang, 'linkIntegrity.label.links')}: ${report.links.length}`,
100
+ `${t(lang, 'linkIntegrity.label.findings')}: ${report.findings.length}`,
101
+ ];
102
+ if (report.links.length > 0) {
103
+ lines.push(t(lang, 'linkIntegrity.label.links'));
104
+ for (const link of report.links.slice(0, 40)) {
105
+ const resolved = link.resolved_path === null ? link.target : link.resolved_path;
106
+ lines.push(`- ${link.path}:${link.line}: ${link.kind} ${resolved} (${link.status})`);
107
+ }
108
+ }
109
+ if (report.findings.length > 0) {
110
+ lines.push(t(lang, 'linkIntegrity.label.findings'));
111
+ for (const finding of report.findings) {
112
+ const line = finding.line === undefined ? '' : `:${finding.line}`;
113
+ lines.push(`- ${finding.path}${line}: ${finding.code} (${finding.message})`);
114
+ }
115
+ }
116
+ if (report.issues.length > 0) {
117
+ lines.push(t(lang, 'linkIntegrity.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
118
+ }
119
+ if (report.links.length === 0 && report.findings.length === 0 && report.issues.length === 0) {
120
+ lines.push(t(lang, 'linkIntegrity.clean'));
121
+ }
122
+ return lines.join('\n');
123
+ }
124
+ export function runDocsLinkIntegrityScript(args, reporter, lang = 'en') {
125
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
126
+ reporter.stdout(getDocsLinkIntegrityHelp(lang));
127
+ return 0;
128
+ }
129
+ const options = parseLinkIntegrityOptions(args, lang);
130
+ if (options.error) {
131
+ printUsageError(reporter, options.error, 'mf script-pack run docs/link-integrity --help', getDocsLinkIntegrityHelp(lang), lang);
132
+ return 1;
133
+ }
134
+ const report = checkLinkIntegrity(resolveMustflowRoot(), {
135
+ paths: options.paths,
136
+ maxFiles: options.maxFiles ?? undefined,
137
+ maxFileBytes: options.maxFileBytes ?? undefined,
138
+ });
139
+ if (options.json) {
140
+ reporter.stdout(JSON.stringify(report, null, 2));
141
+ return report.ok ? 0 : 1;
142
+ }
143
+ reporter.stdout(renderLinkIntegritySummary(report, lang));
144
+ return report.ok ? 0 : 1;
145
+ }