mustflow 2.84.0 → 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 (105) hide show
  1. package/README.md +11 -2
  2. package/dist/cli/commands/script-pack.js +14 -0
  3. package/dist/cli/i18n/en.js +262 -0
  4. package/dist/cli/i18n/es.js +262 -0
  5. package/dist/cli/i18n/fr.js +262 -0
  6. package/dist/cli/i18n/hi.js +262 -0
  7. package/dist/cli/i18n/ko.js +262 -0
  8. package/dist/cli/i18n/zh.js +262 -0
  9. package/dist/cli/lib/repo-map.js +27 -6
  10. package/dist/cli/lib/run-root-trust.js +15 -1
  11. package/dist/cli/lib/script-pack-registry.js +397 -0
  12. package/dist/cli/lib/validation/index.js +2 -2
  13. package/dist/cli/lib/validation/primitives.js +4 -1
  14. package/dist/cli/script-packs/code-change-impact.js +178 -0
  15. package/dist/cli/script-packs/code-dependency-graph.js +181 -0
  16. package/dist/cli/script-packs/code-import-cycle.js +193 -0
  17. package/dist/cli/script-packs/docs-link-integrity.js +145 -0
  18. package/dist/cli/script-packs/repo-approval-gate.js +100 -0
  19. package/dist/cli/script-packs/repo-env-contract.js +156 -0
  20. package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
  21. package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
  22. package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
  23. package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
  24. package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
  25. package/dist/cli/script-packs/repo-version-source.js +92 -0
  26. package/dist/cli/script-packs/test-performance-report.js +247 -0
  27. package/dist/cli/script-packs/test-regression-selector.js +167 -0
  28. package/dist/core/change-impact.js +355 -0
  29. package/dist/core/change-surface-classification.js +198 -0
  30. package/dist/core/change-verification.js +32 -5
  31. package/dist/core/config-loading.js +121 -4
  32. package/dist/core/dependency-graph.js +490 -0
  33. package/dist/core/docs-link-integrity.js +443 -0
  34. package/dist/core/env-contract.js +450 -0
  35. package/dist/core/import-cycle.js +152 -0
  36. package/dist/core/line-endings.js +26 -13
  37. package/dist/core/public-json-contracts.js +167 -0
  38. package/dist/core/repo-approval-gate.js +116 -0
  39. package/dist/core/repo-git-ignore-audit.js +302 -0
  40. package/dist/core/repo-manifest-lock-drift.js +321 -0
  41. package/dist/core/repo-merge-conflict-scan.js +335 -0
  42. package/dist/core/repo-version-source.js +82 -0
  43. package/dist/core/route-outline.js +57 -5
  44. package/dist/core/script-pack-suggestions.js +97 -1
  45. package/dist/core/secret-risk-scan.js +440 -0
  46. package/dist/core/skill-route-audit.js +354 -0
  47. package/dist/core/test-performance-report.js +697 -0
  48. package/dist/core/test-regression-selector.js +335 -0
  49. package/package.json +1 -1
  50. package/schemas/README.md +54 -0
  51. package/schemas/change-impact-report.schema.json +184 -0
  52. package/schemas/commands.schema.json +12 -0
  53. package/schemas/dependency-graph-report.schema.json +149 -0
  54. package/schemas/env-contract-report.schema.json +203 -0
  55. package/schemas/import-cycle-report.schema.json +157 -0
  56. package/schemas/link-integrity-report.schema.json +176 -0
  57. package/schemas/repo-approval-gate-report.schema.json +115 -0
  58. package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
  59. package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
  60. package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
  61. package/schemas/repo-version-source-report.schema.json +127 -0
  62. package/schemas/secret-risk-scan-report.schema.json +152 -0
  63. package/schemas/skill-route-audit-report.schema.json +144 -0
  64. package/schemas/test-performance-report.schema.json +319 -0
  65. package/schemas/test-regression-selector-report.schema.json +187 -0
  66. package/templates/default/i18n.toml +80 -26
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +51 -9
  68. package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
  69. package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
  70. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
  71. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
  72. package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
  73. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
  74. package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
  75. package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
  76. package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
  77. package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
  78. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
  79. package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +23 -8
  80. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
  81. package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
  82. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
  83. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
  84. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
  85. package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
  86. package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
  87. package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
  88. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
  89. package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
  90. package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
  91. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
  92. package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
  93. package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
  94. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
  95. package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
  96. package/templates/default/locales/en/.mustflow/skills/routes.toml +69 -9
  97. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
  98. package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
  99. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
  100. package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
  101. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +323 -0
  102. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +18 -10
  103. package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
  104. package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
  105. package/templates/default/manifest.toml +71 -1
