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,355 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { classifyChangeSurface, normalizeChangePath, selectorFallbackReasonForChangedPath, statusFromGitNameStatus, } from './change-surface-classification.js';
6
+ import { inspectDependencyGraph } from './dependency-graph.js';
7
+ export const CHANGE_IMPACT_PACK_ID = 'code';
8
+ export const CHANGE_IMPACT_SCRIPT_ID = 'change-impact';
9
+ export const CHANGE_IMPACT_SCRIPT_REF = `${CHANGE_IMPACT_PACK_ID}/${CHANGE_IMPACT_SCRIPT_ID}`;
10
+ const DEFAULT_BASE_REF = 'HEAD';
11
+ const DEFAULT_MAX_FILES = 200;
12
+ const DEFAULT_MAX_IMPACTS = 300;
13
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
14
+ function normalizeRelativePath(value) {
15
+ return normalizeChangePath(value);
16
+ }
17
+ function sha256Tagged(value) {
18
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
19
+ }
20
+ function runGit(root, args) {
21
+ const result = spawnSync('git', [...args], {
22
+ cwd: root,
23
+ encoding: 'utf8',
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ windowsHide: true,
26
+ maxBuffer: 16 * 1024 * 1024,
27
+ });
28
+ return {
29
+ ok: result.status === 0,
30
+ stdout: result.stdout ?? '',
31
+ stderr: result.stderr ?? '',
32
+ status: result.status,
33
+ };
34
+ }
35
+ function isInsideGitWorktree(root) {
36
+ const result = runGit(root, ['rev-parse', '--is-inside-work-tree']);
37
+ return result.ok && result.stdout.trim() === 'true';
38
+ }
39
+ function makeFinding(code, severity, pathValue, message) {
40
+ return { code, severity, path: pathValue, message };
41
+ }
42
+ function statusFromCode(code) {
43
+ return statusFromGitNameStatus(code);
44
+ }
45
+ function surfaceForPath(relativePath) {
46
+ return classifyChangeSurface(relativePath);
47
+ }
48
+ function parseNameStatus(stdout) {
49
+ const files = [];
50
+ for (const line of stdout.split(/\r?\n/u)) {
51
+ if (!line.trim()) {
52
+ continue;
53
+ }
54
+ const [statusCode = 'M', firstPath = '', secondPath = ''] = line.split('\t');
55
+ const currentPath = normalizeRelativePath(secondPath || firstPath);
56
+ const previousPath = secondPath ? normalizeRelativePath(firstPath) : null;
57
+ files.push({
58
+ path: currentPath,
59
+ previous_path: previousPath,
60
+ status: statusFromCode(statusCode),
61
+ surface: surfaceForPath(currentPath),
62
+ });
63
+ }
64
+ return files;
65
+ }
66
+ function addUntrackedFiles(root, paths, files) {
67
+ const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard', '--', ...paths]);
68
+ if (!untracked.ok) {
69
+ return;
70
+ }
71
+ const known = new Set(files.map((file) => file.path));
72
+ for (const line of untracked.stdout.split(/\r?\n/u)) {
73
+ const relativePath = normalizeRelativePath(line.trim());
74
+ if (!relativePath || relativePath === '.' || known.has(relativePath)) {
75
+ continue;
76
+ }
77
+ files.push({
78
+ path: relativePath,
79
+ previous_path: null,
80
+ status: 'untracked',
81
+ surface: surfaceForPath(relativePath),
82
+ });
83
+ }
84
+ }
85
+ function addImpact(root, impacts, candidate, policy, findings, issues) {
86
+ const normalized = normalizeRelativePath(candidate.path);
87
+ const key = `${candidate.relationship}:${candidate.source_path}:${normalized}`;
88
+ if (impacts.has(key)) {
89
+ return;
90
+ }
91
+ if (impacts.size >= policy.max_impacts) {
92
+ const message = `Change impact found more than ${policy.max_impacts} candidates; remaining candidates were skipped.`;
93
+ findings.push(makeFinding('change_impact_max_impacts_exceeded', 'high', normalized, message));
94
+ issues.push(message);
95
+ return;
96
+ }
97
+ impacts.set(key, {
98
+ ...candidate,
99
+ path: normalized,
100
+ exists: existsSync(path.join(root, ...normalized.split('/'))),
101
+ surface: surfaceForPath(normalized),
102
+ });
103
+ }
104
+ function siblingBase(relativePath) {
105
+ const directory = path.posix.dirname(normalizeRelativePath(relativePath));
106
+ const parsed = path.posix.parse(normalizeRelativePath(relativePath));
107
+ return directory === '.' ? parsed.name : `${directory}/${parsed.name}`;
108
+ }
109
+ function testCandidatesFor(relativePath) {
110
+ const base = siblingBase(relativePath);
111
+ const name = path.posix.basename(base);
112
+ return [
113
+ `${base}.test.ts`,
114
+ `${base}.test.js`,
115
+ `${base}.spec.ts`,
116
+ `${base}.spec.js`,
117
+ `tests/${name}.test.js`,
118
+ `tests/${name}.test.ts`,
119
+ `tests/cli/${name}.test.js`,
120
+ ];
121
+ }
122
+ function sourceCandidatesFor(relativePath) {
123
+ const normalized = normalizeRelativePath(relativePath);
124
+ return [
125
+ normalized.replace(/^tests\/cli\//u, 'src/cli/').replace(/\.test\.[cm]?[jt]sx?$/u, '.ts'),
126
+ normalized.replace(/^tests\//u, 'src/').replace(/\.test\.[cm]?[jt]sx?$/u, '.ts'),
127
+ ];
128
+ }
129
+ function addSurfaceImpacts(root, changedFile, impacts, policy, findings, issues) {
130
+ addImpact(root, impacts, {
131
+ path: changedFile.path,
132
+ relationship: 'changed_file',
133
+ confidence: 1,
134
+ reason: 'Changed file should be reviewed directly.',
135
+ source_path: changedFile.path,
136
+ }, policy, findings, issues);
137
+ if (changedFile.surface === 'source') {
138
+ for (const candidatePath of testCandidatesFor(changedFile.path)) {
139
+ addImpact(root, impacts, {
140
+ path: candidatePath,
141
+ relationship: 'sibling_test',
142
+ confidence: 0.65,
143
+ reason: 'Nearby test candidate for the changed source path.',
144
+ source_path: changedFile.path,
145
+ }, policy, findings, issues);
146
+ }
147
+ }
148
+ if (changedFile.surface === 'test') {
149
+ for (const candidatePath of sourceCandidatesFor(changedFile.path)) {
150
+ addImpact(root, impacts, {
151
+ path: candidatePath,
152
+ relationship: 'sibling_source',
153
+ confidence: 0.55,
154
+ reason: 'Likely source file covered by the changed test path.',
155
+ source_path: changedFile.path,
156
+ }, policy, findings, issues);
157
+ }
158
+ }
159
+ }
160
+ function addContractImpacts(root, changedFile, impacts, policy, findings, issues) {
161
+ const contractPaths = changedFile.surface === 'schema'
162
+ ? ['schemas/README.md', 'src/core/public-json-contracts.ts', 'tests/cli/schema.test.js']
163
+ : changedFile.surface === 'package' || changedFile.surface === 'template'
164
+ ? ['tests/cli/package.test.js', 'templates/default/manifest.toml']
165
+ : changedFile.surface === 'workflow'
166
+ ? ['.mustflow/config/commands.toml', 'tests/cli/authoring-skill-contracts.test.js']
167
+ : [];
168
+ for (const candidatePath of contractPaths) {
169
+ addImpact(root, impacts, {
170
+ path: candidatePath,
171
+ relationship: changedFile.surface === 'workflow' ? 'workflow_surface' : 'contract_test',
172
+ confidence: 0.7,
173
+ reason: `Contract surface related to changed ${changedFile.surface} file.`,
174
+ source_path: changedFile.path,
175
+ }, policy, findings, issues);
176
+ }
177
+ }
178
+ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, issues) {
179
+ const sourcePaths = changedFiles
180
+ .filter((file) => file.surface === 'source' && file.status !== 'deleted')
181
+ .map((file) => file.path);
182
+ if (sourcePaths.length === 0) {
183
+ return;
184
+ }
185
+ const dependencyReport = inspectDependencyGraph(root, {
186
+ paths: sourcePaths,
187
+ maxDepth: 1,
188
+ maxFiles: policy.max_files,
189
+ maxFileBytes: policy.max_file_bytes,
190
+ maxNodes: policy.max_impacts,
191
+ maxEdges: policy.max_impacts,
192
+ });
193
+ if (!dependencyReport.ok) {
194
+ for (const issue of dependencyReport.issues) {
195
+ issues.push(`dependency-graph: ${issue}`);
196
+ }
197
+ }
198
+ const changedPathSet = new Set(sourcePaths);
199
+ for (const edge of dependencyReport.edges) {
200
+ if (!changedPathSet.has(edge.target_path) || changedPathSet.has(edge.source_path)) {
201
+ continue;
202
+ }
203
+ addImpact(root, impacts, {
204
+ path: edge.source_path,
205
+ relationship: 'imports_changed_file',
206
+ confidence: 0.8,
207
+ reason: `Imports changed file ${edge.target_path} via ${edge.specifier}.`,
208
+ source_path: edge.target_path,
209
+ }, policy, findings, issues);
210
+ }
211
+ }
212
+ function createScriptHints(changedFiles) {
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));
215
+ const hasDocsOrSchema = changedFiles.some((file) => file.surface === 'docs' || file.surface === 'schema');
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
+ }
233
+ if (sourcePaths.length > 0) {
234
+ const pathPart = sourcePaths.map((entry) => JSON.stringify(entry)).join(' ');
235
+ hints.push({
236
+ script_ref: 'code/dependency-graph',
237
+ command: `mf script-pack run code/dependency-graph scan ${pathPart} --json`,
238
+ reason: 'Trace importers and direct dependencies for changed source files.',
239
+ confidence: 0.85,
240
+ });
241
+ hints.push({
242
+ script_ref: 'repo/related-files',
243
+ command: `mf script-pack run repo/related-files map ${pathPart} --json`,
244
+ reason: 'Map nearby source, tests, docs, and config files for changed source paths.',
245
+ confidence: 0.75,
246
+ });
247
+ }
248
+ if (hasDocsOrSchema) {
249
+ hints.push({
250
+ script_ref: 'docs/reference-drift',
251
+ command: 'mf script-pack run docs/reference-drift check README.md schemas/README.md --json',
252
+ reason: 'Check documented command, schema, path, and script-pack references after docs or schema changes.',
253
+ confidence: 0.7,
254
+ });
255
+ }
256
+ return hints;
257
+ }
258
+ function createVerificationHints(changedFiles) {
259
+ const surfaces = new Set(changedFiles.map((file) => file.surface));
260
+ const hints = [];
261
+ const add = (intent, reason, confidence) => {
262
+ if (!hints.some((hint) => hint.intent === intent)) {
263
+ hints.push({ intent, reason, confidence });
264
+ }
265
+ };
266
+ if (surfaces.has('source') || surfaces.has('test')) {
267
+ add('test_related', 'Changed source or tests need related behavior coverage.', 0.85);
268
+ add('build', 'Changed TypeScript or JavaScript source may affect compiled CLI output.', 0.7);
269
+ }
270
+ if (surfaces.has('schema') || surfaces.has('package') || surfaces.has('template')) {
271
+ add('test_release', 'Package, template, or public schema surfaces changed.', 0.85);
272
+ }
273
+ if (surfaces.has('docs') || surfaces.has('schema') || surfaces.has('workflow')) {
274
+ add('docs_validate_fast', 'Docs, schema docs, or workflow text changed.', 0.75);
275
+ }
276
+ if (surfaces.has('workflow') || surfaces.has('template')) {
277
+ add('mustflow_check', 'Workflow or install template surfaces need strict mustflow validation.', 0.8);
278
+ }
279
+ return hints;
280
+ }
281
+ function collectChangedFiles(root, policy, findings, issues) {
282
+ if (!isInsideGitWorktree(root)) {
283
+ const message = 'Git worktree is unavailable; change-impact returned an empty impact set.';
284
+ findings.push(makeFinding('change_impact_git_unavailable', 'low', '.', message));
285
+ issues.push(message);
286
+ return [];
287
+ }
288
+ const diffArgs = policy.head_ref
289
+ ? ['diff', '--name-status', '--diff-filter=ACMRTD', policy.base_ref, policy.head_ref, '--', ...policy.path_filters]
290
+ : ['diff', '--name-status', '--diff-filter=ACMRTD', policy.base_ref, '--', ...policy.path_filters];
291
+ const diff = runGit(root, diffArgs);
292
+ if (!diff.ok) {
293
+ const detail = diff.stderr.trim() || diff.stdout.trim() || `git diff exited with ${diff.status}`;
294
+ findings.push(makeFinding('change_impact_invalid_ref', 'high', '.', detail));
295
+ issues.push(detail);
296
+ return [];
297
+ }
298
+ const files = parseNameStatus(diff.stdout);
299
+ if (!policy.head_ref) {
300
+ addUntrackedFiles(root, policy.path_filters, files);
301
+ }
302
+ files.sort((left, right) => left.path.localeCompare(right.path));
303
+ if (files.length > policy.max_files) {
304
+ const message = `Change impact matched ${files.length} files; max_files is ${policy.max_files}.`;
305
+ findings.push(makeFinding('change_impact_max_files_exceeded', 'high', '.', message));
306
+ issues.push(message);
307
+ }
308
+ return files.slice(0, policy.max_files);
309
+ }
310
+ export function inspectChangeImpact(projectRoot, options = {}) {
311
+ const root = path.resolve(projectRoot);
312
+ const policy = {
313
+ base_ref: options.baseRef ?? DEFAULT_BASE_REF,
314
+ head_ref: options.headRef ?? null,
315
+ compare_worktree: options.headRef === undefined || options.headRef === null,
316
+ max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
317
+ max_impacts: options.maxImpacts ?? DEFAULT_MAX_IMPACTS,
318
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
319
+ path_filters: (options.paths ?? []).map(normalizeRelativePath),
320
+ };
321
+ const findings = [];
322
+ const issues = [];
323
+ const changedFiles = collectChangedFiles(root, policy, findings, issues);
324
+ const impacts = new Map();
325
+ for (const changedFile of changedFiles) {
326
+ addSurfaceImpacts(root, changedFile, impacts, policy, findings, issues);
327
+ addContractImpacts(root, changedFile, impacts, policy, findings, issues);
328
+ }
329
+ addDependencyImpacts(root, changedFiles, impacts, policy, findings, issues);
330
+ const status = findings.some((finding) => finding.severity === 'high' || finding.severity === 'critical')
331
+ ? 'failed'
332
+ : 'passed';
333
+ const inputHash = sha256Tagged(JSON.stringify({ policy, changedFiles }));
334
+ return {
335
+ schema_version: '1',
336
+ command: 'script-pack',
337
+ pack_id: CHANGE_IMPACT_PACK_ID,
338
+ script_id: CHANGE_IMPACT_SCRIPT_ID,
339
+ script_ref: CHANGE_IMPACT_SCRIPT_REF,
340
+ action: 'analyze',
341
+ status,
342
+ ok: status === 'passed',
343
+ mustflow_root: root,
344
+ policy,
345
+ input_hash: inputHash,
346
+ changed_files: changedFiles,
347
+ impacts: [...impacts.values()].sort((left, right) => right.confidence - left.confidence || left.path.localeCompare(right.path)),
348
+ script_hints: createScriptHints(changedFiles),
349
+ verification_hints: createVerificationHints(changedFiles),
350
+ truncated: findings.some((finding) => finding.code === 'change_impact_max_files_exceeded' ||
351
+ finding.code === 'change_impact_max_impacts_exceeded'),
352
+ findings,
353
+ issues,
354
+ };
355
+ }
@@ -0,0 +1,198 @@
1
+ import path from 'node:path';
2
+ export const CHANGE_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
3
+ const PACKAGE_LOCKFILES = new Set(['bun.lock', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'deno.lock']);
4
+ function toPosixPath(value) {
5
+ return value.replace(/\\/gu, '/');
6
+ }
7
+ export function normalizeChangePath(value) {
8
+ return toPosixPath(value).replace(/^\.\/+/u, '') || '.';
9
+ }
10
+ export function statusFromGitNameStatus(code) {
11
+ if (code.startsWith('A')) {
12
+ return 'added';
13
+ }
14
+ if (code.startsWith('D')) {
15
+ return 'deleted';
16
+ }
17
+ if (code.startsWith('R')) {
18
+ return 'renamed';
19
+ }
20
+ if (code.startsWith('T')) {
21
+ return 'type_changed';
22
+ }
23
+ return 'modified';
24
+ }
25
+ export function classifyChangeSurface(relativePath, options = {}) {
26
+ const normalized = normalizeChangePath(relativePath);
27
+ const extension = path.extname(normalized).toLowerCase();
28
+ const basename = path.basename(normalized).toLowerCase();
29
+ if (normalized.startsWith('.github/workflows/')) {
30
+ return 'workflow';
31
+ }
32
+ if (normalized.startsWith('.mustflow/')) {
33
+ return 'workflow';
34
+ }
35
+ if (normalized.startsWith('templates/')) {
36
+ return 'template';
37
+ }
38
+ if (normalized.startsWith('schemas/') || basename.endsWith('.schema.json')) {
39
+ return 'schema';
40
+ }
41
+ if (normalized.startsWith('docs') ||
42
+ (options.includeDocsSite === true && normalized.startsWith('docs-site/')) ||
43
+ ['.md', '.mdx'].includes(extension)) {
44
+ return 'docs';
45
+ }
46
+ if (normalized.includes('/i18n/') || normalized.includes('/locales/') || basename.includes('i18n')) {
47
+ return 'i18n';
48
+ }
49
+ if (normalized === 'package.json' || PACKAGE_LOCKFILES.has(normalized)) {
50
+ return 'package';
51
+ }
52
+ if (/^(?:tsconfig|eslint|vite|vitest|jest|playwright|astro|svelte|tailwind)\b/u.test(basename)) {
53
+ return 'config';
54
+ }
55
+ if (normalized.startsWith('tests/') ||
56
+ /\.test\.[cm]?[jt]sx?$/u.test(normalized) ||
57
+ /\.spec\.[cm]?[jt]sx?$/u.test(normalized)) {
58
+ return 'test';
59
+ }
60
+ if (CHANGE_SOURCE_EXTENSIONS.includes(extension)) {
61
+ return 'source';
62
+ }
63
+ return 'unknown';
64
+ }
65
+ export function selectorFallbackReasonForChangedPath(changedPath) {
66
+ const normalized = normalizeChangePath(changedPath.path);
67
+ const basename = path.posix.basename(normalized).toLowerCase();
68
+ if (changedPath.status === 'deleted') {
69
+ return 'fallback_deleted';
70
+ }
71
+ if (changedPath.status === 'renamed') {
72
+ return 'fallback_renamed';
73
+ }
74
+ if (changedPath.status === 'type_changed') {
75
+ return 'fallback_type_changed';
76
+ }
77
+ if (PACKAGE_LOCKFILES.has(normalized)) {
78
+ return 'fallback_lockfile';
79
+ }
80
+ if (basename === 'package.json') {
81
+ return 'fallback_package_metadata';
82
+ }
83
+ if (changedPath.surface === 'template') {
84
+ return 'fallback_template';
85
+ }
86
+ if (isGeneratedContractPath(normalized)) {
87
+ return 'fallback_generated_contract';
88
+ }
89
+ if (isSharedTestFixturePath(normalized)) {
90
+ return 'fallback_shared_test_fixture';
91
+ }
92
+ if (isMigrationOrDatabasePath(normalized)) {
93
+ return 'fallback_migration_or_database';
94
+ }
95
+ if (normalized.startsWith('.github/workflows/')) {
96
+ return 'fallback_ci_workflow';
97
+ }
98
+ if (normalized.startsWith('.mustflow/')) {
99
+ return 'fallback_mustflow_workflow';
100
+ }
101
+ if (isCompilerOrRunnerConfigPath(normalized) || changedPath.surface === 'config') {
102
+ return 'fallback_compiler_or_runner_config';
103
+ }
104
+ if (changedPath.surface === 'unknown') {
105
+ return 'fallback_unknown';
106
+ }
107
+ return null;
108
+ }
109
+ export function selectorFallbackForChangedPath(changedPath) {
110
+ const reason = selectorFallbackReasonForChangedPath(changedPath);
111
+ if (reason === null) {
112
+ return null;
113
+ }
114
+ return {
115
+ reason,
116
+ path: changedPath.path,
117
+ message: selectorFallbackMessage(reason),
118
+ recommended_intent: selectorFallbackRecommendedIntent(reason),
119
+ };
120
+ }
121
+ function isSharedTestFixturePath(normalized) {
122
+ return (normalized.startsWith('tests/fixtures/') ||
123
+ normalized.startsWith('test/fixtures/') ||
124
+ normalized.startsWith('fixtures/') ||
125
+ /(?:^|\/)(?:test-)?setup\.[cm]?[jt]s$/u.test(normalized) ||
126
+ /(?:^|\/)(?:global-)?setup\.[cm]?[jt]s$/u.test(normalized));
127
+ }
128
+ function isMigrationOrDatabasePath(normalized) {
129
+ const basename = path.posix.basename(normalized).toLowerCase();
130
+ return (normalized.includes('/migrations/') ||
131
+ normalized.startsWith('migrations/') ||
132
+ normalized.includes('/db/') ||
133
+ normalized.includes('/database/') ||
134
+ basename === 'schema.prisma' ||
135
+ basename.endsWith('.sql'));
136
+ }
137
+ function isGeneratedContractPath(normalized) {
138
+ const basename = path.posix.basename(normalized).toLowerCase();
139
+ return (normalized.startsWith('schemas/') ||
140
+ basename.endsWith('.schema.json') ||
141
+ basename === 'openapi.json' ||
142
+ basename === 'openapi.yaml' ||
143
+ basename === 'openapi.yml' ||
144
+ basename === 'asyncapi.json' ||
145
+ basename === 'asyncapi.yaml' ||
146
+ basename === 'asyncapi.yml' ||
147
+ basename === 'schema.graphql');
148
+ }
149
+ function isCompilerOrRunnerConfigPath(normalized) {
150
+ const basename = path.posix.basename(normalized).toLowerCase();
151
+ return /^(?:tsconfig|eslint|vite|vitest|jest|playwright|astro|svelte|tailwind|webpack|rollup|babel|nyc|c8)\b/u.test(basename);
152
+ }
153
+ function selectorFallbackMessage(reason) {
154
+ switch (reason) {
155
+ case 'fallback_deleted':
156
+ return 'Deleted files can invalidate stale tests, public exports, and static dependency mappings.';
157
+ case 'fallback_renamed':
158
+ return 'Renamed files can leave stale import paths, test mappings, and documentation references behind.';
159
+ case 'fallback_type_changed':
160
+ return 'Type-changed files can change executable behavior in ways static test narrowing cannot classify.';
161
+ case 'fallback_lockfile':
162
+ return 'Lockfile changes can alter dependency resolution, toolchain behavior, and package artifacts.';
163
+ case 'fallback_package_metadata':
164
+ return 'Package metadata can change runtime entrypoints, scripts, dependencies, files, and release behavior.';
165
+ case 'fallback_template':
166
+ return 'Template changes can alter installed workflow contracts and downstream repository behavior.';
167
+ case 'fallback_generated_contract':
168
+ return 'Schema, OpenAPI, GraphQL, or generated-contract changes need contract and release-sensitive verification.';
169
+ case 'fallback_shared_test_fixture':
170
+ return 'Shared test fixtures or setup files can affect many tests outside the static source map.';
171
+ case 'fallback_migration_or_database':
172
+ return 'Migration, database, or SQL changes can alter shared test state and runtime contracts.';
173
+ case 'fallback_ci_workflow':
174
+ return 'CI workflow changes can alter job ordering, environment, caches, and selected-test safety.';
175
+ case 'fallback_mustflow_workflow':
176
+ return 'Mustflow workflow changes can alter command contracts, routing, skills, or verification policy.';
177
+ case 'fallback_compiler_or_runner_config':
178
+ return 'Compiler, bundler, lint, or test-runner config changes are unsafe for static test narrowing.';
179
+ case 'fallback_unknown':
180
+ return 'Unknown change surfaces need the configured fallback path instead of selected-test confidence.';
181
+ default:
182
+ return 'Changed files need the configured fallback path instead of selected-test confidence.';
183
+ }
184
+ }
185
+ function selectorFallbackRecommendedIntent(reason) {
186
+ switch (reason) {
187
+ case 'fallback_lockfile':
188
+ case 'fallback_package_metadata':
189
+ case 'fallback_template':
190
+ case 'fallback_generated_contract':
191
+ return 'test_release';
192
+ case 'fallback_migration_or_database':
193
+ case 'fallback_unknown':
194
+ return 'test';
195
+ default:
196
+ return 'test_related';
197
+ }
198
+ }
@@ -1,6 +1,7 @@
1
1
  import { isRecord, readStringArray } from './config-loading.js';
