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
@@ -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
+ }
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { existsSync } from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { classifyChangeSurface, normalizeChangePath, selectorFallbackReasonForChangedPath, statusFromGitNameStatus, } from './change-surface-classification.js';
5
6
  import { inspectDependencyGraph } from './dependency-graph.js';
6
7
  export const CHANGE_IMPACT_PACK_ID = 'code';
7
8
  export const CHANGE_IMPACT_SCRIPT_ID = 'change-impact';
@@ -10,12 +11,8 @@ const DEFAULT_BASE_REF = 'HEAD';
10
11
  const DEFAULT_MAX_FILES = 200;
11
12
  const DEFAULT_MAX_IMPACTS = 300;
12
13
  const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
13
- const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
14
- function toPosixPath(value) {
15
- return value.replace(/\\/gu, '/');
16
- }
17
14
  function normalizeRelativePath(value) {
18
- return toPosixPath(value).replace(/^\.\/+/u, '') || '.';
15
+ return normalizeChangePath(value);
19
16
  }
20
17
  function sha256Tagged(value) {
21
18
  return `sha256:${createHash('sha256').update(value).digest('hex')}`;
@@ -42,53 +39,11 @@ function isInsideGitWorktree(root) {
42
39
  function makeFinding(code, severity, pathValue, message) {
43
40
  return { code, severity, path: pathValue, message };
44
41
  }
45
- function surfaceForPath(relativePath) {
46
- const normalized = normalizeRelativePath(relativePath);
47
- const extension = path.extname(normalized).toLowerCase();
48
- const basename = path.basename(normalized).toLowerCase();
49
- if (normalized.startsWith('.mustflow/')) {
50
- return 'workflow';
51
- }
52
- if (normalized.startsWith('templates/')) {
53
- return 'template';
54
- }
55
- if (normalized.startsWith('schemas/') || basename.endsWith('.schema.json')) {
56
- return 'schema';
57
- }
58
- if (normalized.startsWith('docs') || ['.md', '.mdx'].includes(extension)) {
59
- return 'docs';
60
- }
61
- if (normalized.includes('/i18n/') || normalized.includes('/locales/') || basename.includes('i18n')) {
62
- return 'i18n';
63
- }
64
- if (['package.json', 'bun.lock', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'].includes(normalized)) {
65
- return 'package';
66
- }
67
- if (/^(?:tsconfig|eslint|vite|vitest|jest|playwright|astro|svelte|tailwind)\b/u.test(basename)) {
68
- return 'config';
69
- }
70
- if (normalized.startsWith('tests/') || /\.test\.[cm]?[jt]sx?$/u.test(normalized)) {
71
- return 'test';
72
- }
73
- if (SOURCE_EXTENSIONS.includes(extension)) {
74
- return 'source';
75
- }
76
- return 'unknown';
77
- }
78
42
  function statusFromCode(code) {
79
- if (code.startsWith('A')) {
80
- return 'added';
81
- }
82
- if (code.startsWith('D')) {
83
- return 'deleted';
84
- }
85
- if (code.startsWith('R')) {
86
- return 'renamed';
87
- }
88
- if (code.startsWith('T')) {
89
- return 'type_changed';
90
- }
91
- return 'modified';
43
+ return statusFromGitNameStatus(code);
44
+ }
45
+ function surfaceForPath(relativePath) {
46
+ return classifyChangeSurface(relativePath);
92
47
  }
93
48
  function parseNameStatus(stdout) {
94
49
  const files = [];
@@ -256,8 +211,25 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
256
211
  }
257
212
  function createScriptHints(changedFiles) {
258
213
  const sourcePaths = changedFiles.filter((file) => file.surface === 'source').map((file) => file.path);
214
+ const selectorRelevantFiles = changedFiles.filter((file) => ['source', 'test', 'schema', 'config', 'package', 'template', 'workflow', 'unknown'].includes(file.surface));
259
215
  const hasDocsOrSchema = changedFiles.some((file) => file.surface === 'docs' || file.surface === 'schema');
260
216
  const hints = [];
217
+ if (selectorRelevantFiles.length > 0) {
218
+ const expectedFallbackReasons = [
219
+ ...new Set(selectorRelevantFiles.map(selectorFallbackReasonForChangedPath).filter((reason) => reason !== null)),
220
+ ].sort();
221
+ const relatedIntents = createVerificationHints(changedFiles).map((hint) => hint.intent);
222
+ hints.push({
223
+ script_ref: 'test/regression-selector',
224
+ command: 'mf script-pack run test/regression-selector select --base HEAD --json',
225
+ reason: expectedFallbackReasons.length > 0
226
+ ? 'Select likely regression tests and expose explicit fallback reasons for unsafe changed surfaces.'
227
+ : 'Select likely regression tests for changed source and test surfaces before choosing a cached related-test shortcut.',
228
+ confidence: expectedFallbackReasons.length > 0 ? 0.9 : 0.8,
229
+ related_intents: relatedIntents,
230
+ expected_fallback_reasons: expectedFallbackReasons,
231
+ });
232
+ }
261
233
  if (sourcePaths.length > 0) {
262
234
  const pathPart = sourcePaths.map((entry) => JSON.stringify(entry)).join(' ');
263
235
  hints.push({