mustflow 2.84.0 → 2.99.0

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 (105) hide show
  1. package/README.md +11 -2
  2. package/dist/cli/commands/script-pack.js +14 -0
  3. package/dist/cli/i18n/en.js +262 -0
  4. package/dist/cli/i18n/es.js +262 -0
  5. package/dist/cli/i18n/fr.js +262 -0
  6. package/dist/cli/i18n/hi.js +262 -0
  7. package/dist/cli/i18n/ko.js +262 -0
  8. package/dist/cli/i18n/zh.js +262 -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 +397 -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 +178 -0
  15. package/dist/cli/script-packs/code-dependency-graph.js +181 -0
  16. package/dist/cli/script-packs/code-import-cycle.js +193 -0
  17. package/dist/cli/script-packs/docs-link-integrity.js +145 -0
  18. package/dist/cli/script-packs/repo-approval-gate.js +100 -0
  19. package/dist/cli/script-packs/repo-env-contract.js +156 -0
  20. package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
  21. package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
  22. package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
  23. package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
  24. package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
  25. package/dist/cli/script-packs/repo-version-source.js +92 -0
  26. package/dist/cli/script-packs/test-performance-report.js +247 -0
  27. package/dist/cli/script-packs/test-regression-selector.js +167 -0
  28. package/dist/core/change-impact.js +355 -0
  29. package/dist/core/change-surface-classification.js +198 -0
  30. package/dist/core/change-verification.js +32 -5
  31. package/dist/core/config-loading.js +121 -4
  32. package/dist/core/dependency-graph.js +490 -0
  33. package/dist/core/docs-link-integrity.js +443 -0
  34. package/dist/core/env-contract.js +450 -0
  35. package/dist/core/import-cycle.js +152 -0
  36. package/dist/core/line-endings.js +26 -13
  37. package/dist/core/public-json-contracts.js +167 -0
  38. package/dist/core/repo-approval-gate.js +116 -0
  39. package/dist/core/repo-git-ignore-audit.js +302 -0
  40. package/dist/core/repo-manifest-lock-drift.js +321 -0
  41. package/dist/core/repo-merge-conflict-scan.js +335 -0
  42. package/dist/core/repo-version-source.js +82 -0
  43. package/dist/core/route-outline.js +57 -5
  44. package/dist/core/script-pack-suggestions.js +97 -1
  45. package/dist/core/secret-risk-scan.js +440 -0
  46. package/dist/core/skill-route-audit.js +354 -0
  47. package/dist/core/test-performance-report.js +697 -0
  48. package/dist/core/test-regression-selector.js +335 -0
  49. package/package.json +1 -1
  50. package/schemas/README.md +54 -0
  51. package/schemas/change-impact-report.schema.json +184 -0
  52. package/schemas/commands.schema.json +12 -0
  53. package/schemas/dependency-graph-report.schema.json +149 -0
  54. package/schemas/env-contract-report.schema.json +203 -0
  55. package/schemas/import-cycle-report.schema.json +157 -0
  56. package/schemas/link-integrity-report.schema.json +176 -0
  57. package/schemas/repo-approval-gate-report.schema.json +115 -0
  58. package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
  59. package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
  60. package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
  61. package/schemas/repo-version-source-report.schema.json +127 -0
  62. package/schemas/secret-risk-scan-report.schema.json +152 -0
  63. package/schemas/skill-route-audit-report.schema.json +144 -0
  64. package/schemas/test-performance-report.schema.json +319 -0
  65. package/schemas/test-regression-selector-report.schema.json +187 -0
  66. package/templates/default/i18n.toml +80 -26
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +51 -9
  68. package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
  69. package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
  70. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
  71. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
  72. package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
  73. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
  74. package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
  75. package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
  76. package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
  77. package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
  78. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
  79. package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +23 -8
  80. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
  81. package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
  82. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
  83. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
  84. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
  85. package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
  86. package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
  87. package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
  88. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
  89. package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
  90. package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
  91. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
  92. package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
  93. package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
  94. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
  95. package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
  96. package/templates/default/locales/en/.mustflow/skills/routes.toml +69 -9
  97. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
  98. package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
  99. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
  100. package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
  101. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +323 -0
  102. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +18 -10
  103. package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
  104. package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
  105. package/templates/default/manifest.toml +71 -1