2
2
  import { CHANGE_CLASSIFICATION_SURFACE_AUTHORITY, createPathTarget, } from './surface-decision-model.js';
3
3
  import { classifyVerificationCandidate, createVerificationPlan, } from './verification-plan.js';
4
+ import { evaluateCommandPreconditions } from './command-preconditions.js';
4
5
  import { createVerificationDecisionGraph, } from './verification-decision-graph.js';
5
6
  import { createVerificationSchedule, } from './verification-scheduler.js';
6
7
  import { createProjectTestSelectionPlan, } from './test-selection.js';
@@ -83,6 +84,22 @@ function readIntentCostExpectedSeconds(commandContract, intent) {
83
84
  const expectedSeconds = rawIntent.cost.expected_seconds;
84
85
  return Number.isInteger(expectedSeconds) && Number(expectedSeconds) >= 0 ? Number(expectedSeconds) : null;
85
86
  }
87
+ function intentHasPreconditions(commandContract, intent) {
88
+ const rawIntent = commandContract.intents[intent];
89
+ return isRecord(rawIntent) && Array.isArray(rawIntent.preconditions) && rawIntent.preconditions.length > 0;
90
+ }
91
+ function intentPreconditionsSatisfied(commandContract, projectRoot, intent, cache) {
92
+ if (!intentHasPreconditions(commandContract, intent)) {
93
+ return true;
94
+ }
95
+ const cached = cache.get(intent);
96
+ if (cached !== undefined) {
97
+ return cached;
98
+ }
99
+ const satisfied = evaluateCommandPreconditions(projectRoot, commandContract, intent).every((precondition) => precondition.status === 'satisfied');
100
+ cache.set(intent, satisfied);
101
+ return satisfied;
102
+ }
86
103
  function intentCoverageSignature(commandContract, intent) {
87
104
  const rawIntent = commandContract.intents[intent];
88
105
  if (!isRecord(rawIntent) || !isRecord(rawIntent.covers)) {
@@ -202,9 +219,10 @@ function minNumber(values) {
202
219
  }
203
220
  return minimum;
204
221
  }
205
- function selectVerificationCandidates(commandContract, candidates) {
222
+ function selectVerificationCandidates(commandContract, projectRoot, candidates) {
206
223
  const runnableCandidates = candidates.filter((candidate) => candidate.status === 'runnable' && candidate.intent.length > 0);
207
224
  const selectedIntents = new Set(runnableCandidates.map((candidate) => candidate.intent));
225
+ const preconditionCache = new Map();
208
226
  for (const candidate of runnableCandidates) {
209
227
  const isSubsumed = runnableCandidates.some((other) => {
210
228
  if (other.intent === candidate.intent) {
@@ -233,7 +251,16 @@ function selectVerificationCandidates(commandContract, candidates) {
233
251
  if (group.length < 2) {
234
252
  continue;
235
253
  }
236
- const costs = group.map((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent));
254
+ const preconditionSatisfiedGroup = group.filter((candidate) => intentPreconditionsSatisfied(commandContract, projectRoot, candidate.intent, preconditionCache));
255
+ const selectableGroup = preconditionSatisfiedGroup.length > 0 ? preconditionSatisfiedGroup : group;
256
+ if (preconditionSatisfiedGroup.length > 0 && preconditionSatisfiedGroup.length < group.length) {
257
+ for (const candidate of group) {
258
+ if (!preconditionSatisfiedGroup.some((entry) => entry.intent === candidate.intent)) {
259
+ selectedIntents.delete(candidate.intent);
260
+ }
261
+ }
262
+ }
263
+ const costs = selectableGroup.map((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent));
237
264
  if (costs.some((cost) => cost === null)) {
238
265
  continue;
239
266
  }
@@ -241,11 +268,11 @@ function selectVerificationCandidates(commandContract, candidates) {
241
268
  if (minCost === null) {
242
269
  continue;
243
270
  }
244
- const winners = group.filter((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent) === minCost);
271
+ const winners = selectableGroup.filter((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent) === minCost);
245
272
  if (winners.length !== 1) {
246
273
  continue;
247
274
  }
248
- for (const candidate of group) {
275
+ for (const candidate of selectableGroup) {
249
276
  if (candidate.intent !== winners[0]?.intent) {
250
277
  selectedIntents.delete(candidate.intent);
251
278
  }
@@ -316,7 +343,7 @@ export function createChangeVerificationReport(classificationReport, commandCont
316
343
  const selectedPlans = plansWithProjectTestSelection.map((plan) => ({
317
344
  ...plan,
318
345
  selectedCandidates: uniqueVerificationCandidates([
319
- ...selectVerificationCandidates(commandContract, plan.candidates),
346
+ ...selectVerificationCandidates(commandContract, projectRoot, plan.candidates),
320
347
  ...testSelectionPlan.selectedCandidates
321
348
  .filter((candidate) => candidate.reason === plan.requirement.reason)
322
349
  .map((candidate) => candidate.candidate),