mustflow 2.75.2 → 2.85.4
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/README.md +40 -3
- package/dist/cli/commands/docs.js +86 -2
- package/dist/cli/commands/script-pack.js +9 -0
- package/dist/cli/i18n/en.js +180 -2
- package/dist/cli/i18n/es.js +180 -2
- package/dist/cli/i18n/fr.js +180 -2
- package/dist/cli/i18n/hi.js +180 -2
- package/dist/cli/i18n/ko.js +180 -2
- package/dist/cli/i18n/zh.js +180 -2
- package/dist/cli/lib/repo-map.js +27 -6
- package/dist/cli/lib/run-root-trust.js +15 -1
- package/dist/cli/lib/script-pack-registry.js +275 -6
- package/dist/cli/lib/validation/index.js +2 -2
- package/dist/cli/lib/validation/primitives.js +4 -1
- package/dist/cli/script-packs/code-change-impact.js +172 -0
- package/dist/cli/script-packs/code-dependency-graph.js +181 -0
- package/dist/cli/script-packs/code-export-diff.js +160 -0
- package/dist/cli/script-packs/code-outline.js +33 -5
- package/dist/cli/script-packs/code-route-outline.js +155 -0
- package/dist/cli/script-packs/docs-reference-drift.js +150 -0
- package/dist/cli/script-packs/repo-config-chain.js +163 -0
- package/dist/cli/script-packs/repo-env-contract.js +156 -0
- package/dist/cli/script-packs/repo-related-files.js +161 -0
- package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
- package/dist/core/change-impact.js +383 -0
- package/dist/core/change-verification.js +32 -5
- package/dist/core/code-outline.js +460 -79
- package/dist/core/config-chain.js +595 -0
- package/dist/core/config-loading.js +121 -4
- package/dist/core/dependency-graph.js +490 -0
- package/dist/core/env-contract.js +450 -0
- package/dist/core/export-diff.js +359 -0
- package/dist/core/line-endings.js +26 -13
- package/dist/core/public-json-contracts.js +126 -0
- package/dist/core/reference-drift.js +388 -0
- package/dist/core/related-files.js +493 -0
- package/dist/core/route-outline.js +964 -0
- package/dist/core/script-pack-suggestions.js +131 -5
- package/dist/core/secret-risk-scan.js +440 -0
- package/dist/core/source-anchors.js +13 -1
- package/package.json +1 -1
- package/schemas/README.md +44 -6
- package/schemas/change-impact-report.schema.json +150 -0
- package/schemas/code-outline-report.schema.json +1 -1
- package/schemas/code-symbol-read-report.schema.json +64 -4
- package/schemas/commands.schema.json +12 -0
- package/schemas/config-chain-report.schema.json +187 -0
- package/schemas/dependency-graph-report.schema.json +149 -0
- package/schemas/env-contract-report.schema.json +203 -0
- package/schemas/export-diff-report.schema.json +220 -0
- package/schemas/reference-drift-report.schema.json +166 -0
- package/schemas/related-files-report.schema.json +145 -0
- package/schemas/route-outline-report.schema.json +200 -0
- package/schemas/secret-risk-scan-report.schema.json +152 -0
- package/templates/default/common/.mustflow/config/commands.toml +21 -0
- package/templates/default/i18n.toml +21 -9
- package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +1 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +8 -2
- package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
- package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
- package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +146 -0
- package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
- package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
- package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
- package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
- package/templates/default/locales/en/.mustflow/skills/routes.toml +21 -9
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
- package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +314 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +13 -10
- package/templates/default/manifest.toml +15 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { extractSymbols, languageForPath } from './code-outline.js';
|
|
6
|
+
import { ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
7
|
+
export const CODE_EXPORT_DIFF_SCRIPT_ID = 'export-diff';
|
|
8
|
+
export const CODE_EXPORT_DIFF_SCRIPT_REF = `code/${CODE_EXPORT_DIFF_SCRIPT_ID}`;
|
|
9
|
+
const DEFAULT_BASE_REF = 'HEAD';
|
|
10
|
+
const DEFAULT_MAX_FILES = 100;
|
|
11
|
+
const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
12
|
+
const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
13
|
+
const IGNORED_DIRECTORIES = ['.git', 'node_modules', 'dist', 'build', 'coverage', '.mustflow/cache', '.mustflow/state'];
|
|
14
|
+
const ERROR_CODES = new Set([
|
|
15
|
+
'export_diff_git_unavailable',
|
|
16
|
+
'export_diff_invalid_ref',
|
|
17
|
+
'export_diff_unreadable_path',
|
|
18
|
+
'export_diff_file_too_large',
|
|
19
|
+
'export_diff_max_files_exceeded',
|
|
20
|
+
]);
|
|
21
|
+
function toPosixPath(value) {
|
|
22
|
+
return value.replace(/\\/gu, '/');
|
|
23
|
+
}
|
|
24
|
+
function normalizeRelativePath(value) {
|
|
25
|
+
return toPosixPath(value).replace(/^\.\/+/u, '') || '.';
|
|
26
|
+
}
|
|
27
|
+
function sha256Tagged(value) {
|
|
28
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
29
|
+
}
|
|
30
|
+
function runGit(root, args) {
|
|
31
|
+
const result = spawnSync('git', [...args], {
|
|
32
|
+
cwd: root,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
ok: result.status === 0,
|
|
40
|
+
stdout: result.stdout ?? '',
|
|
41
|
+
stderr: result.stderr ?? '',
|
|
42
|
+
status: result.status,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function makeFinding(code, severity, pathValue, message) {
|
|
46
|
+
return { code, severity, path: pathValue, message };
|
|
47
|
+
}
|
|
48
|
+
function isIgnoredPath(relativePath) {
|
|
49
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
50
|
+
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
51
|
+
}
|
|
52
|
+
function isSupportedPath(relativePath) {
|
|
53
|
+
return SUPPORTED_EXTENSIONS.includes(path.extname(relativePath).toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
function splitGitPaths(stdout) {
|
|
56
|
+
return stdout
|
|
57
|
+
.split(/\r?\n/u)
|
|
58
|
+
.map((entry) => normalizeRelativePath(entry.trim()))
|
|
59
|
+
.filter((entry) => entry.length > 0 && entry !== '.');
|
|
60
|
+
}
|
|
61
|
+
function collectChangedPaths(root, policy, findings, issues) {
|
|
62
|
+
const diffArgs = policy.head_ref
|
|
63
|
+
? ['diff', '--name-only', '--diff-filter=ACMRTD', policy.base_ref, policy.head_ref, '--', ...policy.path_filters]
|
|
64
|
+
: ['diff', '--name-only', '--diff-filter=ACMRTD', policy.base_ref, '--', ...policy.path_filters];
|
|
65
|
+
const diff = runGit(root, diffArgs);
|
|
66
|
+
if (!diff.ok) {
|
|
67
|
+
const detail = diff.stderr.trim() || diff.stdout.trim() || `git diff exited with ${diff.status}`;
|
|
68
|
+
issues.push(detail);
|
|
69
|
+
findings.push(makeFinding('export_diff_invalid_ref', 'high', '.', detail));
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const changed = new Set(splitGitPaths(diff.stdout));
|
|
73
|
+
if (!policy.head_ref) {
|
|
74
|
+
const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard', '--', ...policy.path_filters]);
|
|
75
|
+
if (untracked.ok) {
|
|
76
|
+
for (const entry of splitGitPaths(untracked.stdout)) {
|
|
77
|
+
changed.add(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const paths = [...changed]
|
|
82
|
+
.filter((entry) => isSupportedPath(entry) && !isIgnoredPath(entry))
|
|
83
|
+
.sort((left, right) => left.localeCompare(right));
|
|
84
|
+
if (paths.length > policy.max_files) {
|
|
85
|
+
const message = `Export diff matched ${paths.length} supported files; max_files is ${policy.max_files}.`;
|
|
86
|
+
issues.push(message);
|
|
87
|
+
findings.push(makeFinding('export_diff_max_files_exceeded', 'high', '.', message));
|
|
88
|
+
}
|
|
89
|
+
return paths.slice(0, policy.max_files);
|
|
90
|
+
}
|
|
91
|
+
function readGitSnapshot(root, ref, relativePath, maxFileBytes) {
|
|
92
|
+
const result = runGit(root, ['show', `${ref}:${relativePath}`]);
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return { exists: false, text: null, sha256: null };
|
|
95
|
+
}
|
|
96
|
+
const bytes = Buffer.byteLength(result.stdout, 'utf8');
|
|
97
|
+
if (bytes > maxFileBytes) {
|
|
98
|
+
throw new Error(`${relativePath} at ${ref} has ${bytes} bytes; max_file_bytes is ${maxFileBytes}.`);
|
|
99
|
+
}
|
|
100
|
+
return { exists: true, text: result.stdout, sha256: sha256Tagged(result.stdout) };
|
|
101
|
+
}
|
|
102
|
+
function readWorktreeSnapshot(root, relativePath, maxFileBytes) {
|
|
103
|
+
const absolutePath = path.join(root, ...relativePath.split('/'));
|
|
104
|
+
if (!existsSync(absolutePath)) {
|
|
105
|
+
return { exists: false, text: null, sha256: null };
|
|
106
|
+
}
|
|
107
|
+
ensureInsideWithoutSymlinks(root, absolutePath);
|
|
108
|
+
const buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: maxFileBytes });
|
|
109
|
+
return { exists: true, text: buffer.toString('utf8'), sha256: sha256Tagged(buffer) };
|
|
110
|
+
}
|
|
111
|
+
function snapshotSymbols(relativePath, snapshot) {
|
|
112
|
+
if (!snapshot.exists || snapshot.text === null || snapshot.sha256 === null) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const language = languageForPath(relativePath);
|
|
116
|
+
if (!language) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return extractSymbols(relativePath, language, snapshot.sha256, snapshot.text)
|
|
120
|
+
.filter((symbol) => symbol.exported)
|
|
121
|
+
.map((symbol) => ({
|
|
122
|
+
path: symbol.path,
|
|
123
|
+
name: symbol.name,
|
|
124
|
+
kind: symbol.kind,
|
|
125
|
+
language: symbol.language,
|
|
126
|
+
signature: symbol.signature,
|
|
127
|
+
return_type: symbol.return_type,
|
|
128
|
+
return_behavior: symbol.return_behavior,
|
|
129
|
+
async: symbol.async,
|
|
130
|
+
line: symbol.start_line,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
function symbolKey(symbol) {
|
|
134
|
+
return `${symbol.path}:${symbol.name}`;
|
|
135
|
+
}
|
|
136
|
+
function buildSymbolMap(symbols, findings, issues) {
|
|
137
|
+
const map = new Map();
|
|
138
|
+
for (const symbol of symbols) {
|
|
139
|
+
const key = symbolKey(symbol);
|
|
140
|
+
if (map.has(key)) {
|
|
141
|
+
const message = `Multiple exported symbols named ${symbol.name} were found in ${symbol.path}; export diff kept the first match.`;
|
|
142
|
+
issues.push(message);
|
|
143
|
+
findings.push(makeFinding('export_diff_duplicate_export', 'medium', symbol.path, message));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
map.set(key, symbol);
|
|
147
|
+
}
|
|
148
|
+
return map;
|
|
149
|
+
}
|
|
150
|
+
function hasUnresolvedReexport(text) {
|
|
151
|
+
if (text === null) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return /^\s*export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"][^'"]+['"]/mu.test(text) || /^\s*export\s+\*\s+from\s+['"][^'"]+['"]/mu.test(text);
|
|
155
|
+
}
|
|
156
|
+
function compareSymbols(before, after) {
|
|
157
|
+
if (before === null && after !== null) {
|
|
158
|
+
return { change: 'added', compatibility: 'additive', kind: after.kind };
|
|
159
|
+
}
|
|
160
|
+
if (before !== null && after === null) {
|
|
161
|
+
return { change: 'removed', compatibility: 'removed_export', kind: before.kind };
|
|
162
|
+
}
|
|
163
|
+
if (before === null || after === null) {
|
|
164
|
+
return { change: 'changed', compatibility: 'unknown', kind: 'function' };
|
|
165
|
+
}
|
|
166
|
+
if (before.kind === after.kind &&
|
|
167
|
+
before.signature === after.signature &&
|
|
168
|
+
before.return_type === after.return_type &&
|
|
169
|
+
before.return_behavior === after.return_behavior &&
|
|
170
|
+
before.async === after.async) {
|
|
171
|
+
return { change: 'unchanged', compatibility: 'unchanged', kind: after.kind };
|
|
172
|
+
}
|
|
173
|
+
if (before.return_type !== after.return_type || before.return_behavior !== after.return_behavior) {
|
|
174
|
+
return { change: 'changed', compatibility: 'return_changed', kind: after.kind };
|
|
175
|
+
}
|
|
176
|
+
return { change: 'changed', compatibility: 'signature_changed', kind: after.kind };
|
|
177
|
+
}
|
|
178
|
+
function packageSurface(root) {
|
|
179
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
180
|
+
if (!existsSync(packageJsonPath)) {
|
|
181
|
+
return { package_json_present: false, type: null, exports: [], bin: [], types: null };
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
185
|
+
const exportsValue = parsed.exports;
|
|
186
|
+
const binValue = parsed.bin;
|
|
187
|
+
return {
|
|
188
|
+
package_json_present: true,
|
|
189
|
+
type: typeof parsed.type === 'string' ? parsed.type : null,
|
|
190
|
+
exports: typeof exportsValue === 'string'
|
|
191
|
+
? ['.']
|
|
192
|
+
: exportsValue && typeof exportsValue === 'object' && !Array.isArray(exportsValue)
|
|
193
|
+
? Object.keys(exportsValue).sort((left, right) => left.localeCompare(right))
|
|
194
|
+
: [],
|
|
195
|
+
bin: typeof binValue === 'string'
|
|
196
|
+
? ['.']
|
|
197
|
+
: binValue && typeof binValue === 'object' && !Array.isArray(binValue)
|
|
198
|
+
? Object.keys(binValue).sort((left, right) => left.localeCompare(right))
|
|
199
|
+
: [],
|
|
200
|
+
types: typeof parsed.types === 'string' ? parsed.types : typeof parsed.typings === 'string' ? parsed.typings : null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return { package_json_present: true, type: null, exports: [], bin: [], types: null };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function createInputHash(policy, files, entries) {
|
|
208
|
+
return sha256Tagged(JSON.stringify({
|
|
209
|
+
policy,
|
|
210
|
+
files: files.map((file) => ({
|
|
211
|
+
path: file.path,
|
|
212
|
+
base_sha256: file.base_sha256,
|
|
213
|
+
head_sha256: file.head_sha256,
|
|
214
|
+
exported_count_before: file.exported_count_before,
|
|
215
|
+
exported_count_after: file.exported_count_after,
|
|
216
|
+
})),
|
|
217
|
+
exports: entries.map((entry) => ({
|
|
218
|
+
id: entry.id,
|
|
219
|
+
change: entry.change,
|
|
220
|
+
compatibility: entry.compatibility,
|
|
221
|
+
before: entry.before?.signature ?? null,
|
|
222
|
+
after: entry.after?.signature ?? null,
|
|
223
|
+
})),
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
function statusFromFindings(findings) {
|
|
227
|
+
return findings.some((finding) => ERROR_CODES.has(finding.code)) ? 'error' : 'passed';
|
|
228
|
+
}
|
|
229
|
+
export function inspectExportDiff(projectRoot, options = {}) {
|
|
230
|
+
const root = path.resolve(projectRoot);
|
|
231
|
+
const policy = {
|
|
232
|
+
base_ref: options.baseRef ?? DEFAULT_BASE_REF,
|
|
233
|
+
head_ref: options.headRef ?? null,
|
|
234
|
+
compare_worktree: !options.headRef,
|
|
235
|
+
max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
|
|
236
|
+
max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
|
|
237
|
+
extensions: [...SUPPORTED_EXTENSIONS],
|
|
238
|
+
ignored_directories: [...IGNORED_DIRECTORIES],
|
|
239
|
+
path_filters: [...(options.paths ?? [])].map(normalizeRelativePath),
|
|
240
|
+
};
|
|
241
|
+
const findings = [];
|
|
242
|
+
const issues = [];
|
|
243
|
+
const gitProbe = runGit(root, ['rev-parse', '--is-inside-work-tree']);
|
|
244
|
+
if (!gitProbe.ok || gitProbe.stdout.trim() !== 'true') {
|
|
245
|
+
const detail = gitProbe.stderr.trim() || gitProbe.stdout.trim() || 'Not inside a git work tree.';
|
|
246
|
+
findings.push(makeFinding('export_diff_git_unavailable', 'high', '.', detail));
|
|
247
|
+
issues.push(detail);
|
|
248
|
+
const status = statusFromFindings(findings);
|
|
249
|
+
return {
|
|
250
|
+
schema_version: '1',
|
|
251
|
+
command: 'script-pack',
|
|
252
|
+
pack_id: 'code',
|
|
253
|
+
script_id: CODE_EXPORT_DIFF_SCRIPT_ID,
|
|
254
|
+
script_ref: CODE_EXPORT_DIFF_SCRIPT_REF,
|
|
255
|
+
action: 'compare',
|
|
256
|
+
status,
|
|
257
|
+
ok: false,
|
|
258
|
+
mustflow_root: root,
|
|
259
|
+
policy,
|
|
260
|
+
input_hash: sha256Tagged(JSON.stringify({ policy, findings })),
|
|
261
|
+
package_surface: packageSurface(root),
|
|
262
|
+
files: [],
|
|
263
|
+
exports: [],
|
|
264
|
+
summary: { files_changed: 0, added: 0, removed: 0, changed: 0, unchanged: 0 },
|
|
265
|
+
findings,
|
|
266
|
+
issues,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const changedPaths = collectChangedPaths(root, policy, findings, issues);
|
|
270
|
+
const files = [];
|
|
271
|
+
const entries = [];
|
|
272
|
+
for (const relativePath of changedPaths) {
|
|
273
|
+
const language = languageForPath(relativePath);
|
|
274
|
+
if (!language) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
let baseSnapshot;
|
|
278
|
+
let headSnapshot;
|
|
279
|
+
try {
|
|
280
|
+
baseSnapshot = readGitSnapshot(root, policy.base_ref, relativePath, policy.max_file_bytes);
|
|
281
|
+
headSnapshot = policy.head_ref
|
|
282
|
+
? readGitSnapshot(root, policy.head_ref, relativePath, policy.max_file_bytes)
|
|
283
|
+
: readWorktreeSnapshot(root, relativePath, policy.max_file_bytes);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
const code = message.includes('max_file_bytes') ? 'export_diff_file_too_large' : 'export_diff_unreadable_path';
|
|
288
|
+
findings.push(makeFinding(code, 'high', relativePath, message));
|
|
289
|
+
issues.push(`${relativePath}: ${message}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (hasUnresolvedReexport(baseSnapshot.text) || hasUnresolvedReexport(headSnapshot.text)) {
|
|
293
|
+
const message = `${relativePath} contains re-export declarations; export diff does not resolve barrel targets.`;
|
|
294
|
+
findings.push(makeFinding('export_diff_reexport_unresolved', 'low', relativePath, message));
|
|
295
|
+
}
|
|
296
|
+
const beforeSymbols = snapshotSymbols(relativePath, baseSnapshot);
|
|
297
|
+
const afterSymbols = snapshotSymbols(relativePath, headSnapshot);
|
|
298
|
+
const beforeMap = buildSymbolMap(beforeSymbols, findings, issues);
|
|
299
|
+
const afterMap = buildSymbolMap(afterSymbols, findings, issues);
|
|
300
|
+
const keys = [...new Set([...beforeMap.keys(), ...afterMap.keys()])].sort((left, right) => left.localeCompare(right));
|
|
301
|
+
for (const key of keys) {
|
|
302
|
+
const before = beforeMap.get(key) ?? null;
|
|
303
|
+
const after = afterMap.get(key) ?? null;
|
|
304
|
+
const comparison = compareSymbols(before, after);
|
|
305
|
+
const current = after ?? before;
|
|
306
|
+
if (!current) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
entries.push({
|
|
310
|
+
id: key,
|
|
311
|
+
path: current.path,
|
|
312
|
+
name: current.name,
|
|
313
|
+
kind: comparison.kind,
|
|
314
|
+
change: comparison.change,
|
|
315
|
+
before,
|
|
316
|
+
after,
|
|
317
|
+
compatibility: comparison.compatibility,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
files.push({
|
|
321
|
+
kind: 'source_file',
|
|
322
|
+
path: relativePath,
|
|
323
|
+
language,
|
|
324
|
+
base_sha256: baseSnapshot.sha256,
|
|
325
|
+
head_sha256: headSnapshot.sha256,
|
|
326
|
+
base_exists: baseSnapshot.exists,
|
|
327
|
+
head_exists: headSnapshot.exists,
|
|
328
|
+
exported_count_before: beforeSymbols.length,
|
|
329
|
+
exported_count_after: afterSymbols.length,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const summary = {
|
|
333
|
+
files_changed: files.length,
|
|
334
|
+
added: entries.filter((entry) => entry.change === 'added').length,
|
|
335
|
+
removed: entries.filter((entry) => entry.change === 'removed').length,
|
|
336
|
+
changed: entries.filter((entry) => entry.change === 'changed').length,
|
|
337
|
+
unchanged: entries.filter((entry) => entry.change === 'unchanged').length,
|
|
338
|
+
};
|
|
339
|
+
const status = statusFromFindings(findings);
|
|
340
|
+
return {
|
|
341
|
+
schema_version: '1',
|
|
342
|
+
command: 'script-pack',
|
|
343
|
+
pack_id: 'code',
|
|
344
|
+
script_id: CODE_EXPORT_DIFF_SCRIPT_ID,
|
|
345
|
+
script_ref: CODE_EXPORT_DIFF_SCRIPT_REF,
|
|
346
|
+
action: 'compare',
|
|
347
|
+
status,
|
|
348
|
+
ok: status !== 'error',
|
|
349
|
+
mustflow_root: root,
|
|
350
|
+
policy,
|
|
351
|
+
input_hash: createInputHash(policy, files, entries),
|
|
352
|
+
package_surface: packageSurface(root),
|
|
353
|
+
files,
|
|
354
|
+
exports: entries.sort((left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name)),
|
|
355
|
+
summary,
|
|
356
|
+
findings,
|
|
357
|
+
issues,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
@@ -54,14 +54,17 @@ function detectLineEnding(buffer) {
|
|
|
54
54
|
}
|
|
55
55
|
let lfCount = 0;
|
|
56
56
|
let crlfCount = 0;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let searchFrom = 0;
|
|
58
|
+
while (searchFrom < buffer.length) {
|
|
59
|
+
const index = buffer.indexOf(0x0a, searchFrom);
|
|
60
|
+
if (index === -1) {
|
|
61
|
+
break;
|
|
60
62
|
}
|
|
61
63
|
lfCount += 1;
|
|
62
64
|
if (index > 0 && buffer[index - 1] === 0x0d) {
|
|
63
65
|
crlfCount += 1;
|
|
64
66
|
}
|
|
67
|
+
searchFrom = index + 1;
|
|
65
68
|
}
|
|
66
69
|
if (lfCount === 0) {
|
|
67
70
|
return 'none';
|
|
@@ -72,20 +75,30 @@ function detectLineEnding(buffer) {
|
|
|
72
75
|
return crlfCount === lfCount ? 'crlf' : 'mixed';
|
|
73
76
|
}
|
|
74
77
|
function normalizeLf(buffer) {
|
|
78
|
+
const firstCrIndex = buffer.indexOf(0x0d);
|
|
79
|
+
if (firstCrIndex === -1) {
|
|
80
|
+
return buffer;
|
|
81
|
+
}
|
|
75
82
|
const bytes = Buffer.allocUnsafe(buffer.length);
|
|
83
|
+
let readIndex = 0;
|
|
76
84
|
let writeIndex = 0;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
bytes
|
|
81
|
-
writeIndex +=
|
|
82
|
-
if (buffer[index + 1] === 0x0a) {
|
|
83
|
-
index += 1;
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
85
|
+
let crIndex = firstCrIndex;
|
|
86
|
+
while (crIndex !== -1) {
|
|
87
|
+
if (crIndex > readIndex) {
|
|
88
|
+
buffer.copy(bytes, writeIndex, readIndex, crIndex);
|
|
89
|
+
writeIndex += crIndex - readIndex;
|
|
86
90
|
}
|
|
87
|
-
bytes[writeIndex] =
|
|
91
|
+
bytes[writeIndex] = 0x0a;
|
|
88
92
|
writeIndex += 1;
|
|
93
|
+
readIndex = crIndex + 1;
|
|
94
|
+
if (buffer[readIndex] === 0x0a) {
|
|
95
|
+
readIndex += 1;
|
|
96
|
+
}
|
|
97
|
+
crIndex = buffer.indexOf(0x0d, readIndex);
|
|
98
|
+
}
|
|
99
|
+
if (readIndex < buffer.length) {
|
|
100
|
+
buffer.copy(bytes, writeIndex, readIndex);
|
|
101
|
+
writeIndex += buffer.length - readIndex;
|
|
89
102
|
}
|
|
90
103
|
return bytes.subarray(0, writeIndex);
|
|
91
104
|
}
|
|
@@ -248,6 +248,39 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
248
248
|
'--json',
|
|
249
249
|
],
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
id: 'dependency-graph-report',
|
|
253
|
+
schemaFile: 'dependency-graph-report.schema.json',
|
|
254
|
+
producer: 'mf script-pack run code/dependency-graph scan <path...> --json',
|
|
255
|
+
packaged: true,
|
|
256
|
+
documented: true,
|
|
257
|
+
installedCommand: [
|
|
258
|
+
'mf',
|
|
259
|
+
'script-pack',
|
|
260
|
+
'run',
|
|
261
|
+
'code/dependency-graph',
|
|
262
|
+
'scan',
|
|
263
|
+
'node_modules/mustflow/dist/cli/index.js',
|
|
264
|
+
'--json',
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'change-impact-report',
|
|
269
|
+
schemaFile: 'change-impact-report.schema.json',
|
|
270
|
+
producer: 'mf script-pack run code/change-impact analyze [path...] --json',
|
|
271
|
+
packaged: true,
|
|
272
|
+
documented: true,
|
|
273
|
+
installedCommand: [
|
|
274
|
+
'mf',
|
|
275
|
+
'script-pack',
|
|
276
|
+
'run',
|
|
277
|
+
'code/change-impact',
|
|
278
|
+
'analyze',
|
|
279
|
+
'--base',
|
|
280
|
+
'HEAD',
|
|
281
|
+
'--json',
|
|
282
|
+
],
|
|
283
|
+
},
|
|
251
284
|
{
|
|
252
285
|
id: 'code-symbol-read-report',
|
|
253
286
|
schemaFile: 'code-symbol-read-report.schema.json',
|
|
@@ -268,6 +301,47 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
268
301
|
'--json',
|
|
269
302
|
],
|
|
270
303
|
},
|
|
304
|
+
{
|
|
305
|
+
id: 'route-outline-report',
|
|
306
|
+
schemaFile: 'route-outline-report.schema.json',
|
|
307
|
+
producer: 'mf script-pack run code/route-outline scan <path...> --json',
|
|
308
|
+
packaged: true,
|
|
309
|
+
documented: true,
|
|
310
|
+
installedCommand: [
|
|
311
|
+
'mf',
|
|
312
|
+
'script-pack',
|
|
313
|
+
'run',
|
|
314
|
+
'code/route-outline',
|
|
315
|
+
'scan',
|
|
316
|
+
'node_modules/mustflow/dist/cli/index.js',
|
|
317
|
+
'--json',
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'export-diff-report',
|
|
322
|
+
schemaFile: 'export-diff-report.schema.json',
|
|
323
|
+
producer: 'mf script-pack run code/export-diff compare --json',
|
|
324
|
+
packaged: true,
|
|
325
|
+
documented: true,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'reference-drift-report',
|
|
329
|
+
schemaFile: 'reference-drift-report.schema.json',
|
|
330
|
+
producer: 'mf script-pack run docs/reference-drift check [path...] --json',
|
|
331
|
+
packaged: true,
|
|
332
|
+
documented: true,
|
|
333
|
+
installedCommand: [
|
|
334
|
+
'mf',
|
|
335
|
+
'script-pack',
|
|
336
|
+
'run',
|
|
337
|
+
'docs/reference-drift',
|
|
338
|
+
'check',
|
|
339
|
+
'node_modules/mustflow/README.md',
|
|
340
|
+
'node_modules/mustflow/schemas/README.md',
|
|
341
|
+
'--json',
|
|
342
|
+
],
|
|
343
|
+
expectedExitCodes: [0, 1],
|
|
344
|
+
},
|
|
271
345
|
{
|
|
272
346
|
id: 'text-budget-report',
|
|
273
347
|
schemaFile: 'text-budget-report.schema.json',
|
|
@@ -305,6 +379,58 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
305
379
|
],
|
|
306
380
|
expectedExitCodes: [0, 1],
|
|
307
381
|
},
|
|
382
|
+
{
|
|
383
|
+
id: 'config-chain-report',
|
|
384
|
+
schemaFile: 'config-chain-report.schema.json',
|
|
385
|
+
producer: 'mf script-pack run repo/config-chain inspect <path...> --json',
|
|
386
|
+
packaged: true,
|
|
387
|
+
documented: true,
|
|
388
|
+
installedCommand: [
|
|
389
|
+
'mf',
|
|
390
|
+
'script-pack',
|
|
391
|
+
'run',
|
|
392
|
+
'repo/config-chain',
|
|
393
|
+
'inspect',
|
|
394
|
+
'node_modules/mustflow/dist/cli/index.js',
|
|
395
|
+
'--json',
|
|
396
|
+
],
|
|
397
|
+
expectedExitCodes: [0, 1],
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: 'env-contract-report',
|
|
401
|
+
schemaFile: 'env-contract-report.schema.json',
|
|
402
|
+
producer: 'mf script-pack run repo/env-contract scan [path...] --json',
|
|
403
|
+
packaged: true,
|
|
404
|
+
documented: true,
|
|
405
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/env-contract', 'scan', 'AGENTS.md', '--json'],
|
|
406
|
+
expectedExitCodes: [0, 1],
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'secret-risk-scan-report',
|
|
410
|
+
schemaFile: 'secret-risk-scan-report.schema.json',
|
|
411
|
+
producer: 'mf script-pack run repo/secret-risk-scan scan [path...] --json',
|
|
412
|
+
packaged: true,
|
|
413
|
+
documented: true,
|
|
414
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/secret-risk-scan', 'scan', 'AGENTS.md', '--json'],
|
|
415
|
+
expectedExitCodes: [0, 1],
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: 'related-files-report',
|
|
419
|
+
schemaFile: 'related-files-report.schema.json',
|
|
420
|
+
producer: 'mf script-pack run repo/related-files map <path...> --json',
|
|
421
|
+
packaged: true,
|
|
422
|
+
documented: true,
|
|
423
|
+
installedCommand: [
|
|
424
|
+
'mf',
|
|
425
|
+
'script-pack',
|
|
426
|
+
'run',
|
|
427
|
+
'repo/related-files',
|
|
428
|
+
'map',
|
|
429
|
+
'node_modules/mustflow/dist/cli/index.js',
|
|
430
|
+
'--json',
|
|
431
|
+
],
|
|
432
|
+
expectedExitCodes: [0, 1],
|
|
433
|
+
},
|
|
308
434
|
{
|
|
309
435
|
id: 'skill-route-report',
|
|
310
436
|
schemaFile: 'skill-route-report.schema.json',
|