mustflow 2.75.2 → 2.85.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +40 -3
  2. package/dist/cli/commands/docs.js +86 -2
  3. package/dist/cli/commands/script-pack.js +9 -0
  4. package/dist/cli/i18n/en.js +180 -2
  5. package/dist/cli/i18n/es.js +180 -2
  6. package/dist/cli/i18n/fr.js +180 -2
  7. package/dist/cli/i18n/hi.js +180 -2
  8. package/dist/cli/i18n/ko.js +180 -2
  9. package/dist/cli/i18n/zh.js +180 -2
  10. package/dist/cli/lib/repo-map.js +27 -6
  11. package/dist/cli/lib/run-root-trust.js +15 -1
  12. package/dist/cli/lib/script-pack-registry.js +275 -6
  13. package/dist/cli/lib/validation/index.js +2 -2
  14. package/dist/cli/lib/validation/primitives.js +4 -1
  15. package/dist/cli/script-packs/code-change-impact.js +172 -0
  16. package/dist/cli/script-packs/code-dependency-graph.js +181 -0
  17. package/dist/cli/script-packs/code-export-diff.js +160 -0
  18. package/dist/cli/script-packs/code-outline.js +33 -5
  19. package/dist/cli/script-packs/code-route-outline.js +155 -0
  20. package/dist/cli/script-packs/docs-reference-drift.js +150 -0
  21. package/dist/cli/script-packs/repo-config-chain.js +163 -0
  22. package/dist/cli/script-packs/repo-env-contract.js +156 -0
  23. package/dist/cli/script-packs/repo-related-files.js +161 -0
  24. package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
  25. package/dist/core/change-impact.js +383 -0
  26. package/dist/core/change-verification.js +32 -5
  27. package/dist/core/code-outline.js +460 -79
  28. package/dist/core/config-chain.js +595 -0
  29. package/dist/core/config-loading.js +121 -4
  30. package/dist/core/dependency-graph.js +490 -0
  31. package/dist/core/env-contract.js +450 -0
  32. package/dist/core/export-diff.js +359 -0
  33. package/dist/core/line-endings.js +26 -13
  34. package/dist/core/public-json-contracts.js +126 -0
  35. package/dist/core/reference-drift.js +388 -0
  36. package/dist/core/related-files.js +493 -0
  37. package/dist/core/route-outline.js +964 -0
  38. package/dist/core/script-pack-suggestions.js +131 -5
  39. package/dist/core/secret-risk-scan.js +440 -0
  40. package/dist/core/source-anchors.js +13 -1
  41. package/package.json +1 -1
  42. package/schemas/README.md +44 -6
  43. package/schemas/change-impact-report.schema.json +150 -0
  44. package/schemas/code-outline-report.schema.json +1 -1
  45. package/schemas/code-symbol-read-report.schema.json +64 -4
  46. package/schemas/commands.schema.json +12 -0
  47. package/schemas/config-chain-report.schema.json +187 -0
  48. package/schemas/dependency-graph-report.schema.json +149 -0
  49. package/schemas/env-contract-report.schema.json +203 -0
  50. package/schemas/export-diff-report.schema.json +220 -0
  51. package/schemas/reference-drift-report.schema.json +166 -0
  52. package/schemas/related-files-report.schema.json +145 -0
  53. package/schemas/route-outline-report.schema.json +200 -0
  54. package/schemas/secret-risk-scan-report.schema.json +152 -0
  55. package/templates/default/common/.mustflow/config/commands.toml +21 -0
  56. package/templates/default/i18n.toml +21 -9
  57. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +1 -1
  58. package/templates/default/locales/en/.mustflow/skills/INDEX.md +8 -2
  59. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
  60. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
  61. package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +146 -0
  62. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
  63. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
  64. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
  65. package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
  66. package/templates/default/locales/en/.mustflow/skills/routes.toml +21 -9
  67. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
  68. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +314 -0
  69. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +13 -10
  70. package/templates/default/manifest.toml +15 -1
