mustflow 2.84.0 → 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.
Files changed (48) hide show
  1. package/README.md +11 -2
  2. package/dist/cli/commands/script-pack.js +4 -0
  3. package/dist/cli/i18n/en.js +79 -0
  4. package/dist/cli/i18n/es.js +79 -0
  5. package/dist/cli/i18n/fr.js +79 -0
  6. package/dist/cli/i18n/hi.js +79 -0
  7. package/dist/cli/i18n/ko.js +79 -0
  8. package/dist/cli/i18n/zh.js +79 -0
  9. package/dist/cli/lib/repo-map.js +27 -6
  10. package/dist/cli/lib/run-root-trust.js +15 -1
  11. package/dist/cli/lib/script-pack-registry.js +114 -0
  12. package/dist/cli/lib/validation/index.js +2 -2
  13. package/dist/cli/lib/validation/primitives.js +4 -1
  14. package/dist/cli/script-packs/code-change-impact.js +172 -0
  15. package/dist/cli/script-packs/code-dependency-graph.js +181 -0
  16. package/dist/cli/script-packs/repo-env-contract.js +156 -0
  17. package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
  18. package/dist/core/change-impact.js +383 -0
  19. package/dist/core/change-verification.js +32 -5
  20. package/dist/core/config-loading.js +121 -4
  21. package/dist/core/dependency-graph.js +490 -0
  22. package/dist/core/env-contract.js +450 -0
  23. package/dist/core/line-endings.js +26 -13
  24. package/dist/core/public-json-contracts.js +51 -0
  25. package/dist/core/route-outline.js +57 -5
  26. package/dist/core/script-pack-suggestions.js +21 -1
  27. package/dist/core/secret-risk-scan.js +440 -0
  28. package/package.json +1 -1
  29. package/schemas/README.md +16 -0
  30. package/schemas/change-impact-report.schema.json +150 -0
  31. package/schemas/commands.schema.json +12 -0
  32. package/schemas/dependency-graph-report.schema.json +149 -0
  33. package/schemas/env-contract-report.schema.json +203 -0
  34. package/schemas/secret-risk-scan-report.schema.json +152 -0
  35. package/templates/default/i18n.toml +16 -10
  36. package/templates/default/locales/en/.mustflow/skills/INDEX.md +7 -2
  37. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
  38. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
  39. package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +23 -8
  40. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
  41. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
  42. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
  43. package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
  44. package/templates/default/locales/en/.mustflow/skills/routes.toml +15 -9
  45. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
  46. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +314 -0
  47. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +13 -10
  48. package/templates/default/manifest.toml +8 -1
@@ -2,13 +2,14 @@ import { spawnSync } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  const CODE_NAVIGATION_SCRIPT_REFS = new Set([
4
4
  'code/outline',
5
+ 'code/dependency-graph',
5
6
  'code/symbol-read',
6
7
  'code/route-outline',
7
8
  'code/export-diff',
8
9
  'repo/related-files',
9
10
  ]);
10
11
  const CONFIG_CHAIN_SURFACES = new Set(['config', 'package', 'source', 'test']);
11
- const CONFIG_FILE_PATTERN = /(?:^|\/)(?: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;
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;
12
13
  export function isScriptPackSuggestionPhase(value) {
13
14
  return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
14
15
  }
@@ -166,6 +167,13 @@ function createRunHint(script, analyzedPaths) {
166
167
  }
167
168
  return 'After code/outline returns a source anchor, run: mf script-pack run code/symbol-read read --anchor <anchor-id> --json';
168
169
  }
