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,335 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { ensureInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
6
+ export const REPO_MERGE_CONFLICT_SCAN_PACK_ID = 'repo';
7
+ export const REPO_MERGE_CONFLICT_SCAN_SCRIPT_ID = 'merge-conflict-scan';
8
+ export const REPO_MERGE_CONFLICT_SCAN_SCRIPT_REF = `${REPO_MERGE_CONFLICT_SCAN_PACK_ID}/${REPO_MERGE_CONFLICT_SCAN_SCRIPT_ID}`;
9
+ const DEFAULT_MAX_FILES = 1000;
10
+ const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
11
+ const MARKER_PATTERNS = [
12
+ { prefix: '<<<<<<<', marker: 'start' },
13
+ { prefix: '|||||||', marker: 'base' },
14
+ { prefix: '=======', marker: 'separator' },
15
+ { prefix: '>>>>>>>', marker: 'end' },
16
+ ];
17
+ const SKIPPED_DIRECTORY_NAMES = new Set([
18
+ '.git',
19
+ 'node_modules',
20
+ 'vendor',
21
+ 'third_party',
22
+ 'dist',
23
+ 'build',
24
+ 'coverage',
25
+ '.mustflow/cache',
26
+ '.mustflow/state',
27
+ ]);
28
+ function normalizeRelativePath(value) {
29
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
30
+ }
31
+ function sha256(value) {
32
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
33
+ }
34
+ function positiveInteger(value, fallback) {
35
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
36
+ }
37
+ function runGit(root, args) {
38
+ const result = spawnSync('git', [...args], {
39
+ cwd: root,
40
+ encoding: 'utf8',
41
+ stdio: ['ignore', 'pipe', 'pipe'],
42
+ windowsHide: true,
43
+ maxBuffer: 16 * 1024 * 1024,
44
+ });
45
+ return {
46
+ ok: result.status === 0,
47
+ stdout: result.stdout ?? '',
48
+ stderr: result.stderr ?? '',
49
+ };
50
+ }
51
+ function isInsideGitWorktree(root) {
52
+ const result = runGit(root, ['rev-parse', '--is-inside-work-tree']);
53
+ return result.ok && result.stdout.trim() === 'true';
54
+ }
55
+ function parseGitPaths(stdout) {
56
+ const paths = [];
57
+ const seen = new Set();
58
+ for (const line of stdout.split(/\r?\n/u)) {
59
+ const relativePath = normalizeRelativePath(line.trim());
60
+ if (!relativePath || relativePath === '.' || seen.has(relativePath)) {
61
+ continue;
62
+ }
63
+ seen.add(relativePath);
64
+ paths.push(relativePath);
65
+ }
66
+ return paths.sort((left, right) => left.localeCompare(right));
67
+ }
68
+ function collectGitChangedFiles(root, issues, findings) {
69
+ if (!isInsideGitWorktree(root)) {
70
+ const message = 'Git worktree is unavailable; provide explicit paths to scan.';
71
+ issues.push(message);
72
+ findings.push(makeFinding('merge_conflict_scan_git_unavailable', 'low', '.', message, null, null));
73
+ return [];
74
+ }
75
+ const tracked = runGit(root, ['diff', '--name-only', '--diff-filter=ACMRTUXB', 'HEAD', '--']);
76
+ const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard', '--']);
77
+ const paths = new Set();
78
+ if (tracked.ok) {
79
+ for (const entry of parseGitPaths(tracked.stdout)) {
80
+ paths.add(entry);
81
+ }
82
+ }
83
+ else {
84
+ issues.push(`Could not inspect changed tracked files: ${tracked.stderr.trim() || 'git diff failed'}`);
85
+ }
86
+ if (untracked.ok) {
87
+ for (const entry of parseGitPaths(untracked.stdout)) {
88
+ paths.add(entry);
89
+ }
90
+ }
91
+ else {
92
+ issues.push(`Could not inspect untracked files: ${untracked.stderr.trim() || 'git ls-files failed'}`);
93
+ }
94
+ return [...paths].sort((left, right) => left.localeCompare(right));
95
+ }
96
+ function shouldSkipDirectory(relativePath) {
97
+ const normalized = normalizeRelativePath(relativePath);
98
+ const firstSegment = normalized.split('/')[0] ?? normalized;
99
+ return SKIPPED_DIRECTORY_NAMES.has(normalized) || SKIPPED_DIRECTORY_NAMES.has(firstSegment);
100
+ }
101
+ function isLikelyTextFile(relativePath) {
102
+ const extension = path.posix.extname(relativePath).toLowerCase();
103
+ if (!extension) {
104
+ return true;
105
+ }
106
+ return ![
107
+ '.png',
108
+ '.jpg',
109
+ '.jpeg',
110
+ '.gif',
111
+ '.webp',
112
+ '.ico',
113
+ '.pdf',
114
+ '.zip',
115
+ '.gz',
116
+ '.tgz',
117
+ '.wasm',
118
+ '.sqlite',
119
+ '.db',
120
+ '.lockb',
121
+ ].includes(extension);
122
+ }
123
+ function listFilesUnder(root, relativePath, findings, issues) {
124
+ const absolutePath = path.join(root, ...relativePath.split('/'));
125
+ let entries;
126
+ try {
127
+ ensureInsideWithoutSymlinks(root, absolutePath);
128
+ entries = readdirSync(absolutePath, { withFileTypes: true });
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ issues.push(`${relativePath}: ${message}`);
133
+ findings.push(makeFinding('merge_conflict_scan_unreadable_path', 'high', relativePath, message, null, null));
134
+ return [];
135
+ }
136
+ const files = [];
137
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
138
+ const child = normalizeRelativePath(relativePath === '.' ? entry.name : `${relativePath}/${entry.name}`);
139
+ if (entry.isDirectory()) {
140
+ if (!shouldSkipDirectory(child)) {
141
+ files.push(...listFilesUnder(root, child, findings, issues));
142
+ }
143
+ continue;
144
+ }
145
+ if (entry.isFile() && isLikelyTextFile(child)) {
146
+ files.push(child);
147
+ }
148
+ }
149
+ return files;
150
+ }
151
+ function resolveInputFiles(root, inputPaths, policy, findings, issues) {
152
+ const files = [];
153
+ const seen = new Set();
154
+ for (const inputPath of inputPaths) {
155
+ const absolutePath = path.resolve(root, inputPath);
156
+ let relativePath;
157
+ try {
158
+ ensureInsideWithoutSymlinks(root, absolutePath);
159
+ relativePath = normalizeRelativePath(path.relative(root, absolutePath));
160
+ }
161
+ catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ issues.push(`${inputPath}: ${message}`);
164
+ findings.push(makeFinding('merge_conflict_scan_path_outside_root', 'high', inputPath, message, null, null));
165
+ continue;
166
+ }
167
+ if (!existsSync(absolutePath)) {
168
+ continue;
169
+ }
170
+ let stats;
171
+ try {
172
+ stats = lstatSync(absolutePath);
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ issues.push(`${relativePath}: ${message}`);
177
+ findings.push(makeFinding('merge_conflict_scan_unreadable_path', 'high', relativePath, message, null, null));
178
+ continue;
179
+ }
180
+ const candidates = stats.isDirectory()
181
+ ? listFilesUnder(root, relativePath, findings, issues)
182
+ : stats.isFile() && isLikelyTextFile(relativePath)
183
+ ? [relativePath]
184
+ : [];
185
+ for (const candidate of candidates) {
186
+ if (seen.has(candidate)) {
187
+ continue;
188
+ }
189
+ if (files.length >= policy.max_files) {
190
+ const message = `Merge-conflict scan reached max_files ${policy.max_files}; remaining files were skipped.`;
191
+ issues.push(message);
192
+ findings.push(makeFinding('merge_conflict_scan_max_files_exceeded', 'high', candidate, message, null, null));
193
+ return files;
194
+ }
195
+ seen.add(candidate);
196
+ files.push(candidate);
197
+ }
198
+ }
199
+ return files.sort((left, right) => left.localeCompare(right));
200
+ }
201
+ function markerForLine(line) {
202
+ const trimmedStart = line.trimStart();
203
+ for (const pattern of MARKER_PATTERNS) {
204
+ if (trimmedStart.startsWith(pattern.prefix)) {
205
+ return { marker: pattern.marker, markerLength: pattern.prefix.length };
206
+ }
207
+ }
208
+ return null;
209
+ }
210
+ function makeFinding(code, severity, pathValue, message, line, marker) {
211
+ return {
212
+ code,
213
+ severity,
214
+ path: pathValue,
215
+ line,
216
+ marker,
217
+ message,
218
+ json_pointer: null,
219
+ metric: null,
220
+ actual: null,
221
+ expected: null,
222
+ };
223
+ }
224
+ function scanFile(root, relativePath, policy, findings, issues) {
225
+ let content;
226
+ try {
227
+ content = readUtf8FileInsideWithoutSymlinks(root, path.join(root, ...relativePath.split('/')), {
228
+ maxBytes: policy.max_file_bytes,
229
+ });
230
+ }
231
+ catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error);
233
+ const code = message.includes('exceeds maximum size')
234
+ ? 'merge_conflict_scan_file_too_large'
235
+ : 'merge_conflict_scan_unreadable_path';
236
+ const severity = code === 'merge_conflict_scan_file_too_large' ? 'medium' : 'high';
237
+ issues.push(`${relativePath}: ${message}`);
238
+ findings.push(makeFinding(code, severity, relativePath, message, null, null));
239
+ return { file: null, markers: [] };
240
+ }
241
+ const fileMarkers = [];
242
+ const lines = content.split(/\r\n|\n|\r/u);
243
+ for (const [index, line] of lines.entries()) {
244
+ const marker = markerForLine(line);
245
+ if (!marker) {
246
+ continue;
247
+ }
248
+ const column = line.length - line.trimStart().length + 1;
249
+ const lineNumber = index + 1;
250
+ fileMarkers.push({
251
+ path: relativePath,
252
+ line: lineNumber,
253
+ column,
254
+ marker: marker.marker,
255
+ marker_length: marker.markerLength,
256
+ });
257
+ findings.push(makeFinding('merge_conflict_marker_detected', 'high', relativePath, `Merge conflict marker "${marker.marker}" detected at ${relativePath}:${lineNumber}.`, lineNumber, marker.marker));
258
+ }
259
+ return {
260
+ file: {
261
+ path: relativePath,
262
+ sha256: sha256(content),
263
+ bytes: Buffer.byteLength(content, 'utf8'),
264
+ markers: fileMarkers.length,
265
+ },
266
+ markers: fileMarkers,
267
+ };
268
+ }
269
+ function summarize(files, markers, issues) {
270
+ return {
271
+ files_checked: files.length,
272
+ markers_found: markers.length,
273
+ files_with_markers: files.filter((file) => file.markers > 0).length,
274
+ issues: issues.length,
275
+ };
276
+ }
277
+ function createInputHash(reportInput) {
278
+ return sha256(JSON.stringify(reportInput));
279
+ }
280
+ export function checkRepoMergeConflictScan(projectRoot, options) {
281
+ const root = path.resolve(projectRoot);
282
+ const inputPaths = options.paths.length > 0 ? [...options.paths] : [];
283
+ const findings = [];
284
+ const issues = [];
285
+ const policy = {
286
+ input_mode: inputPaths.length > 0 ? 'explicit_paths' : 'git_changed_files',
287
+ marker_prefixes: MARKER_PATTERNS.map((pattern) => pattern.prefix),
288
+ max_files: positiveInteger(options.maxFiles, DEFAULT_MAX_FILES),
289
+ max_file_bytes: positiveInteger(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES),
290
+ skipped_directories: [...SKIPPED_DIRECTORY_NAMES].sort((left, right) => left.localeCompare(right)),
291
+ };
292
+ const effectivePaths = inputPaths.length > 0 ? inputPaths : collectGitChangedFiles(root, issues, findings);
293
+ const scanTargets = resolveInputFiles(root, effectivePaths, policy, findings, issues);
294
+ const files = [];
295
+ const markers = [];
296
+ for (const target of scanTargets) {
297
+ const result = scanFile(root, target, policy, findings, issues);
298
+ if (result.file) {
299
+ files.push(result.file);
300
+ }
301
+ markers.push(...result.markers);
302
+ }
303
+ if (effectivePaths.length === 0 && issues.length === 0) {
304
+ const message = 'No input files were available for merge-conflict scanning.';
305
+ findings.push(makeFinding('merge_conflict_scan_no_input_files', 'low', '.', message, null, null));
306
+ }
307
+ const summary = summarize(files, markers, issues);
308
+ const hasHighFinding = findings.some((finding) => finding.severity === 'high' || finding.severity === 'critical');
309
+ const status = findings.some((finding) => finding.code === 'merge_conflict_scan_path_outside_root')
310
+ ? 'error'
311
+ : hasHighFinding
312
+ ? 'failed'
313
+ : 'passed';
314
+ return {
315
+ schema_version: '1',
316
+ command: 'script-pack',
317
+ pack_id: REPO_MERGE_CONFLICT_SCAN_PACK_ID,
318
+ script_id: REPO_MERGE_CONFLICT_SCAN_SCRIPT_ID,
319
+ script_ref: REPO_MERGE_CONFLICT_SCAN_SCRIPT_REF,
320
+ action: 'check',
321
+ status,
322
+ ok: status === 'passed',
323
+ mustflow_root: root,
324
+ input: {
325
+ paths: inputPaths,
326
+ },
327
+ policy,
328
+ input_hash: createInputHash({ inputPaths, policy, files, markers, findings, issues }),
329
+ summary,
330
+ files,
331
+ markers,
332
+ findings,
333
+ issues,
334
+ };
335
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isRecord, readMustflowOwnedTomlFile } from './config-loading.js';
5
+ import { detectVersionSources, releaseVersioningIsEnabled, VERSIONING_CONFIG_PATH, } from './version-sources.js';
6
+ export const REPO_VERSION_SOURCE_PACK_ID = 'repo';
7
+ export const REPO_VERSION_SOURCE_SCRIPT_ID = 'version-source';
8
+ export const REPO_VERSION_SOURCE_SCRIPT_REF = `${REPO_VERSION_SOURCE_PACK_ID}/${REPO_VERSION_SOURCE_SCRIPT_ID}`;
9
+ export const REPO_VERSION_SOURCE_PREFERENCES_PATH = '.mustflow/config/preferences.toml';
10
+ function sha256(value) {
11
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
12
+ }
13
+ function readPreferences(projectRoot, issues) {
14
+ const preferencesPath = path.join(projectRoot, ...REPO_VERSION_SOURCE_PREFERENCES_PATH.split('/'));
15
+ if (!existsSync(preferencesPath)) {
16
+ return undefined;
17
+ }
18
+ try {
19
+ const parsed = readMustflowOwnedTomlFile(projectRoot, REPO_VERSION_SOURCE_PREFERENCES_PATH);
20
+ return isRecord(parsed) ? parsed : undefined;
21
+ }
22
+ catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ issues.push(`Could not read ${REPO_VERSION_SOURCE_PREFERENCES_PATH}: ${message}`);
25
+ return undefined;
26
+ }
27
+ }
28
+ function countSources(sources) {
29
+ return {
30
+ sources: sources.length,
31
+ declared_sources: sources.filter((source) => source.declared === true).length,
32
+ source_authority_sources: sources.filter((source) => source.authority === 'source').length,
33
+ derived_authority_sources: sources.filter((source) => source.authority === 'derived').length,
34
+ unclassified_authority_sources: sources.filter((source) => source.authority === undefined).length,
35
+ };
36
+ }
37
+ function createInputHash(reportInput) {
38
+ return sha256(JSON.stringify(reportInput));
39
+ }
40
+ export function inspectRepoVersionSource(projectRoot) {
41
+ const root = path.resolve(projectRoot);
42
+ const issues = [];
43
+ const preferences = readPreferences(root, issues);
44
+ const versioningEnabled = releaseVersioningIsEnabled(preferences);
45
+ const sources = detectVersionSources(root);
46
+ const findings = [];
47
+ if (versioningEnabled && sources.length === 0) {
48
+ findings.push({
49
+ code: 'versioning_enabled_without_sources',
50
+ severity: 'high',
51
+ path: REPO_VERSION_SOURCE_PREFERENCES_PATH,
52
+ message: 'Release versioning preferences are enabled, but no version source was detected.',
53
+ json_pointer: null,
54
+ metric: null,
55
+ actual: 0,
56
+ expected: 1,
57
+ });
58
+ }
59
+ const counts = countSources(sources);
60
+ const status = issues.length > 0 ? 'error' : findings.length > 0 ? 'failed' : 'passed';
61
+ return {
62
+ schema_version: '1',
63
+ command: 'script-pack',
64
+ pack_id: REPO_VERSION_SOURCE_PACK_ID,
65
+ script_id: REPO_VERSION_SOURCE_SCRIPT_ID,
66
+ script_ref: REPO_VERSION_SOURCE_SCRIPT_REF,
67
+ action: 'inspect',
68
+ status,
69
+ ok: status === 'passed',
70
+ mustflow_root: root,
71
+ input: {
72
+ preferences_path: REPO_VERSION_SOURCE_PREFERENCES_PATH,
73
+ declared_sources_path: VERSIONING_CONFIG_PATH,
74
+ },
75
+ input_hash: createInputHash({ versioningEnabled, counts, sources, findings, issues }),
76
+ versioning_enabled: versioningEnabled,
77
+ counts,
78
+ sources,
79
+ findings,
80
+ issues,
81
+ };
82
+ }
@@ -3,13 +3,14 @@ import path from 'node:path';
3
3
  const CODE_NAVIGATION_SCRIPT_REFS = new Set([
4
4
  'code/outline',
5
5
  'code/dependency-graph',
6
+ 'code/import-cycle',
6
7
  'code/symbol-read',
7
8
  'code/route-outline',
8
9
  'code/export-diff',
9
10
  'repo/related-files',
10
11
  ]);