@@ -0,0 +1,450 @@
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 ENV_CONTRACT_PACK_ID = 'repo';
6
+ export const ENV_CONTRACT_SCRIPT_ID = 'env-contract';
7
+ export const ENV_CONTRACT_SCRIPT_REF = `${ENV_CONTRACT_PACK_ID}/${ENV_CONTRACT_SCRIPT_ID}`;
8
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
9
+ const DEFAULT_MAX_FILES = 1000;
10
+ const DEFAULT_MAX_KEYS = 300;
11
+ const MAX_ISSUES = 50;
12
+ const ENV_KEY_PATTERN = '[A-Za-z_][A-Za-z0-9_]*';
13
+ const SCAN_EXTENSIONS = [
14
+ '.ts',
15
+ '.tsx',
16
+ '.mts',
17
+ '.cts',
18
+ '.js',
19
+ '.jsx',
20
+ '.mjs',
21
+ '.cjs',
22
+ '.json',
23
+ '.toml',
24
+ '.yml',
25
+ '.yaml',
26
+ '.md',
27
+ '.mdx',
28
+ ];
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 SECRET_ENV_NAMES = ['.env', '.env.local', '.env.production', '.env.development', '.dev.vars'];
39
+ const IGNORED_DIRECTORIES = [
40
+ '.git',
41
+ '.mustflow/cache',
42
+ '.mustflow/state',
43
+ 'node_modules',
44
+ 'dist',
45
+ 'build',
46
+ 'coverage',
47
+ '.next',
48
+ '.turbo',
49
+ ];
50
+ const ERROR_CODES = new Set([
51
+ 'env_contract_path_outside_root',
52
+ 'env_contract_unreadable_path',
53
+ ]);
54
+ function normalizeRelativePath(value) {
55
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '') || '.';
56
+ }
57
+ function sha256Tagged(value) {
58
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
59
+ }
60
+ function makeFinding(code, severity, pathValue, message, key) {
61
+ return key ? { code, severity, path: pathValue, message, key } : { code, severity, path: pathValue, message };
62
+ }
63
+ function pushIssue(issues, issue) {
64
+ if (issues.length < MAX_ISSUES) {
65
+ issues.push(issue);
66
+ }
67
+ }
68
+ function isIgnoredDirectory(relativePath) {
69
+ const normalized = normalizeRelativePath(relativePath);
70
+ return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
71
+ }
72
+ function isEnvExampleFile(relativePath) {
73
+ const name = path.basename(relativePath).toLowerCase();
74
+ return ENV_EXAMPLE_NAMES.includes(name);
75
+ }
76
+ function isSecretEnvFile(relativePath) {
77
+ const name = path.basename(relativePath).toLowerCase();
78
+ return SECRET_ENV_NAMES.includes(name);
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 === 'env_contract_max_files_exceeded')) {
127
+ const message = `Env-contract matched more than ${policy.max_files} files; remaining files were skipped.`;
128
+ pushIssue(issues, message);
129
+ findings.push(makeFinding('env_contract_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('env_contract_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) {
163
+ addCandidate(candidates, findings, issues, policy, { absolutePath: absoluteEntry, relativePath: relativeEntry, surface });
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 addReference(references, seen, key, candidate, line, kind) {
179
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) {
180
+ return;
181
+ }
182
+ const reference = { key, path: candidate.relativePath, line, surface: candidate.surface, kind };
183
+ const dedupeKey = `${reference.key}\0${reference.path}\0${reference.line}\0${reference.kind}`;
184
+ if (seen.has(dedupeKey)) {
185
+ return;
186
+ }
187
+ seen.add(dedupeKey);
188
+ references.push(reference);
189
+ }
190
+ function collectRegexReferences(text, candidate, references, seen, pattern, kind) {
191
+ for (const match of text.matchAll(pattern)) {
192
+ const key = match.groups?.key;
193
+ if (key) {
194
+ addReference(references, seen, key, candidate, lineNumberAtIndex(text, match.index ?? 0), kind);
195
+ }
196
+ }
197
+ }
198
+ function collectDestructuredProcessEnv(text, candidate, references, seen) {
199
+ const pattern = /\{(?<body>[^}]+)\}\s*=\s*process\.env\b/gu;
200
+ for (const match of text.matchAll(pattern)) {
201
+ const body = match.groups?.body ?? '';
202
+ for (const entry of body.split(',')) {
203
+ const key = new RegExp(`^\\s*(?<key>${ENV_KEY_PATTERN})(?:\\s*:|\\s*$)`, 'u').exec(entry)?.groups?.key;
204
+ if (key) {
205
+ addReference(references, seen, key, candidate, lineNumberAtIndex(text, match.index ?? 0), 'process_env_destructure');
206
+ }
207
+ }
208
+ }
209
+ }
210
+ function collectEnvExampleReferences(text, candidate, references, seen) {
211
+ const lines = text.split(/\r?\n/u);
212
+ for (const [index, line] of lines.entries()) {
213
+ const match = new RegExp(`^\\s*(?:export\\s+)?(?<key>${ENV_KEY_PATTERN})\\s*=`, 'u').exec(line);
214
+ if (match?.groups?.key) {
215
+ addReference(references, seen, match.groups.key, candidate, index + 1, 'env_example');
216
+ }
217
+ }
218
+ }
219
+ function collectCiReferences(text, candidate, references, seen) {
220
+ collectRegexReferences(text, candidate, references, seen, /\$\{\{\s*secrets\.(?<key>[A-Z][A-Z0-9_]+)\s*\}\}/gu, 'ci_secret');
221
+ collectRegexReferences(text, candidate, references, seen, /\$\{\{\s*vars\.(?<key>[A-Z][A-Z0-9_]+)\s*\}\}/gu, 'ci_var');
222
+ const lines = text.split(/\r?\n/u);
223
+ for (const [index, line] of lines.entries()) {
224
+ const match = /^\s{2,}(?<key>[A-Z][A-Z0-9_]{2,})\s*:/u.exec(line);
225
+ if (match?.groups?.key) {
226
+ addReference(references, seen, match.groups.key, candidate, index + 1, 'ci_env');
227
+ }
228
+ }
229
+ }
230
+ function collectDocumentedReferences(text, candidate, references, seen) {
231
+ const pattern = /`(?<key>[A-Z][A-Z0-9_]{2,})`|\b(?<bare>[A-Z][A-Z0-9_]{3,})\b/gu;
232
+ for (const match of text.matchAll(pattern)) {
233
+ const key = match.groups?.key ?? match.groups?.bare;
234
+ if (key && key.includes('_')) {
235
+ addReference(references, seen, key, candidate, lineNumberAtIndex(text, match.index ?? 0), 'documented');
236
+ }
237
+ }
238
+ }
239
+ function collectCodeReferences(text, candidate, references, seen) {
240
+ collectRegexReferences(text, candidate, references, seen, /\bprocess\.env\.(?<key>[A-Za-z_][A-Za-z0-9_]*)\b/gu, 'process_env_dot');
241
+ collectRegexReferences(text, candidate, references, seen, /\bprocess\.env\s*\[\s*['"](?<key>[A-Za-z_][A-Za-z0-9_]*)['"]\s*\]/gu, 'process_env_bracket');
242
+ collectRegexReferences(text, candidate, references, seen, /\bBun\.env\.(?<key>[A-Za-z_][A-Za-z0-9_]*)\b/gu, 'bun_env_dot');
243
+ collectRegexReferences(text, candidate, references, seen, /\bimport\.meta\.env\.(?<key>[A-Za-z_][A-Za-z0-9_]*)\b/gu, 'import_meta_env');
244
+ collectDestructuredProcessEnv(text, candidate, references, seen);
245
+ }
246
+ function collectReferencesForFile(text, candidate) {
247
+ const references = [];
248
+ const seen = new Set();
249
+ if (candidate.surface === 'example') {
250
+ collectEnvExampleReferences(text, candidate, references, seen);
251
+ }
252
+ else if (candidate.surface === 'ci') {
253
+ collectCiReferences(text, candidate, references, seen);
254
+ collectCodeReferences(text, candidate, references, seen);
255
+ }
256
+ else if (candidate.surface === 'docs') {
257
+ collectDocumentedReferences(text, candidate, references, seen);
258
+ }
259
+ else {
260
+ collectCodeReferences(text, candidate, references, seen);
261
+ }
262
+ return references;
263
+ }
264
+ function isSecretLikeKey(key) {
265
+ return /(?:SECRET|TOKEN|PASSWORD|PASS|PRIVATE|CREDENTIAL|API_KEY|ACCESS_KEY|AUTH|SESSION|SIGNING|WEBHOOK)/u.test(key);
266
+ }
267
+ function isPublicLikeKey(key) {
268
+ return /^(?:PUBLIC_|VITE_|NEXT_PUBLIC_|NUXT_PUBLIC_|EXPO_PUBLIC_)/u.test(key);
269
+ }
270
+ function keyFromReferences(key, sources) {
271
+ const usedInCode = sources.some((source) => ['process_env_dot', 'process_env_bracket', 'process_env_destructure', 'bun_env_dot', 'import_meta_env'].includes(source.kind));
272
+ const declaredInExample = sources.some((source) => source.kind === 'env_example');
273
+ const referencedInCi = sources.some((source) => ['ci_secret', 'ci_var', 'ci_env'].includes(source.kind));
274
+ const documented = sources.some((source) => source.kind === 'documented');
275
+ return {
276
+ key,
277
+ used_in_code: usedInCode,
278
+ declared_in_example: declaredInExample,
279
+ referenced_in_ci: referencedInCi,
280
+ documented,
281
+ secret_like: isSecretLikeKey(key),
282
+ public_like: isPublicLikeKey(key),
283
+ source_count: sources.length,
284
+ sources,
285
+ };
286
+ }
287
+ function sortReference(left, right) {
288
+ return left.path.localeCompare(right.path) || left.line - right.line || left.kind.localeCompare(right.kind);
289
+ }
290
+ function buildKeys(references, findings, issues, policy) {
291
+ const byKey = new Map();
292
+ for (const reference of references) {
293
+ const entries = byKey.get(reference.key) ?? [];
294
+ entries.push(reference);
295
+ byKey.set(reference.key, entries);
296
+ }
297
+ const keys = [];
298
+ for (const [key, sources] of [...byKey.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
299
+ if (keys.length >= policy.max_keys) {
300
+ if (!findings.some((finding) => finding.code === 'env_contract_max_keys_exceeded')) {
301
+ const message = `Env-contract found more than ${policy.max_keys} keys; remaining keys were skipped.`;
302
+ pushIssue(issues, message);
303
+ findings.push(makeFinding('env_contract_max_keys_exceeded', 'medium', '.', message));
304
+ }
305
+ break;
306
+ }
307
+ keys.push(keyFromReferences(key, sources.sort(sortReference)));
308
+ }
309
+ for (const entry of keys) {
310
+ const firstPath = entry.sources[0]?.path ?? '.';
311
+ if (entry.used_in_code && !entry.declared_in_example && !entry.documented) {
312
+ findings.push(makeFinding('env_contract_missing_example', 'medium', firstPath, `${entry.key} is used in code but is not declared in env examples or docs.`, entry.key));
313
+ }
314
+ if (entry.declared_in_example && !entry.used_in_code && !entry.documented && !entry.referenced_in_ci) {
315
+ findings.push(makeFinding('env_contract_unused_example', 'low', firstPath, `${entry.key} is declared only in env examples.`, entry.key));
316
+ }
317
+ if (entry.secret_like && entry.public_like) {
318
+ findings.push(makeFinding('env_contract_secret_like_public_name', 'high', firstPath, `${entry.key} looks secret-like but uses a public env prefix.`, entry.key));
319
+ }
320
+ }
321
+ return keys;
322
+ }
323
+ function envContractStatus(findings) {
324
+ if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
325
+ return 'error';
326
+ }
327
+ if (findings.some((finding) => ['medium', 'high', 'critical'].includes(finding.severity))) {
328
+ return 'failed';
329
+ }
330
+ return 'passed';
331
+ }
332
+ function createInputHash(policy, targets, keys, findings, issues) {
333
+ return sha256Tagged(JSON.stringify({
334
+ policy,
335
+ targets,
336
+ keys: keys.map((entry) => ({
337
+ key: entry.key,
338
+ source_count: entry.source_count,
339
+ flags: [entry.used_in_code, entry.declared_in_example, entry.referenced_in_ci, entry.documented],
340
+ })),
341
+ findings: findings.map((finding) => ({ code: finding.code, path: finding.path, key: finding.key })),
342
+ issues,
343
+ }));
344
+ }
345
+ function summarizeEnvContract(targets, fileCount, keys) {
346
+ return {
347
+ target_count: targets.length,
348
+ file_count: fileCount,
349
+ key_count: keys.length,
350
+ code_key_count: keys.filter((entry) => entry.used_in_code).length,
351
+ example_key_count: keys.filter((entry) => entry.declared_in_example).length,
352
+ documented_key_count: keys.filter((entry) => entry.documented).length,
353
+ ci_key_count: keys.filter((entry) => entry.referenced_in_ci).length,
354
+ };
355
+ }
356
+ function inspectEnvCandidate(projectRoot, candidate, policy, findings, issues) {
357
+ if (isSecretEnvFile(candidate.relativePath)) {
358
+ findings.push(makeFinding('env_contract_secret_file_skipped', 'low', candidate.relativePath, `${candidate.relativePath} was skipped to avoid reading real env values.`));
359
+ return [];
360
+ }
361
+ try {
362
+ const buffer = readFileInsideWithoutSymlinks(projectRoot, candidate.absolutePath, { maxBytes: policy.max_file_bytes });
363
+ return collectReferencesForFile(buffer.toString('utf8'), candidate);
364
+ }
365
+ catch (error) {
366
+ const message = error instanceof Error ? error.message : String(error);
367
+ pushIssue(issues, `${candidate.relativePath}: ${message}`);
368
+ findings.push(makeFinding('env_contract_unreadable_path', 'high', candidate.relativePath, message));
369
+ return [];
370
+ }
371
+ }
372
+ export function inspectEnvContract(projectRoot, options = {}) {
373
+ const root = path.resolve(projectRoot);
374
+ const policy = {
375
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
376
+ max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
377
+ max_keys: options.maxKeys ?? DEFAULT_MAX_KEYS,
378
+ extensions: [...SCAN_EXTENSIONS],
379
+ env_example_names: [...ENV_EXAMPLE_NAMES],
380
+ skipped_secret_names: [...SECRET_ENV_NAMES],
381
+ ignored_directories: [...IGNORED_DIRECTORIES],
382
+ };
383
+ const targetInputs = options.paths && options.paths.length > 0 ? options.paths : ['.'];
384
+ const targets = [];
385
+ const candidates = new Map();
386
+ const findings = [];
387
+ const issues = [];
388
+ for (const targetPath of targetInputs) {
389
+ let absolutePath;
390
+ let relativePath;
391
+ try {
392
+ const normalized = normalizeTargetPath(root, targetPath);
393
+ absolutePath = normalized.absolutePath;
394
+ relativePath = normalized.relativePath;
395
+ ensureInsideWithoutSymlinks(root, absolutePath, { allowMissingLeaf: true });
396
+ }
397
+ catch (error) {
398
+ const message = error instanceof Error ? error.message : String(error);
399
+ pushIssue(issues, message);
400
+ targets.push({ input: targetPath, path: targetPath, exists: null, kind: 'unknown' });
401
+ findings.push(makeFinding('env_contract_path_outside_root', 'high', targetPath, message));
402
+ continue;
403
+ }
404
+ let existence;
405
+ try {
406
+ existence = targetKind(absolutePath);
407
+ }
408
+ catch (error) {
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ pushIssue(issues, `${relativePath}: ${message}`);
411
+ targets.push({ input: targetPath, path: relativePath, exists: null, kind: 'unknown' });
412
+ findings.push(makeFinding('env_contract_unreadable_path', 'high', relativePath, message));
413
+ continue;
414
+ }
415
+ targets.push({ input: targetPath, path: relativePath, exists: existence.exists, kind: existence.kind });
416
+ if (existence.kind === 'file') {
417
+ const surface = surfaceForPath(relativePath);
418
+ if (surface || isSecretEnvFile(relativePath)) {
419
+ addCandidate(candidates, findings, issues, policy, { absolutePath, relativePath, surface: surface ?? 'example' });
420
+ }
421
+ }
422
+ else if (existence.kind === 'directory') {
423
+ collectFilesFromDirectory(root, absolutePath, candidates, findings, issues, policy);
424
+ }
425
+ }
426
+ const references = [...candidates.values()].flatMap((candidate) => inspectEnvCandidate(root, candidate, policy, findings, issues));
427
+ const keys = buildKeys(references, findings, issues, policy);
428
+ const status = envContractStatus(findings);
429
+ const truncated = findings.some((finding) => ['env_contract_max_files_exceeded', 'env_contract_max_keys_exceeded'].includes(finding.code));
430
+ const summary = summarizeEnvContract(targets, candidates.size, keys);
431
+ return {
432
+ schema_version: '1',
433
+ command: 'script-pack',
434
+ pack_id: ENV_CONTRACT_PACK_ID,
435
+ script_id: ENV_CONTRACT_SCRIPT_ID,
436
+ script_ref: ENV_CONTRACT_SCRIPT_REF,
437
+ action: 'scan',
438
+ status,
439
+ ok: status === 'passed',
440
+ mustflow_root: root,
441
+ policy,
442
+ input_hash: createInputHash(policy, targets, keys, findings, issues),
443
+ targets,
444
+ summary,
445
+ keys,
446
+ truncated,
447
+ findings,
448
+ issues,
449
+ };
450
+ }