@@ -0,0 +1,86 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { auditSkillRoutes, SKILL_ROUTE_AUDIT_SCRIPT_REF, } from '../../core/skill-route-audit.js';
6
+ const SKILL_ROUTE_AUDIT_OPTIONS = [{ name: '--json', kind: 'boolean' }];
7
+ export function getRepoSkillRouteAuditHelp(lang = 'en') {
8
+ return renderHelp({
9
+ usage: 'mf script-pack run repo/skill-route-audit audit [options]',
10
+ summary: t(lang, 'skillRouteAudit.help.summary'),
11
+ options: [
12
+ { label: '--json', description: t(lang, 'cli.option.json') },
13
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
14
+ ],
15
+ examples: [
16
+ 'mf script-pack run repo/skill-route-audit audit',
17
+ 'mf script-pack run repo/skill-route-audit audit --json',
18
+ ],
19
+ exitCodes: [
20
+ { label: '0', description: t(lang, 'skillRouteAudit.help.exit.ok') },
21
+ { label: '1', description: t(lang, 'skillRouteAudit.help.exit.fail') },
22
+ ],
23
+ }, lang);
24
+ }
25
+ function parseSkillRouteAuditOptions(args, lang) {
26
+ const [action, ...rest] = args;
27
+ const parsed = parseCliOptions(rest, SKILL_ROUTE_AUDIT_OPTIONS, { allowPositionals: false });
28
+ const json = hasParsedCliOption(parsed, '--json');
29
+ if (action !== 'audit') {
30
+ return {
31
+ action: 'audit',
32
+ json,
33
+ error: action
34
+ ? t(lang, 'skillRouteAudit.error.unknownAction', { action })
35
+ : t(lang, 'skillRouteAudit.error.missingAction'),
36
+ };
37
+ }
38
+ if (parsed.error) {
39
+ return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
40
+ }
41
+ return { action, json };
42
+ }
43
+ function renderSkillRouteAuditSummary(report, lang) {
44
+ const lines = [
45
+ t(lang, 'skillRouteAudit.title'),
46
+ `${t(lang, 'scriptPack.label.script')}: ${SKILL_ROUTE_AUDIT_SCRIPT_REF}`,
47
+ `${t(lang, 'label.status')}: ${report.status}`,
48
+ `${t(lang, 'skillRouteAudit.label.sourceSkills')}: ${report.counts.source_skills}`,
49
+ `${t(lang, 'skillRouteAudit.label.routeMetadata')}: ${report.counts.route_metadata}`,
50
+ `${t(lang, 'skillRouteAudit.label.indexRoutes')}: ${report.counts.index_routes}`,
51
+ `${t(lang, 'skillRouteAudit.label.templateSkills')}: ${report.counts.template_skills}`,
52
+ `${t(lang, 'skillRouteAudit.label.findings')}: ${report.findings.length}`,
53
+ ];
54
+ if (report.findings.length > 0) {
55
+ lines.push(t(lang, 'skillRouteAudit.label.findings'));
56
+ for (const finding of report.findings) {
57
+ const target = finding.skill ?? finding.route ?? finding.path;
58
+ lines.push(`- ${target}: ${finding.code} (${finding.message})`);
59
+ }
60
+ }
61
+ if (report.issues.length > 0) {
62
+ lines.push(t(lang, 'generatedBoundary.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
63
+ }
64
+ if (report.findings.length === 0 && report.issues.length === 0) {
65
+ lines.push(t(lang, 'skillRouteAudit.clean'));
66
+ }
67
+ return lines.join('\n');
68
+ }
69
+ export function runRepoSkillRouteAuditScript(args, reporter, lang = 'en') {
70
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
71
+ reporter.stdout(getRepoSkillRouteAuditHelp(lang));
72
+ return 0;
73
+ }
74
+ const options = parseSkillRouteAuditOptions(args, lang);
75
+ if (options.error) {
76
+ printUsageError(reporter, options.error, 'mf script-pack run repo/skill-route-audit --help', getRepoSkillRouteAuditHelp(lang), lang);
77
+ return 1;
78
+ }
79
+ const report = auditSkillRoutes(resolveMustflowRoot());
80
+ if (options.json) {
81
+ reporter.stdout(JSON.stringify(report, null, 2));
82
+ return report.ok ? 0 : 1;
83
+ }
84
+ reporter.stdout(renderSkillRouteAuditSummary(report, lang));
85
+ return report.ok ? 0 : 1;
86
+ }
@@ -0,0 +1,92 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectRepoVersionSource, REPO_VERSION_SOURCE_SCRIPT_REF, } from '../../core/repo-version-source.js';
6
+ const REPO_VERSION_SOURCE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
7
+ export function getRepoVersionSourceHelp(lang = 'en') {
8
+ return renderHelp({
9
+ usage: 'mf script-pack run repo/version-source inspect [options]',
10
+ summary: t(lang, 'versionSource.help.summary'),
11
+ options: [
12
+ { label: '--json', description: t(lang, 'cli.option.json') },
13
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
14
+ ],
15
+ examples: [
16
+ 'mf script-pack run repo/version-source inspect',
17
+ 'mf script-pack run repo/version-source inspect --json',
18
+ ],
19
+ exitCodes: [
20
+ { label: '0', description: t(lang, 'versionSource.help.exit.ok') },
21
+ { label: '1', description: t(lang, 'versionSource.help.exit.fail') },
22
+ ],
23
+ }, lang);
24
+ }
25
+ function parseRepoVersionSourceOptions(args, lang) {
26
+ const [action, ...rest] = args;
27
+ const parsed = parseCliOptions(rest, REPO_VERSION_SOURCE_OPTIONS, { allowPositionals: false });
28
+ const json = hasParsedCliOption(parsed, '--json');
29
+ if (action !== 'inspect') {
30
+ return {
31
+ action: 'inspect',
32
+ json,
33
+ error: action
34
+ ? t(lang, 'versionSource.error.unknownAction', { action })
35
+ : t(lang, 'versionSource.error.missingAction'),
36
+ };
37
+ }
38
+ if (parsed.error) {
39
+ return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
40
+ }
41
+ return { action, json };
42
+ }
43
+ function renderRepoVersionSourceSummary(report, lang) {
44
+ const authorityParts = [
45
+ `source ${report.counts.source_authority_sources}`,
46
+ `derived ${report.counts.derived_authority_sources}`,
47
+ `unclassified ${report.counts.unclassified_authority_sources}`,
48
+ ];
49
+ const lines = [
50
+ t(lang, 'versionSource.title'),
51
+ `${t(lang, 'scriptPack.label.script')}: ${REPO_VERSION_SOURCE_SCRIPT_REF}`,
52
+ `${t(lang, 'label.status')}: ${report.status}`,
53
+ `${t(lang, 'versionSource.label.versioning')}: ${report.versioning_enabled ? t(lang, 'versionSources.value.enabled') : t(lang, 'versionSources.value.disabled')}`,
54
+ `${t(lang, 'versionSource.label.sources')}: ${report.counts.sources}`,
55
+ `${t(lang, 'versionSource.label.sourceAuthorities')}: ${authorityParts.join(', ')}`,
56
+ ];
57
+ for (const source of report.sources) {
58
+ const metadata = [source.kind, source.declared ? 'declared' : undefined, source.authority].filter(Boolean).join(', ');
59
+ lines.push(`- ${source.path} (${metadata})`);
60
+ }
61
+ if (report.findings.length > 0) {
62
+ lines.push(t(lang, 'versionSource.label.findings'));
63
+ for (const finding of report.findings) {
64
+ lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
65
+ }
66
+ }
67
+ if (report.issues.length > 0) {
68
+ lines.push(t(lang, 'versionSource.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
69
+ }
70
+ if (report.sources.length === 0 && report.findings.length === 0 && report.issues.length === 0) {
71
+ lines.push(t(lang, 'versionSources.noSources'));
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ export function runRepoVersionSourceScript(args, reporter, lang = 'en') {
76
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
77
+ reporter.stdout(getRepoVersionSourceHelp(lang));
78
+ return 0;
79
+ }
80
+ const options = parseRepoVersionSourceOptions(args, lang);
81
+ if (options.error) {
82
+ printUsageError(reporter, options.error, 'mf script-pack run repo/version-source --help', getRepoVersionSourceHelp(lang), lang);
83
+ return 1;
84
+ }
85
+ const report = inspectRepoVersionSource(resolveMustflowRoot());
86
+ if (options.json) {
87
+ reporter.stdout(JSON.stringify(report, null, 2));
88
+ return report.ok ? 0 : 1;
89
+ }
90
+ reporter.stdout(renderRepoVersionSourceSummary(report, lang));
91
+ return report.ok ? 0 : 1;
92
+ }
@@ -0,0 +1,247 @@
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 { createTestPerformanceReport, TEST_PERFORMANCE_REPORT_SCRIPT_REF, } from '../../core/test-performance-report.js';
6
+ const TEST_PERFORMANCE_REPORT_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--max-samples', kind: 'string' },
9
+ { name: '--max-intents', kind: 'string' },
10
+ { name: '--max-test-files', kind: 'string' },
11
+ { name: '--max-findings', kind: 'string' },
12
+ { name: '--slow-ms', kind: 'string' },
13
+ { name: '--timeout-ratio', kind: 'string' },
14
+ { name: '--phase-ms', kind: 'string' },
15
+ ];
16
+ function parsePositiveInteger(value, option, lang) {
17
+ if (value === null) {
18
+ return { value: null };
19
+ }
20
+ if (!/^[1-9]\d*$/u.test(value)) {
21
+ return { value: null, error: t(lang, 'testPerformance.error.invalidPositiveInteger', { option, value }) };
22
+ }
23
+ const parsed = Number(value);
24
+ if (!Number.isSafeInteger(parsed)) {
25
+ return { value: null, error: t(lang, 'testPerformance.error.invalidPositiveInteger', { option, value }) };
26
+ }
27
+ return { value: parsed };
28
+ }
29
+ function parsePositiveRatio(value, option, lang) {
30
+ if (value === null) {
31
+ return { value: null };
32
+ }
33
+ const parsed = Number(value);
34
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
35
+ return { value: null, error: t(lang, 'testPerformance.error.invalidRatio', { option, value }) };
36
+ }
37
+ return { value: parsed };
38
+ }
39
+ export function getTestPerformanceReportHelp(lang = 'en') {
40
+ return renderHelp({
41
+ usage: 'mf script-pack run test/performance-report summarize [options]',
42
+ summary: t(lang, 'testPerformance.help.summary'),
43
+ options: [
44
+ { label: '--max-samples <count>', description: t(lang, 'testPerformance.help.option.maxSamples') },
45
+ { label: '--max-intents <count>', description: t(lang, 'testPerformance.help.option.maxIntents') },
46
+ { label: '--max-test-files <count>', description: t(lang, 'testPerformance.help.option.maxTestFiles') },
47
+ { label: '--max-findings <count>', description: t(lang, 'testPerformance.help.option.maxFindings') },
48
+ { label: '--slow-ms <ms>', description: t(lang, 'testPerformance.help.option.slowMs') },
49
+ { label: '--timeout-ratio <ratio>', description: t(lang, 'testPerformance.help.option.timeoutRatio') },
50
+ { label: '--phase-ms <ms>', description: t(lang, 'testPerformance.help.option.phaseMs') },
51
+ { label: '--json', description: t(lang, 'cli.option.json') },
52
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
53
+ ],
54
+ examples: [
55
+ 'mf script-pack run test/performance-report summarize --json',
56
+ 'mf script-pack run test/performance-report summarize --max-samples 100 --slow-ms 60000 --json',
57
+ 'mf script-pack run test/performance-report summarize --timeout-ratio 0.8 --phase-ms 15000',
58
+ ],
59
+ exitCodes: [
60
+ { label: '0', description: t(lang, 'testPerformance.help.exit.ok') },
61
+ { label: '1', description: t(lang, 'testPerformance.help.exit.fail') },
62
+ ],
63
+ }, lang);
64
+ }
65
+ function parseTestPerformanceReportOptions(args, lang) {
66
+ const [action, ...rest] = args;
67
+ const parsed = parseCliOptions(rest, TEST_PERFORMANCE_REPORT_OPTIONS, { allowPositionals: false });
68
+ const json = hasParsedCliOption(parsed, '--json');
69
+ const maxSamples = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-samples'), '--max-samples', lang);
70
+ const maxIntents = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-intents'), '--max-intents', lang);
71
+ const maxTestFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-test-files'), '--max-test-files', lang);
72
+ const maxFindings = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-findings'), '--max-findings', lang);
73
+ const slowMs = parsePositiveInteger(getParsedCliStringOption(parsed, '--slow-ms'), '--slow-ms', lang);
74
+ const timeoutRatio = parsePositiveRatio(getParsedCliStringOption(parsed, '--timeout-ratio'), '--timeout-ratio', lang);
75
+ const phaseMs = parsePositiveInteger(getParsedCliStringOption(parsed, '--phase-ms'), '--phase-ms', lang);
76
+ if (action !== 'summarize') {
77
+ return {
78
+ action: 'summarize',
79
+ json,
80
+ maxSamples: maxSamples.value,
81
+ maxIntents: maxIntents.value,
82
+ maxTestFiles: maxTestFiles.value,
83
+ maxFindings: maxFindings.value,
84
+ slowMs: slowMs.value,
85
+ timeoutRatio: timeoutRatio.value,
86
+ phaseMs: phaseMs.value,
87
+ error: action ? t(lang, 'testPerformance.error.unknownAction', { action }) : t(lang, 'testPerformance.error.missingAction'),
88
+ };
89
+ }
90
+ if (parsed.error) {
91
+ return {
92
+ action,
93
+ json,
94
+ maxSamples: maxSamples.value,
95
+ maxIntents: maxIntents.value,
96
+ maxTestFiles: maxTestFiles.value,
97
+ maxFindings: maxFindings.value,
98
+ slowMs: slowMs.value,
99
+ timeoutRatio: timeoutRatio.value,
100
+ phaseMs: phaseMs.value,
101
+ error: formatCliOptionParseError(parsed.error, lang),
102
+ };
103
+ }
104
+ for (const candidate of [maxSamples, maxIntents, maxTestFiles, maxFindings, slowMs, timeoutRatio, phaseMs]) {
105
+ if (candidate.error) {
106
+ return {
107
+ action,
108
+ json,
109
+ maxSamples: maxSamples.value,
110
+ maxIntents: maxIntents.value,
111
+ maxTestFiles: maxTestFiles.value,
112
+ maxFindings: maxFindings.value,
113
+ slowMs: slowMs.value,
114
+ timeoutRatio: timeoutRatio.value,
115
+ phaseMs: phaseMs.value,
116
+ error: candidate.error,
117
+ };
118
+ }
119
+ }
120
+ return {
121
+ action,
122
+ json,
123
+ maxSamples: maxSamples.value,
124
+ maxIntents: maxIntents.value,
125
+ maxTestFiles: maxTestFiles.value,
126
+ maxFindings: maxFindings.value,
127
+ slowMs: slowMs.value,
128
+ timeoutRatio: timeoutRatio.value,
129
+ phaseMs: phaseMs.value,
130
+ };
131
+ }
132
+ function formatProfileAge(ageMs, lang) {
133
+ if (ageMs === null) {
134
+ return t(lang, 'value.none');
135
+ }
136
+ const totalMinutes = Math.floor(ageMs / 60_000);
137
+ if (totalMinutes < 1) {
138
+ return '<1m';
139
+ }
140
+ const days = Math.floor(totalMinutes / 1440);
141
+ const hours = Math.floor((totalMinutes % 1440) / 60);
142
+ const minutes = totalMinutes % 60;
143
+ const parts = [];
144
+ if (days > 0) {
145
+ parts.push(`${days}d`);
146
+ }
147
+ if (hours > 0) {
148
+ parts.push(`${hours}h`);
149
+ }
150
+ if (minutes > 0 || parts.length === 0) {
151
+ parts.push(`${minutes}m`);
152
+ }
153
+ return parts.join(' ');
154
+ }
155
+ function formatProfileCoverage(value, lang) {
156
+ if (value === null) {
157
+ return t(lang, 'value.none');
158
+ }
159
+ return `${(value * 100).toFixed(1)}%`;
160
+ }
161
+ function formatProfileFileCoverage(report) {
162
+ const declaredCount = report.summary.latest_profile_declared_test_file_count;
163
+ const denominator = Math.max(declaredCount ?? 0, report.summary.latest_profile_test_file_count);
164
+ return denominator > 0
165
+ ? `${report.summary.test_file_count}/${denominator}`
166
+ : `${report.summary.test_file_count}/0`;
167
+ }
168
+ function renderTestPerformanceReportSummary(report, lang) {
169
+ const lines = [
170
+ t(lang, 'testPerformance.title'),
171
+ `${t(lang, 'scriptPack.label.script')}: ${TEST_PERFORMANCE_REPORT_SCRIPT_REF}`,
172
+ `${t(lang, 'label.status')}: ${report.status}`,
173
+ `${t(lang, 'testPerformance.label.samples')}: ${report.summary.sample_count}`,
174
+ `${t(lang, 'testPerformance.label.intents')}: ${report.summary.intent_count}`,
175
+ `${t(lang, 'testPerformance.label.testFiles')}: ${report.summary.test_file_count}`,
176
+ `${t(lang, 'testPerformance.label.findings')}: ${report.findings.length}`,
177
+ `${t(lang, 'testPerformance.label.truncated')}: ${report.truncated ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
178
+ ];
179
+ lines.push(t(lang, 'testPerformance.label.profileEvidence'), `- ${t(lang, 'testPerformance.label.profileGeneratedAt')}: ${report.summary.latest_profile_generated_at ?? t(lang, 'value.none')}`, `- ${t(lang, 'testPerformance.label.profileAge')}: ${formatProfileAge(report.summary.latest_profile_age_ms, lang)}`, `- ${t(lang, 'testPerformance.label.profileFilesShown')}: ${formatProfileFileCoverage(report)}`, `- ${t(lang, 'testPerformance.label.profileCoverage')}: ${formatProfileCoverage(report.summary.latest_profile_test_file_coverage_ratio, lang)}`, `- ${t(lang, 'testPerformance.label.profileFilesTruncated')}: ${report.summary.latest_profile_test_files_truncated ? t(lang, 'value.yes') : t(lang, 'value.no')}`);
180
+ if (report.intents.length > 0) {
181
+ lines.push(t(lang, 'testPerformance.label.slowestIntents'));
182
+ for (const entry of report.intents.slice(0, 20)) {
183
+ lines.push(`- ${entry.intent}: p95 ${entry.p95_duration_ms}ms, max ${entry.max_duration_ms}ms, ` +
184
+ `${t(lang, 'testPerformance.label.failures')} ${entry.failure_count}`);
185
+ }
186
+ }
187
+ if (report.test_files.length > 0) {
188
+ lines.push(t(lang, 'testPerformance.label.slowestTestFiles'));
189
+ for (const testFile of report.test_files.slice(0, 20)) {
190
+ lines.push(`- ${testFile.path}: ${testFile.duration_ms}ms, ${testFile.status}`);
191
+ }
192
+ }
193
+ if (report.phases.length > 0) {
194
+ lines.push(t(lang, 'testPerformance.label.slowestPhases'));
195
+ for (const phase of report.phases.slice(0, 10)) {
196
+ lines.push(`- ${phase.name}: p95 ${phase.p95_duration_ms}ms, max ${phase.max_duration_ms}ms`);
197
+ }
198
+ }
199
+ if (report.findings.length > 0) {
200
+ lines.push(t(lang, 'testPerformance.label.findings'));
201
+ for (const finding of report.findings.slice(0, 40)) {
202
+ const intent = finding.intent ? ` ${finding.intent}` : '';
203
+ const phase = finding.phase ? ` ${finding.phase}` : '';
204
+ lines.push(`- ${finding.code}${intent}${phase}: ${finding.message}`);
205
+ }
206
+ }
207
+ if (report.next_actions.length > 0) {
208
+ lines.push(t(lang, 'testPerformance.label.nextActions'));
209
+ for (const action of report.next_actions) {
210
+ const runHint = action.run_hint ? ` (${action.run_hint})` : '';
211
+ lines.push(`- ${action.code}: ${action.message}${runHint}`);
212
+ }
213
+ }
214
+ if (report.issues.length > 0) {
215
+ lines.push(t(lang, 'testPerformance.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
216
+ }
217
+ if (report.summary.sample_count === 0 && report.issues.length === 0) {
218
+ lines.push(t(lang, 'testPerformance.clean'));
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+ export function runTestPerformanceReportScript(args, reporter, lang = 'en') {
223
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
224
+ reporter.stdout(getTestPerformanceReportHelp(lang));
225
+ return 0;
226
+ }
227
+ const options = parseTestPerformanceReportOptions(args, lang);
228
+ if (options.error) {
229
+ printUsageError(reporter, options.error, 'mf script-pack run test/performance-report --help', getTestPerformanceReportHelp(lang), lang);
230
+ return 1;
231
+ }
232
+ const report = createTestPerformanceReport(resolveMustflowRoot(), {
233
+ maxSamples: options.maxSamples ?? undefined,
234
+ maxIntents: options.maxIntents ?? undefined,
235
+ maxTestFiles: options.maxTestFiles ?? undefined,
236
+ maxFindings: options.maxFindings ?? undefined,
237
+ slowSampleThresholdMs: options.slowMs ?? undefined,
238
+ highTimeoutRatio: options.timeoutRatio ?? undefined,
239
+ phaseBottleneckThresholdMs: options.phaseMs ?? undefined,
240
+ });
241
+ if (options.json) {
242
+ reporter.stdout(JSON.stringify(report, null, 2));
243
+ return report.ok ? 0 : 1;
244
+ }
245
+ reporter.stdout(renderTestPerformanceReportSummary(report, lang));
246
+ return report.ok ? 0 : 1;
247
+ }
@@ -0,0 +1,167 @@
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 { createTestRegressionSelectorReport, TEST_REGRESSION_SELECTOR_SCRIPT_REF, } from '../../core/test-regression-selector.js';
6
+ const TEST_REGRESSION_SELECTOR_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--base', kind: 'string' },
9
+ { name: '--head', kind: 'string' },
10
+ { name: '--max-files', kind: 'string' },
11
+ { name: '--max-tests', kind: 'string' },
12
+ ];
13
+ function parsePositiveInteger(value, option, lang) {
14
+ if (value === null) {
15
+ return { value: null };
16
+ }
17
+ if (!/^[1-9]\d*$/u.test(value)) {
18
+ return { value: null, error: t(lang, 'testRegressionSelector.error.invalidPositiveInteger', { option, value }) };
19
+ }
20
+ const parsed = Number(value);
21
+ if (!Number.isSafeInteger(parsed)) {
22
+ return { value: null, error: t(lang, 'testRegressionSelector.error.invalidPositiveInteger', { option, value }) };
23
+ }
24
+ return { value: parsed };
25
+ }
26
+ export function getTestRegressionSelectorHelp(lang = 'en') {
27
+ return renderHelp({
28
+ usage: 'mf script-pack run test/regression-selector select [path...] [options]',
29
+ summary: t(lang, 'testRegressionSelector.help.summary'),
30
+ options: [
31
+ { label: '--base <ref>', description: t(lang, 'testRegressionSelector.help.option.base') },
32
+ { label: '--head <ref>', description: t(lang, 'testRegressionSelector.help.option.head') },
33
+ { label: '--max-files <count>', description: t(lang, 'testRegressionSelector.help.option.maxFiles') },
34
+ { label: '--max-tests <count>', description: t(lang, 'testRegressionSelector.help.option.maxTests') },
35
+ { label: '--json', description: t(lang, 'cli.option.json') },
36
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
37
+ ],
38
+ examples: [
39
+ 'mf script-pack run test/regression-selector select --base HEAD --json',
40
+ 'mf script-pack run test/regression-selector select src tests --base main --head HEAD --json',
41
+ 'mf script-pack run test/regression-selector select --max-tests 20',
42
+ ],
43
+ exitCodes: [
44
+ { label: '0', description: t(lang, 'testRegressionSelector.help.exit.ok') },
45
+ { label: '1', description: t(lang, 'testRegressionSelector.help.exit.fail') },
46
+ ],
47
+ }, lang);
48
+ }
49
+ function parseTestRegressionSelectorOptions(args, lang) {
50
+ const [action, ...rest] = args;
51
+ const parsed = parseCliOptions(rest, TEST_REGRESSION_SELECTOR_OPTIONS, { allowPositionals: true });
52
+ const json = hasParsedCliOption(parsed, '--json');
53
+ const baseRef = getParsedCliStringOption(parsed, '--base');
54
+ const headRef = getParsedCliStringOption(parsed, '--head');
55
+ const maxFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-files'), '--max-files', lang);
56
+ const maxTests = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-tests'), '--max-tests', lang);
57
+ if (action !== 'select') {
58
+ return {
59
+ action: 'select',
60
+ json,
61
+ baseRef,
62
+ headRef,
63
+ paths: parsed.positionals,
64
+ maxFiles: maxFiles.value,
65
+ maxTests: maxTests.value,
66
+ error: action
67
+ ? t(lang, 'testRegressionSelector.error.unknownAction', { action })
68
+ : t(lang, 'testRegressionSelector.error.missingAction'),
69
+ };
70
+ }
71
+ if (parsed.error) {
72
+ return {
73
+ action,
74
+ json,
75
+ baseRef,
76
+ headRef,
77
+ paths: parsed.positionals,
78
+ maxFiles: maxFiles.value,
79
+ maxTests: maxTests.value,
80
+ error: formatCliOptionParseError(parsed.error, lang),
81
+ };
82
+ }
83
+ for (const candidate of [maxFiles, maxTests]) {
84
+ if (candidate.error) {
85
+ return {
86
+ action,
87
+ json,
88
+ baseRef,
89
+ headRef,
90
+ paths: parsed.positionals,
91
+ maxFiles: maxFiles.value,
92
+ maxTests: maxTests.value,
93
+ error: candidate.error,
94
+ };
95
+ }
96
+ }
97
+ return {
98
+ action,
99
+ json,
100
+ baseRef,
101
+ headRef,
102
+ paths: parsed.positionals,
103
+ maxFiles: maxFiles.value,
104
+ maxTests: maxTests.value,
105
+ };
106
+ }
107
+ function renderTestRegressionSelectorSummary(report, lang) {
108
+ const lines = [
109
+ t(lang, 'testRegressionSelector.title'),
110
+ `${t(lang, 'scriptPack.label.script')}: ${TEST_REGRESSION_SELECTOR_SCRIPT_REF}`,
111
+ `${t(lang, 'label.status')}: ${report.status}`,
112
+ `${t(lang, 'testRegressionSelector.label.selectionStatus')}: ${report.summary.selection_status}`,
113
+ `${t(lang, 'testRegressionSelector.label.changedFiles')}: ${report.summary.changed_file_count}`,
114
+ `${t(lang, 'testRegressionSelector.label.selectedTests')}: ${report.summary.selected_test_count}`,
115
+ `${t(lang, 'testRegressionSelector.label.fallbacks')}: ${report.summary.fallback_count}`,
116
+ `${t(lang, 'testRegressionSelector.label.recommendedIntent')}: ${report.summary.recommended_intent}`,
117
+ ];
118
+ if (report.selected_tests.length > 0) {
119
+ lines.push(t(lang, 'testRegressionSelector.label.selectedTests'));
120
+ for (const testCandidate of report.selected_tests.slice(0, 40)) {
121
+ lines.push(`- ${testCandidate.path}: ${testCandidate.reasons.join(', ')} (${testCandidate.source_paths.join(', ')})`);
122
+ }
123
+ }
124
+ if (report.fallbacks.length > 0) {
125
+ lines.push(t(lang, 'testRegressionSelector.label.fallbacks'));
126
+ for (const fallback of report.fallbacks) {
127
+ lines.push(`- ${fallback.path}: ${fallback.reason} -> ${fallback.recommended_intent}; ${fallback.message}`);
128
+ }
129
+ }
130
+ if (report.run_hint) {
131
+ lines.push(`${t(lang, 'testRegressionSelector.label.runHint')}: ${report.run_hint}`);
132
+ }
133
+ if (report.findings.length > 0) {
134
+ lines.push(t(lang, 'testRegressionSelector.label.findings'), ...report.findings.map((finding) => `- ${finding.path}: ${finding.code} (${finding.message})`));
135
+ }
136
+ if (report.issues.length > 0) {
137
+ lines.push(t(lang, 'testRegressionSelector.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
138
+ }
139
+ if (report.changed_files.length === 0 && report.findings.length === 0 && report.issues.length === 0) {
140
+ lines.push(t(lang, 'testRegressionSelector.clean'));
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+ export function runTestRegressionSelectorScript(args, reporter, lang = 'en') {
145
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
146
+ reporter.stdout(getTestRegressionSelectorHelp(lang));
147
+ return 0;
148
+ }
149
+ const options = parseTestRegressionSelectorOptions(args, lang);
150
+ if (options.error) {
151
+ printUsageError(reporter, options.error, 'mf script-pack run test/regression-selector --help', getTestRegressionSelectorHelp(lang), lang);
152
+ return 1;
153
+ }
154
+ const report = createTestRegressionSelectorReport(resolveMustflowRoot(), {
155
+ baseRef: options.baseRef ?? undefined,
156
+ headRef: options.headRef,
157
+ paths: options.paths,
158
+ maxFiles: options.maxFiles ?? undefined,
159
+ maxTests: options.maxTests ?? undefined,
160
+ });
161
+ if (options.json) {
162
+ reporter.stdout(JSON.stringify(report, null, 2));
163
+ return report.ok ? 0 : 1;
164
+ }
165
+ reporter.stdout(renderTestRegressionSelectorSummary(report, lang));
166
+ return report.ok ? 0 : 1;
167
+ }