mustflow 2.108.2 → 2.108.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/commands/script-pack.js +3 -0
  3. package/dist/cli/i18n/en.js +37 -0
  4. package/dist/cli/i18n/es.js +37 -0
  5. package/dist/cli/i18n/fr.js +37 -0
  6. package/dist/cli/i18n/hi.js +37 -0
  7. package/dist/cli/i18n/ko.js +37 -0
  8. package/dist/cli/i18n/zh.js +37 -0
  9. package/dist/cli/lib/command-registry.js +3 -0
  10. package/dist/cli/lib/script-pack-registry.js +84 -0
  11. package/dist/cli/script-packs/repo-automation-surface.js +88 -0
  12. package/dist/cli/script-packs/repo-dependency-surface.js +87 -0
  13. package/dist/cli/script-packs/repo-toolchain-provenance.js +90 -0
  14. package/dist/core/public-json-contracts.js +27 -0
  15. package/dist/core/repo-automation-surface.js +376 -0
  16. package/dist/core/repo-dependency-surface.js +282 -0
  17. package/dist/core/repo-toolchain-provenance.js +421 -0
  18. package/dist/core/script-pack-suggestions.js +33 -1
  19. package/package.json +1 -1
  20. package/schemas/README.md +10 -0
  21. package/schemas/repo-automation-surface-report.schema.json +148 -0
  22. package/schemas/repo-dependency-surface-report.schema.json +121 -0
  23. package/schemas/repo-toolchain-provenance-report.schema.json +124 -0
  24. package/templates/default/i18n.toml +5 -5
  25. package/templates/default/locales/en/.mustflow/skills/INDEX.md +5 -5
  26. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +56 -17
  27. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +86 -27
  28. package/templates/default/locales/en/.mustflow/skills/routes.toml +4 -4
  29. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +51 -32
  30. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +47 -29
  31. package/templates/default/manifest.toml +1 -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.108.2",
3
+ "version": "2.108.3",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
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
+ }