170
+ if (script.ref === 'code/dependency-graph') {
171
+ const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
172
+ return createConcretePathHint('mf script-pack run code/dependency-graph scan', sourcePaths, script.usage);
173
+ }
174
+ if (script.ref === 'code/change-impact') {
175
+ return 'mf script-pack run code/change-impact analyze --base HEAD --json';
176
+ }
169
177
  if (script.ref === 'code/route-outline') {
170
178
  const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
171
179
  return createConcretePathHint('mf script-pack run code/route-outline scan', sourcePaths, script.usage);
@@ -200,6 +208,18 @@ function createRunHint(script, analyzedPaths) {
200
208
  .map((entry) => entry.path);
201
209
  return createConcretePathHint('mf script-pack run repo/config-chain inspect', configPaths, script.usage);
202
210
  }
211
+ if (script.ref === 'repo/env-contract') {
212
+ const envPaths = analyzedPaths
213
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'config' || surface === 'source' || surface === 'docs' || surface === 'package'))
214
+ .map((entry) => entry.path);
215
+ return createConcretePathHint('mf script-pack run repo/env-contract scan', envPaths, script.usage);
216
+ }
217
+ if (script.ref === 'repo/secret-risk-scan') {
218
+ const secretRiskPaths = analyzedPaths
219
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'config' || surface === 'source' || surface === 'docs' || surface === 'package' || surface === 'test'))
220
+ .map((entry) => entry.path);
221
+ return createConcretePathHint('mf script-pack run repo/secret-risk-scan scan', secretRiskPaths, script.usage);
222
+ }
203
223
  if (script.ref === 'repo/related-files') {
204
224
  const relatedPaths = analyzedPaths
205
225
  .filter((entry) => entry.surfaces.some((surface) => surface === 'source' || surface === 'test'))
@@ -0,0 +1,440 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
+ export const SECRET_RISK_SCAN_PACK_ID = 'repo';
6
+ export const SECRET_RISK_SCAN_SCRIPT_ID = 'secret-risk-scan';
7
+ export const SECRET_RISK_SCAN_SCRIPT_REF = `${SECRET_RISK_SCAN_PACK_ID}/${SECRET_RISK_SCAN_SCRIPT_ID}`;
8
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
9
+ const DEFAULT_MAX_FILES = 1000;
10
+ const DEFAULT_MAX_FINDINGS = 200;
11
+ const MAX_ISSUES = 50;
12
+ const SCAN_EXTENSIONS = [
13
+ '.ts',
14
+ '.tsx',
15
+ '.mts',
16
+ '.cts',
17
+ '.js',
18
+ '.jsx',
19
+ '.mjs',
20
+ '.cjs',
21
+ '.json',
22
+ '.toml',
23
+ '.yml',
24
+ '.yaml',
25
+ '.md',
26
+ '.mdx',
27
+ ];
28
+ const SECRET_FILE_NAMES = ['.env', '.env.local', '.env.production', '.env.development', '.dev.vars'];
29
+ const ENV_EXAMPLE_NAMES = [
30
+ '.env.example',
31
+ '.env.sample',
32
+ '.env.template',
33
+ '.env.defaults',
34
+ '.env.test.example',
35
+ '.env.local.example',
36
+ '.dev.vars.example',
37
+ ];
38
+ const IGNORED_DIRECTORIES = [
39
+ '.git',
40
+ '.mustflow/cache',
41
+ '.mustflow/state',
42
+ 'node_modules',
43
+ 'dist',
44
+ 'build',
45
+ 'coverage',
46
+ '.next',
47
+ '.turbo',
48
+ ];
49
+ const ERROR_CODES = new Set([
50
+ 'secret_risk_path_outside_root',
51
+ 'secret_risk_unreadable_path',
52
+ ]);
53
+ function normalizeRelativePath(value) {
54
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '') || '.';
55
+ }
56
+ function fingerprint(value) {
57
+ return `sha256:${createHash('sha256').update(value).digest('hex').slice(0, 16)}`;
58
+ }
59
+ function sha256Tagged(value) {
60
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
61
+ }
62
+ function pushIssue(issues, issue) {
63
+ if (issues.length < MAX_ISSUES) {
64
+ issues.push(issue);
65
+ }
66
+ }
67
+ function makeFinding(code, severity, pathValue, message, details = {}) {
68
+ return { code, severity, path: pathValue, message, ...details };
69
+ }
70
+ function isIgnoredDirectory(relativePath) {
71
+ const normalized = normalizeRelativePath(relativePath);
72
+ return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
73
+ }
74
+ function isSecretFile(relativePath) {
75
+ return SECRET_FILE_NAMES.includes(path.basename(relativePath).toLowerCase());
76
+ }
77
+ function isEnvExampleFile(relativePath) {
78
+ return ENV_EXAMPLE_NAMES.includes(path.basename(relativePath).toLowerCase());
79
+ }
80
+ function surfaceForPath(relativePath) {
81
+ if (isEnvExampleFile(relativePath)) {
82
+ return 'example';
83
+ }
84
+ const normalized = normalizeRelativePath(relativePath);
85
+ const extension = path.extname(normalized).toLowerCase();
86
+ if (!SCAN_EXTENSIONS.includes(extension)) {
87
+ return null;
88
+ }
89
+ if (normalized.startsWith('.github/workflows/') || ['.yml', '.yaml'].includes(extension)) {
90
+ return 'ci';
91
+ }
92
+ if (['.md', '.mdx'].includes(extension)) {
93
+ return 'docs';
94
+ }
95
+ if (['.json', '.toml'].includes(extension) || normalized.startsWith('.mustflow/config/')) {
96
+ return 'config';
97
+ }
98
+ return 'code';
99
+ }
100
+ function normalizeTargetPath(projectRoot, targetPath) {
101
+ const absolutePath = path.resolve(process.cwd(), targetPath);
102
+ ensureInside(projectRoot, absolutePath);
103
+ return {
104
+ absolutePath,
105
+ relativePath: normalizeRelativePath(path.relative(projectRoot, absolutePath)),
106
+ };
107
+ }
108
+ function targetKind(absolutePath) {
109
+ if (!existsSync(absolutePath)) {
110
+ return { exists: false, kind: 'missing' };
111
+ }
112
+ const stats = lstatSync(absolutePath);
113
+ if (stats.isFile()) {
114
+ return { exists: true, kind: 'file' };
115
+ }
116
+ if (stats.isDirectory()) {
117
+ return { exists: true, kind: 'directory' };
118
+ }
119
+ return { exists: true, kind: 'other' };
120
+ }
121
+ function addCandidate(candidates, findings, issues, policy, candidate) {
122
+ if (candidates.has(candidate.relativePath)) {
123
+ return;
124
+ }
125
+ if (candidates.size >= policy.max_files) {
126
+ if (!findings.some((finding) => finding.code === 'secret_risk_max_files_exceeded')) {
127
+ const message = `Secret-risk scan matched more than ${policy.max_files} files; remaining files were skipped.`;
128
+ pushIssue(issues, message);
129
+ findings.push(makeFinding('secret_risk_max_files_exceeded', 'medium', candidate.relativePath, message));
130
+ }
131
+ return;
132
+ }
133
+ candidates.set(candidate.relativePath, candidate);
134
+ }
135
+ function collectFilesFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
136
+ const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
137
+ if (isIgnoredDirectory(relativeDirectory)) {
138
+ return;
139
+ }
140
+ let entries;
141
+ try {
142
+ ensureInsideWithoutSymlinks(projectRoot, absoluteDirectory);
143
+ entries = readdirSync(absoluteDirectory, { withFileTypes: true });
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ pushIssue(issues, `${relativeDirectory}: ${message}`);
148
+ findings.push(makeFinding('secret_risk_unreadable_path', 'high', relativeDirectory, message));
149
+ return;
150
+ }
151
+ for (const entry of entries) {
152
+ const absoluteEntry = path.join(absoluteDirectory, entry.name);
153
+ const relativeEntry = normalizeRelativePath(path.relative(projectRoot, absoluteEntry));
154
+ if (entry.isDirectory()) {
155
+ collectFilesFromDirectory(projectRoot, absoluteEntry, candidates, findings, issues, policy);
156
+ continue;
157
+ }
158
+ if (!entry.isFile()) {
159
+ continue;
160
+ }
161
+ const surface = surfaceForPath(relativeEntry);
162
+ if (surface || isSecretFile(relativeEntry)) {
163
+ addCandidate(candidates, findings, issues, policy, { absolutePath: absoluteEntry, relativePath: relativeEntry, surface: surface ?? 'config' });
164
+ }
165
+ }
166
+ }
167
+ function lineNumberAtIndex(text, index) {
168
+ let line = 1;
169
+ let offset = 0;
170
+ while (offset < index) {
171
+ if (text.charCodeAt(offset) === 10) {
172
+ line += 1;
173
+ }
174
+ offset += 1;
175
+ }
176
+ return line;
177
+ }
178
+ function looksLikePlaceholder(value) {
179
+ const normalized = value.trim().replace(/^['"`]|['"`]$/gu, '').toLowerCase();
180
+ if (normalized.length === 0) {
181
+ return true;
182
+ }
183
+ if (/^(?:example|sample|dummy|fake|test|changeme|change_me|placeholder|todo|null|undefined|your[_-].*|xxx+|\*+)$/u.test(normalized)) {
184
+ return true;
185
+ }
186
+ return ((normalized.startsWith('<') && normalized.endsWith('>')) ||
187
+ (normalized.startsWith('${') && normalized.endsWith('}')) ||
188
+ (normalized.startsWith('{{') && normalized.endsWith('}}')));
189
+ }
190
+ function entropyScore(value) {
191
+ const normalized = value.trim();
192
+ if (normalized.length === 0) {
193
+ return 0;
194
+ }
195
+ const counts = new Map();
196
+ for (const character of normalized) {
197
+ counts.set(character, (counts.get(character) ?? 0) + 1);
198
+ }
199
+ let entropy = 0;
200
+ for (const count of counts.values()) {
201
+ const probability = count / normalized.length;
202
+ entropy -= probability * Math.log2(probability);
203
+ }
204
+ return entropy;
205
+ }
206
+ function isSecretLikeName(name) {
207
+ return /(?:SECRET|TOKEN|PASSWORD|PASS|PRIVATE|CREDENTIAL|API[_-]?KEY|ACCESS[_-]?KEY|AUTH|SESSION|SIGNING|WEBHOOK)/iu.test(name);
208
+ }
209
+ function looksLikeSecretValue(value) {
210
+ const cleaned = value.trim().replace(/^['"`]|['"`]$/gu, '');
211
+ if (looksLikePlaceholder(cleaned)) {
212
+ return false;
213
+ }
214
+ if (/^(?:sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16})$/u.test(cleaned)) {
215
+ return true;
216
+ }
217
+ return cleaned.length >= 24 && entropyScore(cleaned) >= 3.5 && /[A-Za-z]/u.test(cleaned) && /\d/u.test(cleaned);
218
+ }
219
+ function addBoundedFinding(findings, issues, policy, finding) {
220
+ if (findings.length >= policy.max_findings) {
221
+ if (!findings.some((entry) => entry.code === 'secret_risk_max_findings_exceeded')) {
222
+ const message = `Secret-risk scan found more than ${policy.max_findings} findings; remaining findings were skipped.`;
223
+ pushIssue(issues, message);
224
+ findings.push(makeFinding('secret_risk_max_findings_exceeded', 'medium', finding.path, message));
225
+ }
226
+ return;
227
+ }
228
+ findings.push(finding);
229
+ }
230
+ function scanPattern(text, candidate, findings, issues, policy, pattern, create) {
231
+ for (const match of text.matchAll(pattern)) {
232
+ const line = lineNumberAtIndex(text, match.index ?? 0);
233
+ const finding = create(match, line);
234
+ if (finding) {
235
+ addBoundedFinding(findings, issues, policy, finding);
236
+ }
237
+ }
238
+ }
239
+ function scanPrivateKeyBlocks(text, candidate, findings, issues, policy) {
240
+ scanPattern(text, candidate, findings, issues, policy, /-----BEGIN [A-Z ]*PRIVATE KEY-----/gu, (match, line) => makeFinding('secret_risk_private_key_block', 'critical', candidate.relativePath, 'Private key block marker found.', {
241
+ line,
242
+ detector: 'private_key_block',
243
+ fingerprint: fingerprint(match[0]),
244
+ }));
245
+ }
246
+ function scanBearerTokens(text, candidate, findings, issues, policy) {
247
+ scanPattern(text, candidate, findings, issues, policy, /\bBearer\s+(?<value>[A-Za-z0-9._~+/=-]{24,})\b/gu, (match, line) => {
248
+ const value = match.groups?.value ?? '';
249
+ if (!looksLikeSecretValue(value)) {
250
+ return null;
251
+ }
252
+ return makeFinding('secret_risk_bearer_token', 'high', candidate.relativePath, 'Bearer token-like value found.', {
253
+ line,
254
+ detector: 'bearer_token',
255
+ fingerprint: fingerprint(value),
256
+ });
257
+ });
258
+ }
259
+ function scanProviderTokens(text, candidate, findings, issues, policy) {
260
+ const pattern = /\b(?<value>sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16})\b/gu;
261
+ scanPattern(text, candidate, findings, issues, policy, pattern, (match, line) => {
262
+ const value = match.groups?.value ?? '';
263
+ if (looksLikePlaceholder(value)) {
264
+ return null;
265
+ }
266
+ return makeFinding('secret_risk_provider_token', 'high', candidate.relativePath, 'Provider token-like value found.', {
267
+ line,
268
+ detector: 'provider_token',
269
+ fingerprint: fingerprint(value),
270
+ });
271
+ });
272
+ }
273
+ function scanAssignments(text, candidate, findings, issues, policy) {
274
+ const pattern = /(?:const|let|var|export\s+const|export\s+let|export\s+var)?\s*(?<name>[A-Za-z_][A-Za-z0-9_.-]*(?:SECRET|TOKEN|PASSWORD|PASS|PRIVATE|CREDENTIAL|API[_-]?KEY|ACCESS[_-]?KEY|AUTH|SESSION|SIGNING|WEBHOOK)[A-Za-z0-9_.-]*)\s*[:=]\s*["'`](?<value>[^"'`\r\n]{8,})["'`]/giu;
275
+ scanPattern(text, candidate, findings, issues, policy, pattern, (match, line) => {
276
+ const name = match.groups?.name ?? '';
277
+ const value = match.groups?.value ?? '';
278
+ if (!isSecretLikeName(name) || !looksLikeSecretValue(value)) {
279
+ return null;
280
+ }
281
+ return makeFinding('secret_risk_generic_assignment', 'high', candidate.relativePath, 'Secret-like assignment found.', {
282
+ line,
283
+ detector: 'generic_assignment',
284
+ fingerprint: fingerprint(`${name}:${value}`),
285
+ });
286
+ });
287
+ }
288
+ function scanEnvExample(text, candidate, findings, issues, policy) {
289
+ const lines = text.split(/\r?\n/u);
290
+ for (const [index, line] of lines.entries()) {
291
+ const match = /^\s*(?:export\s+)?(?<name>[A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(?<value>.+?)\s*$/u.exec(line);
292
+ if (!match?.groups) {
293
+ continue;
294
+ }
295
+ const { name, value } = match.groups;
296
+ if (!isSecretLikeName(name) || !looksLikeSecretValue(value)) {
297
+ continue;
298
+ }
299
+ addBoundedFinding(findings, issues, policy, makeFinding('secret_risk_realistic_env_example', 'medium', candidate.relativePath, 'Env example contains a realistic secret-like value.', {
300
+ line: index + 1,
301
+ detector: 'realistic_env_example',
302
+ fingerprint: fingerprint(`${name}:${value}`),
303
+ }));
304
+ }
305
+ }
306
+ function inspectCandidate(projectRoot, candidate, policy, findings, issues) {
307
+ if (isSecretFile(candidate.relativePath)) {
308
+ addBoundedFinding(findings, issues, policy, makeFinding('secret_risk_secret_file_skipped', 'low', candidate.relativePath, `${candidate.relativePath} was skipped to avoid reading real secret values.`, {
309
+ detector: 'secret_file_skipped',
310
+ }));
311
+ return;
312
+ }
313
+ let text;
314
+ try {
315
+ text = readFileInsideWithoutSymlinks(projectRoot, candidate.absolutePath, { maxBytes: policy.max_file_bytes }).toString('utf8');
316
+ }
317
+ catch (error) {
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ pushIssue(issues, `${candidate.relativePath}: ${message}`);
320
+ findings.push(makeFinding('secret_risk_unreadable_path', 'high', candidate.relativePath, message));
321
+ return;
322
+ }
323
+ scanPrivateKeyBlocks(text, candidate, findings, issues, policy);
324
+ scanBearerTokens(text, candidate, findings, issues, policy);
325
+ scanProviderTokens(text, candidate, findings, issues, policy);
326
+ scanAssignments(text, candidate, findings, issues, policy);
327
+ if (candidate.surface === 'example') {
328
+ scanEnvExample(text, candidate, findings, issues, policy);
329
+ }
330
+ }
331
+ function secretRiskStatus(findings) {
332
+ if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
333
+ return 'error';
334
+ }
335
+ if (findings.some((finding) => ['medium', 'high', 'critical'].includes(finding.severity))) {
336
+ return 'failed';
337
+ }
338
+ return 'passed';
339
+ }
340
+ function summarizeSecretRisk(targets, fileCount, findings) {
341
+ return {
342
+ target_count: targets.length,
343
+ file_count: fileCount,
344
+ finding_count: findings.length,
345
+ skipped_secret_file_count: findings.filter((finding) => finding.code === 'secret_risk_secret_file_skipped').length,
346
+ high_or_critical_count: findings.filter((finding) => ['high', 'critical'].includes(finding.severity)).length,
347
+ };
348
+ }
349
+ function createInputHash(policy, targets, findings, issues) {
350
+ return sha256Tagged(JSON.stringify({
351
+ policy,
352
+ targets,
353
+ findings: findings.map((finding) => ({
354
+ code: finding.code,
355
+ path: finding.path,
356
+ line: finding.line,
357
+ detector: finding.detector,
358
+ fingerprint: finding.fingerprint,
359
+ })),
360
+ issues,
361
+ }));
362
+ }
363
+ export function inspectSecretRiskScan(projectRoot, options = {}) {
364
+ const root = path.resolve(projectRoot);
365
+ const policy = {
366
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
367
+ max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
368
+ max_findings: options.maxFindings ?? DEFAULT_MAX_FINDINGS,
369
+ extensions: [...SCAN_EXTENSIONS],
370
+ skipped_secret_names: [...SECRET_FILE_NAMES],
371
+ ignored_directories: [...IGNORED_DIRECTORIES],
372
+ };
373
+ const targetInputs = options.paths && options.paths.length > 0 ? options.paths : ['.'];
374
+ const targets = [];
375
+ const candidates = new Map();
376
+ const findings = [];
377
+ const issues = [];
378
+ for (const targetPath of targetInputs) {
379
+ let absolutePath;
380
+ let relativePath;
381
+ try {
382
+ const normalized = normalizeTargetPath(root, targetPath);
383
+ absolutePath = normalized.absolutePath;
384
+ relativePath = normalized.relativePath;
385
+ ensureInsideWithoutSymlinks(root, absolutePath, { allowMissingLeaf: true });
386
+ }
387
+ catch (error) {
388
+ const message = error instanceof Error ? error.message : String(error);
389
+ pushIssue(issues, message);
390
+ targets.push({ input: targetPath, path: targetPath, exists: null, kind: 'unknown' });
391
+ findings.push(makeFinding('secret_risk_path_outside_root', 'high', targetPath, message));
392
+ continue;
393
+ }
394
+ let existence;
395
+ try {
396
+ existence = targetKind(absolutePath);
397
+ }
398
+ catch (error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ pushIssue(issues, `${relativePath}: ${message}`);
401
+ targets.push({ input: targetPath, path: relativePath, exists: null, kind: 'unknown' });
402
+ findings.push(makeFinding('secret_risk_unreadable_path', 'high', relativePath, message));
403
+ continue;
404
+ }
405
+ targets.push({ input: targetPath, path: relativePath, exists: existence.exists, kind: existence.kind });
406
+ if (existence.kind === 'file') {
407
+ const surface = surfaceForPath(relativePath);
408
+ if (surface || isSecretFile(relativePath)) {
409
+ addCandidate(candidates, findings, issues, policy, { absolutePath, relativePath, surface: surface ?? 'config' });
410
+ }
411
+ }
412
+ else if (existence.kind === 'directory') {
413
+ collectFilesFromDirectory(root, absolutePath, candidates, findings, issues, policy);
414
+ }
415
+ }
416
+ for (const candidate of candidates.values()) {
417
+ inspectCandidate(root, candidate, policy, findings, issues);
418
+ }
419
+ const status = secretRiskStatus(findings);
420
+ const truncated = findings.some((finding) => ['secret_risk_max_files_exceeded', 'secret_risk_max_findings_exceeded'].includes(finding.code));
421
+ const summary = summarizeSecretRisk(targets, candidates.size, findings);
422
+ return {
423
+ schema_version: '1',
424
+ command: 'script-pack',
425
+ pack_id: SECRET_RISK_SCAN_PACK_ID,
426
+ script_id: SECRET_RISK_SCAN_SCRIPT_ID,
427
+ script_ref: SECRET_RISK_SCAN_SCRIPT_REF,
428
+ action: 'scan',
429
+ status,
430
+ ok: status === 'passed',
431
+ mustflow_root: root,
432
+ policy,
433
+ input_hash: createInputHash(policy, targets, findings, issues),
434
+ targets,
435
+ summary,
436
+ truncated,
437
+ findings,
438
+ issues,
439
+ };
440
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.84.0",
3
+ "version": "2.85.4",
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
@@ -69,6 +69,14 @@ Current schemas:
69
69
  file hashes, language metadata, symbol names, declaration kinds, line ranges, signatures, export
70
70
  flags, source-anchor navigation metadata, static return metadata, and stable input-limit finding
71
71
  codes
72
+ - `dependency-graph-report.schema.json`: output of
73
+ `mf script-pack run code/dependency-graph scan <path...> --json`, containing bounded relative
74
+ TypeScript and JavaScript import graph nodes, edges, cycle hints, target flags, depth, importer
75
+ counts, policy limits, and stable input-limit finding codes
76
+ - `change-impact-report.schema.json`: output of
77
+ `mf script-pack run code/change-impact analyze [path...] --json`, containing git-diff changed
78
+ files, surface classifications, bounded impact candidates, optional script-pack hints,
79
+ verification intent hints, policy limits, and stable git or input-limit finding codes
72
80
  - `code-symbol-read-report.schema.json`: output of
73
81
  `mf script-pack run code/symbol-read read <path> --start-line <line> --json`, containing a
74
82
  focused source snippet selected by source anchor, outline symbol line, or explicit line range with
@@ -91,6 +99,14 @@ Current schemas:
91
99
  `mf script-pack run repo/config-chain inspect <path...> --json`, containing nearby package,
92
100
  TypeScript, ESLint, Prettier, Vite, Vitest, Tailwind, Jest, Playwright, and mustflow config files
93
101
  with static inheritance, reference, workspace, dynamic-config, path, and content-hash metadata
102
+ - `env-contract-report.schema.json`: output of
103
+ `mf script-pack run repo/env-contract scan [path...] --json`, containing code, CI, docs, config,
104
+ and env-example references for environment-variable contract drift without reading secret env files
105
+ or printing env values
106
+ - `secret-risk-scan-report.schema.json`: output of
107
+ `mf script-pack run repo/secret-risk-scan scan [path...] --json`, containing plausible
108
+ hardcoded-secret findings with detector names, paths, line numbers, and redacted fingerprints
109
+ without printing secret values
94
110
  - `text-budget-report.schema.json`: output of
95
111
  `mf script-pack run core/text-budget check <path...> --json`, containing
96
112
  exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field
@@ -0,0 +1,150 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://mustflow.github.io/schemas/change-impact-report.schema.json",
4
+ "title": "mustflow change-impact 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
+ "policy",
18
+ "input_hash",
19
+ "changed_files",
20
+ "impacts",
21
+ "script_hints",
22
+ "verification_hints",
23
+ "truncated",
24
+ "findings",
25
+ "issues"
26
+ ],
27
+ "properties": {
28
+ "schema_version": { "const": "1" },
29
+ "command": { "const": "script-pack" },
30
+ "pack_id": { "const": "code" },
31
+ "script_id": { "const": "change-impact" },
32
+ "script_ref": { "const": "code/change-impact" },
33
+ "action": { "const": "analyze" },
34
+ "status": { "enum": ["passed", "failed", "error"] },
35
+ "ok": { "type": "boolean" },
36
+ "mustflow_root": { "type": "string" },
37
+ "policy": { "$ref": "#/$defs/policy" },
38
+ "input_hash": { "$ref": "#/$defs/sha256" },
39
+ "changed_files": { "type": "array", "items": { "$ref": "#/$defs/changedFile" } },
40
+ "impacts": { "type": "array", "items": { "$ref": "#/$defs/impact" } },
41
+ "script_hints": { "type": "array", "items": { "$ref": "#/$defs/scriptHint" } },
42
+ "verification_hints": { "type": "array", "items": { "$ref": "#/$defs/verificationHint" } },
43
+ "truncated": { "type": "boolean" },
44
+ "findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } },
45
+ "issues": { "type": "array", "items": { "type": "string" } }
46
+ },
47
+ "$defs": {
48
+ "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
49
+ "surface": {
50
+ "enum": ["source", "test", "docs", "schema", "config", "package", "template", "workflow", "i18n", "unknown"]
51
+ },
52
+ "policy": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "required": [
56
+ "base_ref",
57
+ "head_ref",
58
+ "compare_worktree",
59
+ "max_files",
60
+ "max_impacts",
61
+ "max_file_bytes",
62
+ "path_filters"
63
+ ],
64
+ "properties": {
65
+ "base_ref": { "type": "string" },
66
+ "head_ref": { "type": ["string", "null"] },
67
+ "compare_worktree": { "type": "boolean" },
68
+ "max_files": { "type": "integer", "minimum": 1 },
69
+ "max_impacts": { "type": "integer", "minimum": 1 },
70
+ "max_file_bytes": { "type": "integer", "minimum": 1 },
71
+ "path_filters": { "type": "array", "items": { "type": "string" } }
72
+ }
73
+ },
74
+ "changedFile": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["path", "previous_path", "status", "surface"],
78
+ "properties": {
79
+ "path": { "type": "string" },
80
+ "previous_path": { "type": ["string", "null"] },
81
+ "status": { "enum": ["added", "modified", "deleted", "renamed", "type_changed", "untracked"] },
82
+ "surface": { "$ref": "#/$defs/surface" }
83
+ }
84
+ },
85
+ "impact": {
86
+ "type": "object",
87
+ "additionalProperties": false,
88
+ "required": ["path", "exists", "surface", "relationship", "confidence", "reason", "source_path"],
89
+ "properties": {
90
+ "path": { "type": "string" },
91
+ "exists": { "type": "boolean" },
92
+ "surface": { "$ref": "#/$defs/surface" },
93
+ "relationship": {
94
+ "enum": [
95
+ "changed_file",
96
+ "imports_changed_file",
97
+ "sibling_test",
98
+ "sibling_source",
99
+ "contract_doc",
100
+ "contract_test",
101
+ "package_surface",
102
+ "workflow_surface"
103
+ ]
104
+ },
105
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
106
+ "reason": { "type": "string" },
107
+ "source_path": { "type": "string" }
108
+ }
109
+ },
110
+ "scriptHint": {
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "required": ["script_ref", "command", "reason", "confidence"],
114
+ "properties": {
115
+ "script_ref": { "type": "string" },
116
+ "command": { "type": "string" },
117
+ "reason": { "type": "string" },
118
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 }
119
+ }
120
+ },
121
+ "verificationHint": {
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "required": ["intent", "reason", "confidence"],
125
+ "properties": {
126
+ "intent": { "type": "string" },
127
+ "reason": { "type": "string" },
128
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 }
129
+ }
130
+ },
131
+ "finding": {
132
+ "type": "object",
133
+ "additionalProperties": false,
134
+ "required": ["code", "severity", "message", "path"],
135
+ "properties": {
136
+ "code": {
137
+ "enum": [
138
+ "change_impact_git_unavailable",
139
+ "change_impact_invalid_ref",
140
+ "change_impact_max_files_exceeded",
141
+ "change_impact_max_impacts_exceeded"
142
+ ]
143
+ },
144
+ "severity": { "enum": ["low", "medium", "high", "critical"] },
145
+ "message": { "type": "string" },
146
+ "path": { "type": "string" }
147
+ }
148
+ }
149
+ }
150
+ }