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.
- package/dist/cli/commands/script-pack.js +10 -0
- package/dist/cli/i18n/en.js +183 -0
- package/dist/cli/i18n/es.js +183 -0
- package/dist/cli/i18n/fr.js +183 -0
- package/dist/cli/i18n/hi.js +183 -0
- package/dist/cli/i18n/ko.js +183 -0
- package/dist/cli/i18n/zh.js +183 -0
- package/dist/cli/lib/script-pack-registry.js +284 -1
- package/dist/cli/script-packs/code-change-impact.js +6 -0
- package/dist/cli/script-packs/code-import-cycle.js +193 -0
- package/dist/cli/script-packs/docs-link-integrity.js +145 -0
- package/dist/cli/script-packs/repo-approval-gate.js +100 -0
- package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
- package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
- package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
- package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
- package/dist/cli/script-packs/repo-version-source.js +92 -0
- package/dist/cli/script-packs/test-performance-report.js +247 -0
- package/dist/cli/script-packs/test-regression-selector.js +167 -0
- package/dist/core/change-impact.js +23 -51
- package/dist/core/change-surface-classification.js +198 -0
- package/dist/core/docs-link-integrity.js +443 -0
- package/dist/core/import-cycle.js +152 -0
- package/dist/core/public-json-contracts.js +116 -0
- package/dist/core/repo-approval-gate.js +116 -0
- package/dist/core/repo-git-ignore-audit.js +302 -0
- package/dist/core/repo-manifest-lock-drift.js +321 -0
- package/dist/core/repo-merge-conflict-scan.js +335 -0
- package/dist/core/repo-version-source.js +82 -0
- package/dist/core/script-pack-suggestions.js +77 -1
- package/dist/core/skill-route-audit.js +354 -0
- package/dist/core/test-performance-report.js +697 -0
- package/dist/core/test-regression-selector.js +335 -0
- package/package.json +1 -1
- package/schemas/README.md +40 -2
- package/schemas/change-impact-report.schema.json +35 -1
- package/schemas/import-cycle-report.schema.json +157 -0
- package/schemas/link-integrity-report.schema.json +176 -0
- package/schemas/repo-approval-gate-report.schema.json +115 -0
- package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
- package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
- package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
- package/schemas/repo-version-source-report.schema.json +127 -0
- package/schemas/skill-route-audit-report.schema.json +144 -0
- package/schemas/test-performance-report.schema.json +319 -0
- package/schemas/test-regression-selector-report.schema.json +187 -0
- package/templates/default/i18n.toml +66 -18
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +45 -8
- package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
- package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
- package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
- package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
- package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
- package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
- package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
- package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
- package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
- package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
- package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
- package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
- package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
- package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
- package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
- package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
- package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
- package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
- package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
- package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +9 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +14 -9
- package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
- package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
- 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
|
}
|