mustflow 2.108.2 → 2.108.8
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 +3 -0
- package/dist/cli/commands/script-pack.js +3 -0
- package/dist/cli/i18n/en.js +37 -0
- package/dist/cli/i18n/es.js +37 -0
- package/dist/cli/i18n/fr.js +37 -0
- package/dist/cli/i18n/hi.js +37 -0
- package/dist/cli/i18n/ko.js +37 -0
- package/dist/cli/i18n/zh.js +37 -0
- package/dist/cli/lib/command-registry.js +3 -0
- package/dist/cli/lib/script-pack-registry.js +84 -0
- package/dist/cli/script-packs/repo-automation-surface.js +88 -0
- package/dist/cli/script-packs/repo-dependency-surface.js +87 -0
- package/dist/cli/script-packs/repo-toolchain-provenance.js +90 -0
- package/dist/core/public-json-contracts.js +27 -0
- package/dist/core/repo-automation-surface.js +376 -0
- package/dist/core/repo-dependency-surface.js +282 -0
- package/dist/core/repo-toolchain-provenance.js +421 -0
- package/dist/core/script-pack-suggestions.js +33 -1
- package/package.json +1 -1
- package/schemas/README.md +10 -0
- package/schemas/repo-automation-surface-report.schema.json +148 -0
- package/schemas/repo-dependency-surface-report.schema.json +121 -0
- package/schemas/repo-toolchain-provenance-report.schema.json +124 -0
- package/templates/default/i18n.toml +18 -6
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +15 -5
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +98 -22
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +86 -27
- package/templates/default/locales/en/.mustflow/skills/routes.toml +16 -4
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +51 -32
- package/templates/default/locales/en/.mustflow/skills/split-refactor-residual-path-review/SKILL.md +176 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +47 -29
- package/templates/default/locales/en/.mustflow/skills/ui-state-resurrection-review/SKILL.md +218 -0
- package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +14 -13
- package/templates/default/manifest.toml +15 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isRecord, readMustflowOwnedTomlFile } from './config-loading.js';
|
|
5
|
+
export const REPO_TOOLCHAIN_PROVENANCE_PACK_ID = 'repo';
|
|
6
|
+
export const REPO_TOOLCHAIN_PROVENANCE_SCRIPT_ID = 'toolchain-provenance';
|
|
7
|
+
export const REPO_TOOLCHAIN_PROVENANCE_SCRIPT_REF = `${REPO_TOOLCHAIN_PROVENANCE_PACK_ID}/${REPO_TOOLCHAIN_PROVENANCE_SCRIPT_ID}`;
|
|
8
|
+
const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
9
|
+
const NODE_VERSION_FILES = ['.node-version', '.nvmrc'];
|
|
10
|
+
const PACKAGE_LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'];
|
|
11
|
+
const PYTHON_LOCKFILES = ['uv.lock', 'poetry.lock', 'Pipfile.lock'];
|
|
12
|
+
const RUST_LOCKFILES = ['Cargo.lock'];
|
|
13
|
+
const GO_LOCKFILES = ['go.sum'];
|
|
14
|
+
const STATIC_SOURCE_PATHS = [
|
|
15
|
+
'package.json',
|
|
16
|
+
'pyproject.toml',
|
|
17
|
+
'go.mod',
|
|
18
|
+
'rust-toolchain.toml',
|
|
19
|
+
'rust-toolchain',
|
|
20
|
+
'mise.toml',
|
|
21
|
+
'.mise.toml',
|
|
22
|
+
'.tool-versions',
|
|
23
|
+
'.python-version',
|
|
24
|
+
...NODE_VERSION_FILES,
|
|
25
|
+
...PACKAGE_LOCKFILES,
|
|
26
|
+
...PYTHON_LOCKFILES,
|
|
27
|
+
...RUST_LOCKFILES,
|
|
28
|
+
...GO_LOCKFILES,
|
|
29
|
+
'Dockerfile',
|
|
30
|
+
];
|
|
31
|
+
function sha256(value) {
|
|
32
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
33
|
+
}
|
|
34
|
+
function normalizeRelativePath(value) {
|
|
35
|
+
return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
36
|
+
}
|
|
37
|
+
function lineForOffset(content, offset) {
|
|
38
|
+
let line = 1;
|
|
39
|
+
for (let index = 0; index < offset; index += 1) {
|
|
40
|
+
if (content.charCodeAt(index) === 10) {
|
|
41
|
+
line += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return line;
|
|
45
|
+
}
|
|
46
|
+
function safeReadText(root, relativePath, maxFileBytes, issues) {
|
|
47
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
48
|
+
const absolute = path.join(root, ...normalized.split('/'));
|
|
49
|
+
try {
|
|
50
|
+
const stats = statSync(absolute);
|
|
51
|
+
if (!stats.isFile()) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (stats.size > maxFileBytes) {
|
|
55
|
+
issues.push(`${normalized} exceeds max_file_bytes (${stats.size} > ${maxFileBytes}).`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return readFileSync(absolute, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!existsSync(absolute)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
issues.push(`Could not read ${normalized}: ${message}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function firstLine(content, pattern) {
|
|
70
|
+
const match = pattern.exec(content);
|
|
71
|
+
return match && match.index >= 0 ? lineForOffset(content, match.index) : null;
|
|
72
|
+
}
|
|
73
|
+
function uniqueStrings(values) {
|
|
74
|
+
return [...new Set([...values].filter((value) => value.trim().length > 0))].sort((left, right) => left.localeCompare(right));
|
|
75
|
+
}
|
|
76
|
+
function addSource(sources, source) {
|
|
77
|
+
if (typeof source.value !== 'string' || source.value.trim().length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
sources.push({ ...source, value: source.value.trim() });
|
|
81
|
+
}
|
|
82
|
+
function scanPackageJson(root, sources, scannedPaths, issues) {
|
|
83
|
+
const relativePath = 'package.json';
|
|
84
|
+
scannedPaths.add(relativePath);
|
|
85
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
86
|
+
if (content === null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(content);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
issues.push(`Could not parse ${relativePath}: ${message}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(parsed)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
addSource(sources, {
|
|
102
|
+
kind: 'package_manager',
|
|
103
|
+
source_kind: 'package_json',
|
|
104
|
+
path: relativePath,
|
|
105
|
+
line: firstLine(content, /"packageManager"\s*:/u),
|
|
106
|
+
key: 'packageManager',
|
|
107
|
+
value: parsed.packageManager,
|
|
108
|
+
});
|
|
109
|
+
const engines = isRecord(parsed.engines) ? parsed.engines : undefined;
|
|
110
|
+
addSource(sources, {
|
|
111
|
+
kind: 'node',
|
|
112
|
+
source_kind: 'package_json',
|
|
113
|
+
path: relativePath,
|
|
114
|
+
line: firstLine(content, /"node"\s*:/u),
|
|
115
|
+
key: 'engines.node',
|
|
116
|
+
value: engines?.node,
|
|
117
|
+
});
|
|
118
|
+
const devEngines = isRecord(parsed.devEngines) ? parsed.devEngines : undefined;
|
|
119
|
+
const packageManager = isRecord(devEngines?.packageManager) ? devEngines?.packageManager : undefined;
|
|
120
|
+
addSource(sources, {
|
|
121
|
+
kind: 'package_manager',
|
|
122
|
+
source_kind: 'package_json',
|
|
123
|
+
path: relativePath,
|
|
124
|
+
line: firstLine(content, /"packageManager"\s*:/u),
|
|
125
|
+
key: 'devEngines.packageManager.name',
|
|
126
|
+
value: packageManager?.name,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function scanLineFile(root, relativePath, kind, sourceKind, key, sources, scannedPaths, issues) {
|
|
130
|
+
scannedPaths.add(relativePath);
|
|
131
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
132
|
+
if (content === null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const value = content
|
|
136
|
+
.split(/\r?\n/u)
|
|
137
|
+
.map((line) => line.trim())
|
|
138
|
+
.find((line) => line.length > 0 && !line.startsWith('#'));
|
|
139
|
+
addSource(sources, { kind, source_kind: sourceKind, path: relativePath, line: value ? 1 : null, key, value });
|
|
140
|
+
}
|
|
141
|
+
function scanGoMod(root, sources, scannedPaths, issues) {
|
|
142
|
+
const relativePath = 'go.mod';
|
|
143
|
+
scannedPaths.add(relativePath);
|
|
144
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
145
|
+
if (content === null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const [key, pattern] of [
|
|
149
|
+
['go', /^go\s+([^\s]+)/mu],
|
|
150
|
+
['toolchain', /^toolchain\s+([^\s]+)/mu],
|
|
151
|
+
]) {
|
|
152
|
+
const match = pattern.exec(content);
|
|
153
|
+
addSource(sources, {
|
|
154
|
+
kind: 'go',
|
|
155
|
+
source_kind: 'go_mod',
|
|
156
|
+
path: relativePath,
|
|
157
|
+
line: match && match.index >= 0 ? lineForOffset(content, match.index) : null,
|
|
158
|
+
key,
|
|
159
|
+
value: match?.[1],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function scanRustToolchain(root, sources, scannedPaths, issues) {
|
|
164
|
+
for (const relativePath of ['rust-toolchain.toml', 'rust-toolchain']) {
|
|
165
|
+
scannedPaths.add(relativePath);
|
|
166
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
167
|
+
if (content === null) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const channel = /channel\s*=\s*["']([^"']+)["']/u.exec(content)?.[1] ?? content.trim().split(/\s+/u)[0];
|
|
171
|
+
addSource(sources, {
|
|
172
|
+
kind: 'rust',
|
|
173
|
+
source_kind: 'rust_toolchain',
|
|
174
|
+
path: relativePath,
|
|
175
|
+
line: firstLine(content, /channel\s*=|^\s*[^\s#]+/u),
|
|
176
|
+
key: 'channel',
|
|
177
|
+
value: channel,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function scanPyproject(root, sources, scannedPaths, issues) {
|
|
182
|
+
const relativePath = 'pyproject.toml';
|
|
183
|
+
scannedPaths.add(relativePath);
|
|
184
|
+
if (!existsSync(path.join(root, relativePath))) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const parsed = readMustflowOwnedTomlFile(root, relativePath);
|
|
189
|
+
if (!isRecord(parsed)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const project = isRecord(parsed.project) ? parsed.project : undefined;
|
|
193
|
+
addSource(sources, {
|
|
194
|
+
kind: 'python',
|
|
195
|
+
source_kind: 'pyproject_toml',
|
|
196
|
+
path: relativePath,
|
|
197
|
+
line: null,
|
|
198
|
+
key: 'project.requires-python',
|
|
199
|
+
value: project?.['requires-python'],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
issues.push(`Could not parse ${relativePath}: ${message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function scanToolVersionFile(root, sources, scannedPaths, issues) {
|
|
208
|
+
const relativePath = '.tool-versions';
|
|
209
|
+
scannedPaths.add(relativePath);
|
|
210
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
211
|
+
if (content === null) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const [name, value] = trimmed.split(/\s+/u);
|
|
220
|
+
const kind = name === 'nodejs' ? 'node' : name === 'python' ? 'python' : name === 'golang' ? 'go' : name === 'rust' ? 'rust' : null;
|
|
221
|
+
if (!kind) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
addSource(sources, {
|
|
225
|
+
kind,
|
|
226
|
+
source_kind: 'tool_versions',
|
|
227
|
+
path: relativePath,
|
|
228
|
+
line: index + 1,
|
|
229
|
+
key: name,
|
|
230
|
+
value,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function scanMise(root, sources, scannedPaths, issues) {
|
|
235
|
+
for (const relativePath of ['mise.toml', '.mise.toml']) {
|
|
236
|
+
scannedPaths.add(relativePath);
|
|
237
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
238
|
+
if (content === null) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const [tool, kind] of [
|
|
242
|
+
['node', 'node'],
|
|
243
|
+
['nodejs', 'node'],
|
|
244
|
+
['python', 'python'],
|
|
245
|
+
['go', 'go'],
|
|
246
|
+
['golang', 'go'],
|
|
247
|
+
['rust', 'rust'],
|
|
248
|
+
['bun', 'bun'],
|
|
249
|
+
]) {
|
|
250
|
+
const pattern = new RegExp(`^\\s*${tool}\\s*=\\s*["']?([^"'\\n#]+)`, 'mu');
|
|
251
|
+
const match = pattern.exec(content);
|
|
252
|
+
addSource(sources, {
|
|
253
|
+
kind,
|
|
254
|
+
source_kind: 'mise_toml',
|
|
255
|
+
path: relativePath,
|
|
256
|
+
line: match && match.index >= 0 ? lineForOffset(content, match.index) : null,
|
|
257
|
+
key: tool,
|
|
258
|
+
value: match?.[1],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function scanDockerAndCi(root, sources, scannedPaths, issues) {
|
|
264
|
+
for (const relativePath of ['Dockerfile', '.github/workflows/ci.yml', '.github/workflows/ci.yaml']) {
|
|
265
|
+
scannedPaths.add(relativePath);
|
|
266
|
+
const content = safeReadText(root, relativePath, DEFAULT_MAX_FILE_BYTES, issues);
|
|
267
|
+
if (content === null) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const sourceKind = relativePath === 'Dockerfile' ? 'dockerfile' : 'ci_workflow';
|
|
271
|
+
for (const [kind, key, pattern] of [
|
|
272
|
+
['node', 'node-version', /node-version:\s*['"]?([^'"\n]+)/u],
|
|
273
|
+
['python', 'python-version', /python-version:\s*['"]?([^'"\n]+)/u],
|
|
274
|
+
['go', 'go-version', /go-version:\s*['"]?([^'"\n]+)/u],
|
|
275
|
+
['rust', 'rust-toolchain', /toolchain:\s*['"]?([^'"\n]+)/u],
|
|
276
|
+
['docker', 'FROM', /^FROM\s+([^\s]+)/mu],
|
|
277
|
+
]) {
|
|
278
|
+
const match = pattern.exec(content);
|
|
279
|
+
addSource(sources, {
|
|
280
|
+
kind,
|
|
281
|
+
source_kind: sourceKind,
|
|
282
|
+
path: relativePath,
|
|
283
|
+
line: match && match.index >= 0 ? lineForOffset(content, match.index) : null,
|
|
284
|
+
key,
|
|
285
|
+
value: match?.[1],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function detectLockfiles(root, scannedPaths) {
|
|
291
|
+
const lockfiles = [...PACKAGE_LOCKFILES, ...PYTHON_LOCKFILES, ...RUST_LOCKFILES, ...GO_LOCKFILES].filter((relativePath) => {
|
|
292
|
+
scannedPaths.add(relativePath);
|
|
293
|
+
return existsSync(path.join(root, relativePath));
|
|
294
|
+
});
|
|
295
|
+
return uniqueStrings(lockfiles);
|
|
296
|
+
}
|
|
297
|
+
function createFindings(sources, lockfiles) {
|
|
298
|
+
const findings = [];
|
|
299
|
+
const nodeSources = sources.filter((source) => source.kind === 'node' && source.source_kind !== 'ci_workflow');
|
|
300
|
+
const nodeValues = uniqueStrings(nodeSources.map((source) => source.value));
|
|
301
|
+
if (nodeValues.length > 1) {
|
|
302
|
+
findings.push({
|
|
303
|
+
code: 'conflicting_node_version_sources',
|
|
304
|
+
severity: 'medium',
|
|
305
|
+
path: nodeSources[0]?.path ?? 'package.json',
|
|
306
|
+
message: 'Multiple local Node version declarations were detected. Review which source owns the runtime contract.',
|
|
307
|
+
json_pointer: null,
|
|
308
|
+
metric: 'node_version_source_count',
|
|
309
|
+
actual: nodeValues.length,
|
|
310
|
+
expected: 1,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const packageLockfiles = lockfiles.filter((lockfile) => PACKAGE_LOCKFILES.includes(lockfile));
|
|
314
|
+
if (packageLockfiles.length > 1) {
|
|
315
|
+
findings.push({
|
|
316
|
+
code: 'conflicting_package_manager_lockfiles',
|
|
317
|
+
severity: 'high',
|
|
318
|
+
path: packageLockfiles[0] ?? 'package.json',
|
|
319
|
+
message: 'Multiple JavaScript package-manager lockfiles were detected.',
|
|
320
|
+
json_pointer: null,
|
|
321
|
+
metric: 'package_manager_lockfile_count',
|
|
322
|
+
actual: packageLockfiles.length,
|
|
323
|
+
expected: 1,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const packageManagerSources = sources.filter((source) => source.kind === 'package_manager');
|
|
327
|
+
if (packageManagerSources.length > 0 && packageLockfiles.length === 0) {
|
|
328
|
+
findings.push({
|
|
329
|
+
code: 'package_manager_without_lockfile',
|
|
330
|
+
severity: 'medium',
|
|
331
|
+
path: packageManagerSources[0]?.path ?? 'package.json',
|
|
332
|
+
message: 'A package manager is declared, but no JavaScript lockfile was detected.',
|
|
333
|
+
json_pointer: null,
|
|
334
|
+
metric: 'package_manager_lockfile_count',
|
|
335
|
+
actual: 0,
|
|
336
|
+
expected: 1,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (sources.some((source) => source.kind === 'node') && packageManagerSources.length === 0) {
|
|
340
|
+
findings.push({
|
|
341
|
+
code: 'toolchain_declared_without_package_manager',
|
|
342
|
+
severity: 'low',
|
|
343
|
+
path: sources.find((source) => source.kind === 'node')?.path ?? 'package.json',
|
|
344
|
+
message: 'Node is declared, but no package manager provenance was detected.',
|
|
345
|
+
json_pointer: null,
|
|
346
|
+
metric: 'package_manager_source_count',
|
|
347
|
+
actual: 0,
|
|
348
|
+
expected: 1,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
for (const source of sources.filter((entry) => entry.source_kind === 'ci_workflow')) {
|
|
352
|
+
const localSameKind = sources.some((entry) => entry.kind === source.kind && entry.source_kind !== 'ci_workflow');
|
|
353
|
+
if (!localSameKind) {
|
|
354
|
+
findings.push({
|
|
355
|
+
code: 'runtime_declared_in_ci_only',
|
|
356
|
+
severity: 'low',
|
|
357
|
+
path: source.path,
|
|
358
|
+
message: `${source.kind} runtime appears in CI, but no local repository contract was detected.`,
|
|
359
|
+
json_pointer: null,
|
|
360
|
+
metric: null,
|
|
361
|
+
actual: null,
|
|
362
|
+
expected: null,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return findings;
|
|
367
|
+
}
|
|
368
|
+
function createSummary(sources, lockfiles, findings) {
|
|
369
|
+
const runtimeKinds = uniqueStrings(sources.filter((source) => source.kind !== 'package_manager').map((source) => source.kind));
|
|
370
|
+
return {
|
|
371
|
+
source_count: sources.length,
|
|
372
|
+
runtime_count: runtimeKinds.length,
|
|
373
|
+
package_manager_count: sources.filter((source) => source.kind === 'package_manager').length,
|
|
374
|
+
lockfile_count: lockfiles.length,
|
|
375
|
+
ci_source_count: sources.filter((source) => source.source_kind === 'ci_workflow').length,
|
|
376
|
+
finding_count: findings.length,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
export function inspectRepoToolchainProvenance(projectRoot) {
|
|
380
|
+
const root = path.resolve(projectRoot);
|
|
381
|
+
const issues = [];
|
|
382
|
+
const scannedPaths = new Set(STATIC_SOURCE_PATHS);
|
|
383
|
+
const sources = [];
|
|
384
|
+
scanPackageJson(root, sources, scannedPaths, issues);
|
|
385
|
+
for (const nodePath of NODE_VERSION_FILES) {
|
|
386
|
+
scanLineFile(root, nodePath, 'node', 'node_version_file', nodePath, sources, scannedPaths, issues);
|
|
387
|
+
}
|
|
388
|
+
scanLineFile(root, '.python-version', 'python', 'python_version_file', '.python-version', sources, scannedPaths, issues);
|
|
389
|
+
scanPyproject(root, sources, scannedPaths, issues);
|
|
390
|
+
scanGoMod(root, sources, scannedPaths, issues);
|
|
391
|
+
scanRustToolchain(root, sources, scannedPaths, issues);
|
|
392
|
+
scanToolVersionFile(root, sources, scannedPaths, issues);
|
|
393
|
+
scanMise(root, sources, scannedPaths, issues);
|
|
394
|
+
scanDockerAndCi(root, sources, scannedPaths, issues);
|
|
395
|
+
const lockfiles = detectLockfiles(root, scannedPaths);
|
|
396
|
+
const sortedSources = sources.sort((left, right) => left.path.localeCompare(right.path) || left.key.localeCompare(right.key));
|
|
397
|
+
const findings = createFindings(sortedSources, lockfiles);
|
|
398
|
+
const summary = createSummary(sortedSources, lockfiles, findings);
|
|
399
|
+
const status = issues.length > 0 ? 'error' : findings.length > 0 ? 'failed' : 'passed';
|
|
400
|
+
return {
|
|
401
|
+
schema_version: '1',
|
|
402
|
+
command: 'script-pack',
|
|
403
|
+
pack_id: REPO_TOOLCHAIN_PROVENANCE_PACK_ID,
|
|
404
|
+
script_id: REPO_TOOLCHAIN_PROVENANCE_SCRIPT_ID,
|
|
405
|
+
script_ref: REPO_TOOLCHAIN_PROVENANCE_SCRIPT_REF,
|
|
406
|
+
action: 'inspect',
|
|
407
|
+
status,
|
|
408
|
+
ok: status === 'passed',
|
|
409
|
+
mustflow_root: root,
|
|
410
|
+
input: {
|
|
411
|
+
scanned_paths: uniqueStrings(scannedPaths),
|
|
412
|
+
max_file_bytes: DEFAULT_MAX_FILE_BYTES,
|
|
413
|
+
},
|
|
414
|
+
input_hash: sha256(JSON.stringify({ summary, sources: sortedSources, lockfiles, findings, issues })),
|
|
415
|
+
summary,
|
|
416
|
+
sources: sortedSources,
|
|
417
|
+
lockfiles,
|
|
418
|
+
findings,
|
|
419
|
+
issues,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -11,7 +11,7 @@ const CODE_NAVIGATION_SCRIPT_REFS = new Set([
|
|
|
11
11
|
'repo/related-files',
|
|
12
12
|
]);
|
|
13
13
|
const CONFIG_CHAIN_SURFACES = new Set(['config', 'package', 'source', 'test']);
|
|
14
|
-
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|wrangler\.(?:toml|jsonc?)|vercel\.json|netlify\.toml|Dockerfile|docker-compose\.ya?ml|compose\.ya?ml)$/u;
|
|
14
|
+
const CONFIG_FILE_PATTERN = /(?:^|\/)(?:\.gitignore|\.env\.(?:example|sample|template|defaults)|\.dev\.vars\.example|\.nvmrc|\.node-version|\.python-version|\.tool-versions|mise\.toml|\.mise\.toml|pyproject\.toml|go\.mod|rust-toolchain(?:\.toml)?|Makefile|Taskfile\.ya?ml|justfile|Justfile|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|wrangler\.(?:toml|jsonc?)|vercel\.json|netlify\.toml|Dockerfile|docker-compose\.ya?ml|compose\.ya?ml)$/u;
|
|
15
15
|
export function isScriptPackSuggestionPhase(value) {
|
|
16
16
|
return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
|
|
17
17
|
}
|
|
@@ -70,7 +70,15 @@ export function classifyScriptPackPathSurface(relativePath) {
|
|
|
70
70
|
}
|
|
71
71
|
if (normalized === 'package.json' ||
|
|
72
72
|
normalized === 'bun.lock' ||
|
|
73
|
+
normalized === 'bun.lockb' ||
|
|
73
74
|
normalized === 'package-lock.json' ||
|
|
75
|
+
normalized === 'npm-shrinkwrap.json' ||
|
|
76
|
+
normalized === 'pnpm-lock.yaml' ||
|
|
77
|
+
normalized === 'yarn.lock' ||
|
|
78
|
+
normalized === 'uv.lock' ||
|
|
79
|
+
normalized === 'poetry.lock' ||
|
|
80
|
+
normalized === 'Cargo.lock' ||
|
|
81
|
+
normalized === 'go.sum' ||
|
|
74
82
|
normalized.startsWith('.github/workflows/')) {
|
|
75
83
|
surfaces.push('package');
|
|
76
84
|
}
|
|
@@ -267,6 +275,15 @@ function createRunHint(script, analyzedPaths) {
|
|
|
267
275
|
if (script.ref === 'repo/version-source') {
|
|
268
276
|
return 'mf script-pack run repo/version-source inspect --json';
|
|
269
277
|
}
|
|
278
|
+
if (script.ref === 'repo/toolchain-provenance') {
|
|
279
|
+
return 'mf script-pack run repo/toolchain-provenance inspect --json';
|
|
280
|
+
}
|
|
281
|
+
if (script.ref === 'repo/automation-surface') {
|
|
282
|
+
return 'mf script-pack run repo/automation-surface inspect --json';
|
|
283
|
+
}
|
|
284
|
+
if (script.ref === 'repo/dependency-surface') {
|
|
285
|
+
return 'mf script-pack run repo/dependency-surface inspect --json';
|
|
286
|
+
}
|
|
270
287
|
if (script.ref === 'repo/approval-gate') {
|
|
271
288
|
return 'mf script-pack run repo/approval-gate check --action <action_type> --json';
|
|
272
289
|
}
|
|
@@ -386,6 +403,21 @@ export function createScriptPackSuggestionReport(mustflowRoot, options) {
|
|
|
386
403
|
score += 2;
|
|
387
404
|
reasons.push('Prioritizes deploy-surface inspection for push, tag, release, docs, and package publication follow-up.');
|
|
388
405
|
}
|
|
406
|
+
if (script.ref === 'repo/toolchain-provenance' &&
|
|
407
|
+
(requestedSurfaces.has('package') || requestedSurfaces.has('config'))) {
|
|
408
|
+
score += 2;
|
|
409
|
+
reasons.push('Prioritizes toolchain provenance for runtime, package-manager, lockfile, Docker, and CI contract surfaces.');
|
|
410
|
+
}
|
|
411
|
+
if (script.ref === 'repo/automation-surface' &&
|
|
412
|
+
(requestedSurfaces.has('package') || requestedSurfaces.has('config'))) {
|
|
413
|
+
score += 2;
|
|
414
|
+
reasons.push('Prioritizes automation surface inspection for package scripts, workflows, and command-intent mapping.');
|
|
415
|
+
}
|
|
416
|
+
if (script.ref === 'repo/dependency-surface' &&
|
|
417
|
+
(requestedSurfaces.has('package') || requestedSurfaces.has('config'))) {
|
|
418
|
+
score += 2;
|
|
419
|
+
reasons.push('Prioritizes dependency surface inspection for manifests, lockfiles, update automation, and audit policy evidence.');
|
|
420
|
+
}
|
|
389
421
|
if (script.ref === 'repo/security-pattern-scan' &&
|
|
390
422
|
(hasSourcePath ||
|
|
391
423
|
requestedSurfaces.has('config') ||
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -165,6 +165,16 @@ Current schemas:
|
|
|
165
165
|
- `repo-version-source-report.schema.json`: output of
|
|
166
166
|
`mf script-pack run repo/version-source inspect --json`, containing detected version sources,
|
|
167
167
|
release-versioning preference status, source authority counts, and missing-source findings
|
|
168
|
+
- `repo-toolchain-provenance-report.schema.json`: output of
|
|
169
|
+
`mf script-pack run repo/toolchain-provenance inspect --json`, containing repository-visible
|
|
170
|
+
runtime, package-manager, lockfile, Docker, and CI toolchain provenance evidence
|
|
171
|
+
- `repo-automation-surface-report.schema.json`: output of
|
|
172
|
+
`mf script-pack run repo/automation-surface inspect --json`, containing package scripts,
|
|
173
|
+
task-runner entries, CI workflows, mustflow command intents, risk classifications, and
|
|
174
|
+
command-contract coverage findings
|
|
175
|
+
- `repo-dependency-surface-report.schema.json`: output of
|
|
176
|
+
`mf script-pack run repo/dependency-surface inspect --json`, containing dependency manifests,
|
|
177
|
+
lockfiles, update automation, audit, license, and SBOM policy surfaces
|
|
168
178
|
- `repo-approval-gate-report.schema.json`: output of
|
|
169
179
|
`mf script-pack run repo/approval-gate check --action <type> --json`, containing approval policy
|
|
170
180
|
decisions, required-action findings, and unreadable policy issues
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://mustflow.github.io/schemas/repo-automation-surface-report.schema.json",
|
|
4
|
+
"title": "mustflow repo automation-surface report",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"command",
|
|
10
|
+
"pack_id",
|
|
11
|
+
"script_id",
|
|
12
|
+
"script_ref",
|
|
13
|
+
"action",
|
|
14
|
+
"status",
|
|
15
|
+
"ok",
|
|
16
|
+
"mustflow_root",
|
|
17
|
+
"input",
|
|
18
|
+
"input_hash",
|
|
19
|
+
"summary",
|
|
20
|
+
"surfaces",
|
|
21
|
+
"findings",
|
|
22
|
+
"issues"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"schema_version": { "const": "1" },
|
|
26
|
+
"command": { "const": "script-pack" },
|
|
27
|
+
"pack_id": { "const": "repo" },
|
|
28
|
+
"script_id": { "const": "automation-surface" },
|
|
29
|
+
"script_ref": { "const": "repo/automation-surface" },
|
|
30
|
+
"action": { "const": "inspect" },
|
|
31
|
+
"status": { "enum": ["passed", "failed", "error"] },
|
|
32
|
+
"ok": { "type": "boolean" },
|
|
33
|
+
"mustflow_root": { "type": "string" },
|
|
34
|
+
"input": { "$ref": "#/$defs/input" },
|
|
35
|
+
"input_hash": { "$ref": "#/$defs/sha256" },
|
|
36
|
+
"summary": { "$ref": "#/$defs/summary" },
|
|
37
|
+
"surfaces": { "type": "array", "items": { "$ref": "#/$defs/surface" } },
|
|
38
|
+
"findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } },
|
|
39
|
+
"issues": { "type": "array", "items": { "type": "string" } }
|
|
40
|
+
},
|
|
41
|
+
"$defs": {
|
|
42
|
+
"sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
|
43
|
+
"input": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["scanned_paths", "max_file_bytes"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"scanned_paths": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
49
|
+
"max_file_bytes": { "type": "integer", "minimum": 1 }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"summary": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"required": [
|
|
56
|
+
"surface_count",
|
|
57
|
+
"mustflow_intent_count",
|
|
58
|
+
"raw_surface_count",
|
|
59
|
+
"agent_allowed_intent_count",
|
|
60
|
+
"manual_only_intent_count",
|
|
61
|
+
"risky_surface_count",
|
|
62
|
+
"long_running_surface_count"
|
|
63
|
+
],
|
|
64
|
+
"properties": {
|
|
65
|
+
"surface_count": { "type": "integer", "minimum": 0 },
|
|
66
|
+
"mustflow_intent_count": { "type": "integer", "minimum": 0 },
|
|
67
|
+
"raw_surface_count": { "type": "integer", "minimum": 0 },
|
|
68
|
+
"agent_allowed_intent_count": { "type": "integer", "minimum": 0 },
|
|
69
|
+
"manual_only_intent_count": { "type": "integer", "minimum": 0 },
|
|
70
|
+
"risky_surface_count": { "type": "integer", "minimum": 0 },
|
|
71
|
+
"long_running_surface_count": { "type": "integer", "minimum": 0 }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"surface": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": [
|
|
78
|
+
"id",
|
|
79
|
+
"kind",
|
|
80
|
+
"path",
|
|
81
|
+
"line",
|
|
82
|
+
"name",
|
|
83
|
+
"command_hint",
|
|
84
|
+
"category",
|
|
85
|
+
"risks",
|
|
86
|
+
"mapped_intent",
|
|
87
|
+
"agent_allowed"
|
|
88
|
+
],
|
|
89
|
+
"properties": {
|
|
90
|
+
"id": { "type": "string", "minLength": 1 },
|
|
91
|
+
"kind": { "enum": ["ci_workflow", "make_target", "mise_task", "mustflow_intent", "package_script", "taskfile_task"] },
|
|
92
|
+
"path": { "type": "string", "minLength": 1 },
|
|
93
|
+
"line": { "type": ["integer", "null"], "minimum": 1 },
|
|
94
|
+
"name": { "type": "string", "minLength": 1 },
|
|
95
|
+
"command_hint": { "type": ["string", "null"] },
|
|
96
|
+
"category": {
|
|
97
|
+
"enum": [
|
|
98
|
+
"bootstrap",
|
|
99
|
+
"check",
|
|
100
|
+
"clean",
|
|
101
|
+
"db",
|
|
102
|
+
"deploy",
|
|
103
|
+
"deps",
|
|
104
|
+
"dev_server",
|
|
105
|
+
"doctor",
|
|
106
|
+
"fix",
|
|
107
|
+
"release",
|
|
108
|
+
"smoke",
|
|
109
|
+
"test",
|
|
110
|
+
"watch",
|
|
111
|
+
"workflow",
|
|
112
|
+
"unknown"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"risks": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"items": {
|
|
118
|
+
"enum": ["destructive", "git_state", "interactive", "long_running", "network", "release", "secret", "writes"]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"mapped_intent": { "type": ["string", "null"] },
|
|
122
|
+
"agent_allowed": { "type": ["boolean", "null"] }
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"finding": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"required": ["code", "severity", "message", "path", "json_pointer", "metric", "actual", "expected"],
|
|
129
|
+
"properties": {
|
|
130
|
+
"code": {
|
|
131
|
+
"enum": [
|
|
132
|
+
"dangerous_automation_surface",
|
|
133
|
+
"long_running_automation_surface",
|
|
134
|
+
"raw_command_without_mustflow_intent",
|
|
135
|
+
"mustflow_intent_manual_boundary"
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"severity": { "enum": ["low", "medium", "high", "critical"] },
|
|
139
|
+
"message": { "type": "string" },
|
|
140
|
+
"path": { "type": "string", "minLength": 1 },
|
|
141
|
+
"json_pointer": { "type": ["string", "null"] },
|
|
142
|
+
"metric": { "type": ["string", "null"] },
|
|
143
|
+
"actual": { "type": ["number", "null"] },
|
|
144
|
+
"expected": { "type": ["number", "null"] }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|