11
12
  const CONFIG_CHAIN_SURFACES = new Set(['config', 'package', 'source', 'test']);
12
- const CONFIG_FILE_PATTERN = /(?:^|\/)(?:\.env\.(?:example|sample|template|defaults)|\.dev\.vars\.example|tsconfig(?:\..*)?\.json|eslint\.config\.[cm]?[jt]s|\.eslintrc(?:\.json)?|\.prettierrc(?:\.json)?|prettier\.config\.[cm]?[jt]s|vite\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|tailwind\.config\.[cm]?[jt]s|jest\.config\.[cm]?[jt]s|playwright\.config\.[cm]?[jt]s|astro\.config\.mjs|svelte\.config\.js)$/u;
13
+ const CONFIG_FILE_PATTERN = /(?:^|\/)(?:\.gitignore|\.env\.(?:example|sample|template|defaults)|\.dev\.vars\.example|tsconfig(?:\..*)?\.json|eslint\.config\.[cm]?[jt]s|\.eslintrc(?:\.json)?|\.prettierrc(?:\.json)?|prettier\.config\.[cm]?[jt]s|vite\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|tailwind\.config\.[cm]?[jt]s|jest\.config\.[cm]?[jt]s|playwright\.config\.[cm]?[jt]s|astro\.config\.mjs|svelte\.config\.js)$/u;
13
14
  export function isScriptPackSuggestionPhase(value) {
14
15
  return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
15
16
  }