@@ -0,0 +1,443 @@
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 LINK_INTEGRITY_PACK_ID = 'docs';
6
+ export const LINK_INTEGRITY_SCRIPT_ID = 'link-integrity';
7
+ export const LINK_INTEGRITY_SCRIPT_REF = `${LINK_INTEGRITY_PACK_ID}/${LINK_INTEGRITY_SCRIPT_ID}`;
8
+ const DEFAULT_MAX_FILES = 200;
9
+ const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
10
+ const MAX_ISSUES = 50;
11
+ const DEFAULT_PATHS = ['README.md', 'schemas/README.md', 'docs-site/src/content/docs'];
12
+ const PATH_FILTERS = ['*.md', '*.mdx'];
13
+ const CHECKED_LINK_KINDS = ['local_file', 'local_anchor'];
14
+ const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.astro']);
15
+ const ERROR_CODES = new Set([
16
+ 'link_integrity_path_outside_root',
17
+ 'link_integrity_unreadable_path',
18
+ 'link_integrity_file_too_large',
19
+ 'link_integrity_max_files_exceeded',
20
+ ]);
21
+ function normalizeRelativePath(value) {
22
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '') || '.';
23
+ }
24
+ function sha256Tagged(buffer) {
25
+ return `sha256:${createHash('sha256').update(buffer).digest('hex')}`;
26
+ }
27
+ function makeFinding(code, severity, pathValue, message, line) {
28
+ return line === undefined ? { code, severity, path: pathValue, message } : { code, severity, path: pathValue, line, message };
29
+ }
30
+ function pushIssue(issues, issue) {
31
+ if (issues.length < MAX_ISSUES) {
32
+ issues.push(issue);
33
+ }
34
+ }
35
+ function isDocumentPath(relativePath) {
36
+ return /\.(?:md|mdx)$/u.test(relativePath);
37
+ }
38
+ function normalizeInputPath(projectRoot, inputPath) {
39
+ const absolutePath = path.resolve(process.cwd(), inputPath);
40
+ ensureInside(projectRoot, absolutePath);
41
+ return {
42
+ absolutePath,
43
+ relativePath: normalizeRelativePath(path.relative(projectRoot, absolutePath)),
44
+ };
45
+ }
46
+ function addCandidate(candidates, findings, issues, policy, candidate) {
47
+ if (candidates.has(candidate.relativePath)) {
48
+ return;
49
+ }
50
+ if (candidates.size >= policy.max_files) {
51
+ if (!findings.some((finding) => finding.code === 'link_integrity_max_files_exceeded')) {
52
+ const message = `Link-integrity matched more than ${policy.max_files} document files; remaining files were skipped.`;
53
+ pushIssue(issues, message);
54
+ findings.push(makeFinding('link_integrity_max_files_exceeded', 'medium', candidate.relativePath, message));
55
+ }
56
+ return;
57
+ }
58
+ candidates.set(candidate.relativePath, candidate);
59
+ }
60
+ function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
61
+ const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
62
+ if (IGNORED_DIRECTORIES.has(path.basename(relativeDirectory)) || [...IGNORED_DIRECTORIES].some((entry) => relativeDirectory.startsWith(`${entry}/`))) {
63
+ return;
64
+ }
65
+ let entries;
66
+ try {
67
+ ensureInsideWithoutSymlinks(projectRoot, absoluteDirectory);
68
+ entries = readdirSync(absoluteDirectory, { withFileTypes: true });
69
+ }
70
+ catch (error) {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ pushIssue(issues, `${relativeDirectory}: ${message}`);
73
+ findings.push(makeFinding('link_integrity_unreadable_path', 'high', relativeDirectory, message));
74
+ return;
75
+ }
76
+ for (const entry of entries) {
77
+ const absoluteEntry = path.join(absoluteDirectory, entry.name);
78
+ const relativeEntry = normalizeRelativePath(path.relative(projectRoot, absoluteEntry));
79
+ if (entry.isDirectory()) {
80
+ collectDocumentsFromDirectory(projectRoot, absoluteEntry, candidates, findings, issues, policy);
81
+ continue;
82
+ }
83
+ if (entry.isFile() && isDocumentPath(relativeEntry)) {
84
+ addCandidate(candidates, findings, issues, policy, { absolutePath: absoluteEntry, relativePath: relativeEntry });
85
+ }
86
+ }
87
+ }
88
+ function collectDocumentCandidates(projectRoot, inputPaths, policy, findings, issues) {
89
+ const candidates = new Map();
90
+ const pathsToCheck = inputPaths.length > 0 ? inputPaths : policy.default_paths;
91
+ for (const inputPath of pathsToCheck) {
92
+ let absolutePath;
93
+ let relativePath;
94
+ try {
95
+ const normalized = normalizeInputPath(projectRoot, inputPath);
96
+ absolutePath = normalized.absolutePath;
97
+ relativePath = normalized.relativePath;
98
+ ensureInsideWithoutSymlinks(projectRoot, absolutePath, {
99
+ allowMissingDescendant: true,
100
+ allowMissingLeaf: true,
101
+ });
102
+ }
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ pushIssue(issues, message);
106
+ findings.push(makeFinding('link_integrity_path_outside_root', 'high', inputPath, message));
107
+ continue;
108
+ }
109
+ if (!existsSync(absolutePath)) {
110
+ continue;
111
+ }
112
+ let stats;
113
+ try {
114
+ stats = lstatSync(absolutePath);
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ pushIssue(issues, `${relativePath}: ${message}`);
119
+ findings.push(makeFinding('link_integrity_unreadable_path', 'high', relativePath, message));
120
+ continue;
121
+ }
122
+ if (stats.isDirectory()) {
123
+ collectDocumentsFromDirectory(projectRoot, absolutePath, candidates, findings, issues, policy);
124
+ continue;
125
+ }
126
+ if (stats.isFile() && isDocumentPath(relativePath)) {
127
+ addCandidate(candidates, findings, issues, policy, { absolutePath, relativePath });
128
+ }
129
+ }
130
+ return [...candidates.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
131
+ }
132
+ function stripOptionalLinkTitle(target) {
133
+ return target.trim().replace(/\s+["'][^"']*["']\s*$/u, '').trim();
134
+ }
135
+ function normalizeInlineTarget(target) {
136
+ const stripped = stripOptionalLinkTitle(target);
137
+ if (stripped.startsWith('<') && stripped.endsWith('>')) {
138
+ return stripped.slice(1, -1).trim();
139
+ }
140
+ return stripped;
141
+ }
142
+ function extractRawLinks(relativePath, text) {
143
+ const links = [];
144
+ const lines = text.split(/\r?\n/u);
145
+ for (const [index, line] of lines.entries()) {
146
+ const lineNumber = index + 1;
147
+ for (const match of line.matchAll(/!?\[([^\]\n]*)\]\(([^)\n]+)\)/gu)) {
148
+ const target = normalizeInlineTarget(match[2] ?? '');
149
+ if (target.length === 0) {
150
+ continue;
151
+ }
152
+ links.push({
153
+ path: relativePath,
154
+ line: lineNumber,
155
+ text: match[1] ?? '',
156
+ target,
157
+ });
158
+ }
159
+ }
160
+ return links;
161
+ }
162
+ function decodeUriComponentSafe(value) {
163
+ try {
164
+ return decodeURIComponent(value);
165
+ }
166
+ catch {
167
+ return value;
168
+ }
169
+ }
170
+ function splitTarget(target) {
171
+ const [pathAndQuery = '', rawAnchor] = target.split('#', 2);
172
+ const pathPart = pathAndQuery.split('?', 1)[0] ?? '';
173
+ const anchor = rawAnchor === undefined ? null : decodeUriComponentSafe(rawAnchor);
174
+ return { pathPart, anchor };
175
+ }
176
+ function slugHeading(value) {
177
+ return value
178
+ .replace(/<[^>]+>/gu, '')
179
+ .replace(/[`*_~]/gu, '')
180
+ .replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1')
181
+ .toLocaleLowerCase()
182
+ .replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
183
+ .trim()
184
+ .replace(/\s+/gu, '-');
185
+ }
186
+ function collectAnchors(text) {
187
+ const anchors = new Set();
188
+ const counts = new Map();
189
+ for (const line of text.split(/\r?\n/u)) {
190
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/u.exec(line);
191
+ if (!match) {
192
+ continue;
193
+ }
194
+ const base = slugHeading(match[2] ?? '');
195
+ if (base.length === 0) {
196
+ continue;
197
+ }
198
+ const count = counts.get(base) ?? 0;
199
+ counts.set(base, count + 1);
200
+ anchors.add(count === 0 ? base : `${base}-${count}`);
201
+ }
202
+ return anchors;
203
+ }
204
+ function normalizeAnchorTarget(anchor) {
205
+ return slugHeading(anchor.replace(/^#+/u, ''));
206
+ }
207
+ function readDocumentText(projectRoot, absolutePath, maxBytes) {
208
+ return readFileInsideWithoutSymlinks(projectRoot, absolutePath, { maxBytes }).toString('utf8');
209
+ }
210
+ function validateLink(projectRoot, source, link, maxBytes) {
211
+ if (/^https?:\/\//iu.test(link.target)) {
212
+ return {
213
+ ...link,
214
+ kind: 'external_url',
215
+ resolved_path: null,
216
+ anchor: null,
217
+ status: 'skipped',
218
+ message: 'External URL is not fetched by this read-only offline check.',
219
+ };
220
+ }
221
+ if (/^mailto:/iu.test(link.target)) {
222
+ return {
223
+ ...link,
224
+ kind: 'email',
225
+ resolved_path: null,
226
+ anchor: null,
227
+ status: 'skipped',
228
+ message: 'Email link is outside local file and anchor validation.',
229
+ };
230
+ }
231
+ if (/^[a-z][a-z0-9+.-]*:/iu.test(link.target)) {
232
+ return {
233
+ ...link,
234
+ kind: 'external_url',
235
+ resolved_path: null,
236
+ anchor: null,
237
+ status: 'skipped',
238
+ message: 'Non-file URI scheme is outside local file and anchor validation.',
239
+ };
240
+ }
241
+ if (link.target.startsWith('/')) {
242
+ return {
243
+ ...link,
244
+ kind: 'site_path',
245
+ resolved_path: null,
246
+ anchor: null,
247
+ status: 'skipped',
248
+ message: 'Site-root path is not resolved against repository files.',
249
+ };
250
+ }
251
+ const { pathPart, anchor } = splitTarget(link.target);
252
+ const targetAbsolutePath = pathPart.length === 0
253
+ ? source.absolutePath
254
+ : path.resolve(path.dirname(source.absolutePath), decodeUriComponentSafe(pathPart));
255
+ try {
256
+ ensureInside(projectRoot, targetAbsolutePath);
257
+ }
258
+ catch {
259
+ return {
260
+ ...link,
261
+ kind: anchor === null ? 'local_file' : 'local_anchor',
262
+ resolved_path: null,
263
+ anchor,
264
+ status: 'missing',
265
+ message: `Linked path escapes the mustflow root: ${link.target}`,
266
+ };
267
+ }
268
+ const resolvedPath = normalizeRelativePath(path.relative(projectRoot, targetAbsolutePath));
269
+ if (!existsSync(targetAbsolutePath)) {
270
+ return {
271
+ ...link,
272
+ kind: anchor === null ? 'local_file' : 'local_anchor',
273
+ resolved_path: resolvedPath,
274
+ anchor,
275
+ status: 'missing',
276
+ message: `Linked file is missing: ${resolvedPath}`,
277
+ };
278
+ }
279
+ if (anchor === null || anchor.length === 0) {
280
+ return {
281
+ ...link,
282
+ kind: 'local_file',
283
+ resolved_path: resolvedPath,
284
+ anchor: null,
285
+ status: 'ok',
286
+ message: `Linked file exists: ${resolvedPath}`,
287
+ };
288
+ }
289
+ if (!isDocumentPath(resolvedPath)) {
290
+ return {
291
+ ...link,
292
+ kind: 'local_anchor',
293
+ resolved_path: resolvedPath,
294
+ anchor,
295
+ status: 'unknown',
296
+ message: `Anchor target is not a Markdown document: ${resolvedPath}`,
297
+ };
298
+ }
299
+ const targetText = readDocumentText(projectRoot, targetAbsolutePath, maxBytes);
300
+ const normalizedAnchor = normalizeAnchorTarget(anchor);
301
+ const anchors = collectAnchors(targetText);
302
+ return anchors.has(normalizedAnchor)
303
+ ? {
304
+ ...link,
305
+ kind: 'local_anchor',
306
+ resolved_path: resolvedPath,
307
+ anchor,
308
+ status: 'ok',
309
+ message: `Linked anchor exists: ${resolvedPath}#${normalizedAnchor}`,
310
+ }
311
+ : {
312
+ ...link,
313
+ kind: 'local_anchor',
314
+ resolved_path: resolvedPath,
315
+ anchor,
316
+ status: 'missing',
317
+ message: `Linked anchor is missing: ${resolvedPath}#${normalizedAnchor}`,
318
+ };
319
+ }
320
+ function findingForLink(link) {
321
+ if (link.status !== 'missing') {
322
+ return null;
323
+ }
324
+ if (link.kind === 'local_anchor' && link.anchor !== null && link.message.includes('Linked anchor is missing')) {
325
+ return makeFinding('link_integrity_missing_anchor', 'medium', link.path, link.message, link.line);
326
+ }
327
+ return makeFinding('link_integrity_missing_file', 'medium', link.path, link.message, link.line);
328
+ }
329
+ function linkIntegrityStatus(findings) {
330
+ if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
331
+ return 'error';
332
+ }
333
+ if (findings.some((finding) => ['medium', 'high', 'critical'].includes(finding.severity))) {
334
+ return 'failed';
335
+ }
336
+ return 'passed';
337
+ }
338
+ function summarizeLinks(files, links) {
339
+ const count = (status) => links.filter((link) => link.status === status).length;
340
+ return {
341
+ files_checked: files.length,
342
+ links_checked: links.length,
343
+ ok: count('ok'),
344
+ missing: count('missing'),
345
+ skipped: count('skipped'),
346
+ unknown: count('unknown'),
347
+ };
348
+ }
349
+ function createInputHash(policy, files, links, findings) {
350
+ return sha256Tagged(JSON.stringify({
351
+ policy,
352
+ files: files.map((file) => ({ path: file.path, sha256: file.sha256, link_count: file.link_count })),
353
+ links: links.map((link) => ({
354
+ kind: link.kind,
355
+ path: link.path,
356
+ line: link.line,
357
+ target: link.target,
358
+ resolved_path: link.resolved_path,
359
+ anchor: link.anchor,
360
+ status: link.status,
361
+ })),
362
+ findings: findings.map((finding) => ({ code: finding.code, path: finding.path, line: finding.line })),
363
+ }));
364
+ }
365
+ export function checkLinkIntegrity(projectRoot, options) {
366
+ const root = path.resolve(projectRoot);
367
+ const policy = {
368
+ max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
369
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
370
+ default_paths: [...DEFAULT_PATHS],
371
+ path_filters: [...PATH_FILTERS],
372
+ checked_link_kinds: [...CHECKED_LINK_KINDS],
373
+ };
374
+ const findings = [];
375
+ const issues = [];
376
+ const fileEntries = [];
377
+ const links = [];
378
+ const candidates = collectDocumentCandidates(root, options.paths, policy, findings, issues);
379
+ for (const candidate of candidates) {
380
+ let buffer = null;
381
+ try {
382
+ buffer = readFileInsideWithoutSymlinks(root, candidate.absolutePath, { maxBytes: policy.max_file_bytes });
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ const code = message.includes('exceeds maximum size')
387
+ ? 'link_integrity_file_too_large'
388
+ : 'link_integrity_unreadable_path';
389
+ pushIssue(issues, `${candidate.relativePath}: ${message}`);
390
+ findings.push(makeFinding(code, 'high', candidate.relativePath, message));
391
+ }
392
+ const text = buffer?.toString('utf8') ?? '';
393
+ const rawLinks = buffer ? extractRawLinks(candidate.relativePath, text) : [];
394
+ for (const rawLink of rawLinks) {
395
+ try {
396
+ const validatedLink = validateLink(root, candidate, rawLink, policy.max_file_bytes);
397
+ links.push(validatedLink);
398
+ const finding = findingForLink(validatedLink);
399
+ if (finding) {
400
+ findings.push(finding);
401
+ }
402
+ }
403
+ catch (error) {
404
+ const message = error instanceof Error ? error.message : String(error);
405
+ pushIssue(issues, `${candidate.relativePath}:${rawLink.line}: ${message}`);
406
+ findings.push(makeFinding('link_integrity_unreadable_path', 'high', candidate.relativePath, message, rawLink.line));
407
+ }
408
+ }
409
+ fileEntries.push({
410
+ kind: 'document',
411
+ path: candidate.relativePath,
412
+ sha256: buffer ? sha256Tagged(buffer) : null,
413
+ size_bytes: buffer?.byteLength ?? null,
414
+ line_count: buffer ? text.split(/\r?\n/u).length : null,
415
+ link_count: rawLinks.length,
416
+ });
417
+ }
418
+ const sortedFiles = fileEntries.sort((left, right) => left.path.localeCompare(right.path));
419
+ const sortedLinks = links.sort((left, right) => left.path.localeCompare(right.path) ||
420
+ left.line - right.line ||
421
+ left.kind.localeCompare(right.kind) ||
422
+ left.target.localeCompare(right.target));
423
+ const status = linkIntegrityStatus(findings);
424
+ const summary = summarizeLinks(sortedFiles, sortedLinks);
425
+ return {
426
+ schema_version: '1',
427
+ command: 'script-pack',
428
+ pack_id: LINK_INTEGRITY_PACK_ID,
429
+ script_id: LINK_INTEGRITY_SCRIPT_ID,
430
+ script_ref: LINK_INTEGRITY_SCRIPT_REF,
431
+ action: 'check',
432
+ status,
433
+ ok: status === 'passed',
434
+ mustflow_root: root,
435
+ policy,
436
+ input_hash: createInputHash(policy, sortedFiles, sortedLinks, findings),
437
+ files: sortedFiles,
438
+ links: sortedLinks,
439
+ summary,
440
+ findings,
441
+ issues,
442
+ };
443
+ }