@@ -122,7 +123,13 @@ function surfacesForScript(script) {
122
123
  addIf('generated', /generated|protected|vendor|cache|boundary/u);
123
124
  addIf('config', /config|command/u);
124
125
  addIf('package', /package|release/u);
126
+ addIf('test', /test|suite|fixture|coverage|selection|timing|performance/u);
125
127
  addIf('source', /code|source|symbol/u);
128
+ if (script.ref === 'repo/manifest-lock-drift') {
129
+ surfaces.add('config');
130
+ surfaces.add('generated');
131
+ surfaces.add('template');
132
+ }
126
133
  return uniqueSortedSurfaces(surfaces);
127
134
  }
128
135
  function confidenceForScore(score) {
@@ -140,6 +147,10 @@ function pathsWithSurface(analyzedPaths, surface) {
140
147
  function hasPathWithSurface(analyzedPaths, surface) {
141
148
  return analyzedPaths.some((entry) => entry.surfaces.includes(surface));
142
149
  }
150
+ function isGitIgnoreAuditPriorityPath(relativePath) {
151
+ const normalized = relativePath.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
152
+ return normalized === '.gitignore' || normalized.endsWith('/.gitignore') || normalized === '.git/info/exclude';
153
+ }
143
154
  function firstAvailablePath(analyzedPaths, preferredSurfaces) {
144
155
  for (const surface of preferredSurfaces) {
145
156
  const [candidate] = pathsWithSurface(analyzedPaths, surface);
@@ -171,6 +182,10 @@ function createRunHint(script, analyzedPaths) {
171
182
  const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
172
183
  return createConcretePathHint('mf script-pack run code/dependency-graph scan', sourcePaths, script.usage);
173
184
  }
185
+ if (script.ref === 'code/import-cycle') {
186
+ const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
187
+ return createConcretePathHint('mf script-pack run code/import-cycle check', sourcePaths, script.usage);
188
+ }
174
189
  if (script.ref === 'code/change-impact') {
175
190
  return 'mf script-pack run code/change-impact analyze --base HEAD --json';
176
191
  }
@@ -199,9 +214,46 @@ function createRunHint(script, analyzedPaths) {
199
214
  .map((entry) => entry.path);
200
215
  return createConcretePathHint('mf script-pack run docs/reference-drift check', docsPaths, script.usage);
201
216
  }
217
+ if (script.ref === 'docs/link-integrity') {
218
+ const docsPaths = analyzedPaths
219
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'docs' || surface === 'schema'))
220
+ .map((entry) => entry.path);
221
+ return createConcretePathHint('mf script-pack run docs/link-integrity check', docsPaths, script.usage);
222
+ }
223
+ if (script.ref === 'test/performance-report') {
224
+ return 'mf script-pack run test/performance-report summarize --json';
225
+ }
226
+ if (script.ref === 'test/regression-selector') {
227
+ const testSelectionPaths = analyzedPaths
228
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'source' || surface === 'test'))
229
+ .map((entry) => entry.path);
230
+ const pathPart = testSelectionPaths.length > 0 ? ` ${testSelectionPaths.map(quoteCliArg).join(' ')}` : '';
231
+ return `mf script-pack run test/regression-selector select${pathPart} --base HEAD --json`;
232
+ }
202
233
  if (script.ref === 'repo/generated-boundary') {
203
234
  return createConcretePathHint('mf script-pack run repo/generated-boundary check', analyzedPaths.map((entry) => entry.path), script.usage);
204
235
  }
236
+ if (script.ref === 'repo/merge-conflict-scan') {
237
+ return createConcretePathHint('mf script-pack run repo/merge-conflict-scan check', analyzedPaths.map((entry) => entry.path), 'mf script-pack run repo/merge-conflict-scan check --json');
238
+ }
239
+ if (script.ref === 'repo/git-ignore-audit') {
240
+ return createConcretePathHint('mf script-pack run repo/git-ignore-audit audit', analyzedPaths.map((entry) => entry.path), 'mf script-pack run repo/git-ignore-audit audit --json');
241
+ }
242
+ if (script.ref === 'repo/manifest-lock-drift') {
243
+ const manifestPaths = analyzedPaths
244
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'config' || surface === 'template' || surface === 'generated'))
245
+ .map((entry) => entry.path);
246
+ return createConcretePathHint('mf script-pack run repo/manifest-lock-drift check', manifestPaths, 'mf script-pack run repo/manifest-lock-drift check --json');
247
+ }
248
+ if (script.ref === 'repo/skill-route-audit') {
249
+ return 'mf script-pack run repo/skill-route-audit audit --json';
250
+ }
251
+ if (script.ref === 'repo/version-source') {
252
+ return 'mf script-pack run repo/version-source inspect --json';
253
+ }
254
+ if (script.ref === 'repo/approval-gate') {
255
+ return 'mf script-pack run repo/approval-gate check --action <action_type> --json';
256
+ }
205
257
  if (script.ref === 'repo/config-chain') {
206
258
  const configPaths = analyzedPaths
207
259
  .filter((entry) => entry.surfaces.some((surface) => CONFIG_CHAIN_SURFACES.has(surface)))
@@ -280,6 +332,30 @@ export function createScriptPackSuggestionReport(mustflowRoot, options) {
280
332
  score += 2;
281
333
  reasons.push('Prioritizes generated-boundary checks for generated paths.');
282
334
  }
335
+ if (script.ref === 'repo/merge-conflict-scan' && requestedSurfaces.size > 0) {
336
+ score += 1;
337
+ reasons.push('Prioritizes merge-conflict marker scans for changed repository paths.');
338
+ }
339
+ if (script.ref === 'repo/git-ignore-audit' &&
340
+ analyzedPaths.some((entry) => isGitIgnoreAuditPriorityPath(entry.path))) {
341
+ score += 1;
342
+ reasons.push('Prioritizes Git ignore evidence for explicit ignore-control paths.');
343
+ }
344
+ if (script.ref === 'repo/skill-route-audit' &&
345
+ (requestedSurfaces.has('skill') || requestedSurfaces.has('template') || requestedSurfaces.has('config'))) {
346
+ score += 3;
347
+ reasons.push('Prioritizes skill-route audits for skill, template, and workflow metadata surfaces.');
348
+ }
349
+ if (script.ref === 'repo/version-source' &&
350
+ (requestedSurfaces.has('package') || requestedSurfaces.has('template') || requestedSurfaces.has('config'))) {
351
+ score += 2;
352
+ reasons.push('Prioritizes version-source inspection for package, template, and versioning metadata surfaces.');
353
+ }
354
+ if (script.ref === 'repo/approval-gate' &&
355
+ (requestedSurfaces.has('config') || requestedSurfaces.has('package') || requestedSurfaces.has('template'))) {
356
+ score += 2;
357
+ reasons.push('Prioritizes approval-gate checks for approval-sensitive workflow and release surfaces.');
358
+ }
283
359
  if (score === 0) {
284
360
  return null;
285
361
  }