mustflow 2.103.3 → 2.103.12

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 (50) hide show
  1. package/dist/cli/commands/run.js +11 -0
  2. package/dist/cli/commands/script-pack.js +2 -0
  3. package/dist/cli/i18n/en.js +35 -0
  4. package/dist/cli/i18n/es.js +35 -0
  5. package/dist/cli/i18n/fr.js +35 -0
  6. package/dist/cli/i18n/hi.js +35 -0
  7. package/dist/cli/i18n/ko.js +35 -0
  8. package/dist/cli/i18n/zh.js +35 -0
  9. package/dist/cli/lib/external-skill-import.js +78 -14
  10. package/dist/cli/lib/local-index/sql.js +9 -1
  11. package/dist/cli/lib/run-plan.js +37 -0
  12. package/dist/cli/lib/script-pack-registry.js +57 -0
  13. package/dist/cli/script-packs/repo-deploy-surface.js +98 -0
  14. package/dist/cli/script-packs/repo-security-pattern-scan.js +150 -0
  15. package/dist/core/change-impact.js +16 -0
  16. package/dist/core/code-outline.js +3 -13
  17. package/dist/core/command-env.js +26 -8
  18. package/dist/core/config-chain.js +3 -13
  19. package/dist/core/dependency-graph.js +3 -13
  20. package/dist/core/docs-link-integrity.js +23 -4
  21. package/dist/core/env-contract.js +3 -13
  22. package/dist/core/export-diff.js +3 -3
  23. package/dist/core/ignored-directories.js +40 -0
  24. package/dist/core/public-json-contracts.js +18 -0
  25. package/dist/core/reference-drift.js +4 -2
  26. package/dist/core/related-files.js +3 -13
  27. package/dist/core/repo-deploy-surface.js +428 -0
  28. package/dist/core/repo-merge-conflict-scan.js +3 -9
  29. package/dist/core/route-outline.js +3 -13
  30. package/dist/core/script-pack-suggestions.js +52 -14
  31. package/dist/core/secret-risk-scan.js +3 -13
  32. package/dist/core/security-pattern-scan.js +518 -0
  33. package/dist/core/skill-route-resolution.js +21 -1
  34. package/package.json +2 -2
  35. package/schemas/README.md +7 -0
  36. package/schemas/link-integrity-report.schema.json +1 -0
  37. package/schemas/reference-drift-report.schema.json +1 -0
  38. package/schemas/repo-deploy-surface-report.schema.json +190 -0
  39. package/schemas/security-pattern-scan-report.schema.json +196 -0
  40. package/templates/default/i18n.toml +20 -8
  41. package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +30 -7
  42. package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +18 -9
  43. package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +12 -6
  44. package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +20 -9
  45. package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +20 -15
  46. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +22 -7
  47. package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +21 -19
  48. package/templates/default/locales/en/.mustflow/skills/react-code-change/SKILL.md +54 -8
  49. package/templates/default/locales/en/.mustflow/skills/vertical-slice-tdd/SKILL.md +22 -8
  50. package/templates/default/manifest.toml +1 -1
@@ -453,6 +453,35 @@ export const SCRIPT_PACKS = [
453
453
  reportSchemaFile: 'secret-risk-scan-report.schema.json',
454
454
  loadRunner: async () => (await import('../script-packs/repo-secret-risk-scan.js')).runRepoSecretRiskScanScript,
455
455
  },
456
+ {
457
+ packId: 'repo',
458
+ id: 'security-pattern-scan',
459
+ ref: scriptRef('repo', 'security-pattern-scan'),
460
+ usage: 'mf script-pack run repo/security-pattern-scan scan [path...] [options]',
461
+ summaryKey: 'scriptPack.script.securityPatternScan.summary',
462
+ actions: ['scan'],
463
+ useWhen: [
464
+ 'Scan source, CI, and config files for high-signal security code patterns without printing matched source lines or secret values.',
465
+ 'Review filesystem, command, injection, browser, token, session, parser, logging, access-control, and transport-security pattern leads before source-to-sink review.',
466
+ ],
467
+ phases: ['before_change', 'after_change', 'review'],
468
+ readOnly: true,
469
+ mutates: false,
470
+ network: false,
471
+ inputs: ['path', 'max_files', 'max_file_bytes', 'max_findings'],
472
+ outputs: ['human_summary', 'json_report', 'security_pattern_findings', 'review_focus'],
473
+ relatedSkills: [
474
+ 'api-access-control-review',
475
+ 'file-upload-security-review',
476
+ 'security-flow-review',
477
+ 'security-privacy-review',
478
+ 'security-regression-tests',
479
+ ],
480
+ riskLevel: 'medium',
481
+ cost: 'low',
482
+ reportSchemaFile: 'security-pattern-scan-report.schema.json',
483
+ loadRunner: async () => (await import('../script-packs/repo-security-pattern-scan.js')).runRepoSecurityPatternScanScript,
484
+ },
456
485
  {
457
486
  packId: 'repo',
458
487
  id: 'generated-boundary',
@@ -655,6 +684,34 @@ export const SCRIPT_PACKS = [
655
684
  reportSchemaFile: 'repo-approval-gate-report.schema.json',
656
685
  loadRunner: async () => (await import('../script-packs/repo-approval-gate.js')).runRepoApprovalGateScript,
657
686
  },
687
+ {
688
+ packId: 'repo',
689
+ id: 'deploy-surface',
690
+ ref: scriptRef('repo', 'deploy-surface'),
691
+ usage: 'mf script-pack run repo/deploy-surface inspect [options]',
692
+ summaryKey: 'scriptPack.script.deploySurface.summary',
693
+ actions: ['inspect'],
694
+ useWhen: [
695
+ 'Inspect local workflows, package metadata, package scripts, and deploy config files before claiming push, tag, release, or deploy follow-up is available.',
696
+ 'Review detected deploy surfaces with trigger, required verification, and manual gate evidence after repository changes.',
697
+ ],
698
+ phases: ['before_change', 'after_change', 'review'],
699
+ readOnly: true,
700
+ mutates: false,
701
+ network: false,
702
+ inputs: [],
703
+ outputs: ['human_summary', 'json_report', 'deploy_surfaces', 'required_verification', 'manual_gates'],
704
+ relatedSkills: [
705
+ 'completion-evidence-gate',
706
+ 'deployment-rollout-safety-review',
707
+ 'public-json-contract-change',
708
+ 'release-publish-change',
709
+ ],
710
+ riskLevel: 'low',
711
+ cost: 'low',
712
+ reportSchemaFile: 'repo-deploy-surface-report.schema.json',
713
+ loadRunner: async () => (await import('../script-packs/repo-deploy-surface.js')).runRepoDeploySurfaceScript,
714
+ },
658
715
  {
659
716
  packId: 'repo',
660
717
  id: 'related-files',
@@ -0,0 +1,98 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectRepoDeploySurface, REPO_DEPLOY_SURFACE_SCRIPT_REF, } from '../../core/repo-deploy-surface.js';
6
+ const REPO_DEPLOY_SURFACE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
7
+ export function getRepoDeploySurfaceHelp(lang = 'en') {
8
+ return renderHelp({
9
+ usage: 'mf script-pack run repo/deploy-surface inspect [options]',
10
+ summary: t(lang, 'deploySurface.help.summary'),
11
+ options: [
12
+ { label: '--json', description: t(lang, 'cli.option.json') },
13
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
14
+ ],
15
+ examples: [
16
+ 'mf script-pack run repo/deploy-surface inspect',
17
+ 'mf script-pack run repo/deploy-surface inspect --json',
18
+ ],
19
+ exitCodes: [
20
+ { label: '0', description: t(lang, 'deploySurface.help.exit.ok') },
21
+ { label: '1', description: t(lang, 'deploySurface.help.exit.fail') },
22
+ ],
23
+ }, lang);
24
+ }
25
+ function parseRepoDeploySurfaceOptions(args, lang) {
26
+ const [action, ...rest] = args;
27
+ const parsed = parseCliOptions(rest, REPO_DEPLOY_SURFACE_OPTIONS, { allowPositionals: false });
28
+ const json = hasParsedCliOption(parsed, '--json');
29
+ if (action !== 'inspect') {
30
+ return {
31
+ action: 'inspect',
32
+ json,
33
+ error: action
34
+ ? t(lang, 'deploySurface.error.unknownAction', { action })
35
+ : t(lang, 'deploySurface.error.missingAction'),
36
+ };
37
+ }
38
+ if (parsed.error) {
39
+ return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
40
+ }
41
+ return { action, json };
42
+ }
43
+ function renderRepoDeploySurfaceSummary(report, lang) {
44
+ const lines = [
45
+ t(lang, 'deploySurface.title'),
46
+ `${t(lang, 'scriptPack.label.script')}: ${REPO_DEPLOY_SURFACE_SCRIPT_REF}`,
47
+ `${t(lang, 'label.status')}: ${report.status}`,
48
+ `${t(lang, 'deploySurface.label.deploySurface')}: ${report.has_deploy_surface ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
49
+ `${t(lang, 'deploySurface.label.surfaces')}: ${report.summary.surface_count}`,
50
+ `${t(lang, 'deploySurface.label.requiredVerification')}: ${report.summary.required_verification_count}`,
51
+ `${t(lang, 'deploySurface.label.manualGates')}: ${report.summary.manual_gate_count}`,
52
+ ];
53
+ if (report.surfaces.length > 0) {
54
+ lines.push(t(lang, 'deploySurface.label.surfaceDetails'));
55
+ for (const surface of report.surfaces) {
56
+ const location = surface.line === null ? surface.path : `${surface.path}:${surface.line}`;
57
+ const trigger = surface.trigger === null ? '' : `, trigger ${surface.trigger}`;
58
+ lines.push(`- ${surface.surface_type} (${surface.kind}) at ${location}${trigger}`);
59
+ }
60
+ }
61
+ if (report.required_verification.length > 0) {
62
+ lines.push(t(lang, 'deploySurface.label.requiredVerification'));
63
+ for (const verification of report.required_verification) {
64
+ lines.push(`- ${verification}`);
65
+ }
66
+ }
67
+ if (report.manual_gates.length > 0) {
68
+ lines.push(t(lang, 'deploySurface.label.manualGates'));
69
+ for (const gate of report.manual_gates) {
70
+ lines.push(`- ${gate}`);
71
+ }
72
+ }
73
+ if (report.issues.length > 0) {
74
+ lines.push(t(lang, 'deploySurface.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
75
+ }
76
+ if (!report.has_deploy_surface && report.issues.length === 0) {
77
+ lines.push(t(lang, 'deploySurface.noSurfaces'));
78
+ }
79
+ return lines.join('\n');
80
+ }
81
+ export function runRepoDeploySurfaceScript(args, reporter, lang = 'en') {
82
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
83
+ reporter.stdout(getRepoDeploySurfaceHelp(lang));
84
+ return 0;
85
+ }
86
+ const options = parseRepoDeploySurfaceOptions(args, lang);
87
+ if (options.error) {
88
+ printUsageError(reporter, options.error, 'mf script-pack run repo/deploy-surface --help', getRepoDeploySurfaceHelp(lang), lang);
89
+ return 1;
90
+ }
91
+ const report = inspectRepoDeploySurface(resolveMustflowRoot());
92
+ if (options.json) {
93
+ reporter.stdout(JSON.stringify(report, null, 2));
94
+ return report.ok ? 0 : 1;
95
+ }
96
+ reporter.stdout(renderRepoDeploySurfaceSummary(report, lang));
97
+ return report.ok ? 0 : 1;
98
+ }
@@ -0,0 +1,150 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectSecurityPatternScan, SECURITY_PATTERN_SCAN_SCRIPT_REF, } from '../../core/security-pattern-scan.js';
6
+ const SECURITY_PATTERN_SCAN_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--max-files', kind: 'string' },
9
+ { name: '--max-file-bytes', kind: 'string' },
10
+ { name: '--max-findings', kind: 'string' },
11
+ ];
12
+ function parsePositiveInteger(value, option, lang) {
13
+ if (value === null) {
14
+ return { value: null };
15
+ }
16
+ if (!/^[1-9]\d*$/u.test(value)) {
17
+ return { value: null, error: t(lang, 'securityPatternScan.error.invalidPositiveInteger', { option, value }) };
18
+ }
19
+ const parsed = Number(value);
20
+ if (!Number.isSafeInteger(parsed)) {
21
+ return { value: null, error: t(lang, 'securityPatternScan.error.invalidPositiveInteger', { option, value }) };
22
+ }
23
+ return { value: parsed };
24
+ }
25
+ export function getRepoSecurityPatternScanHelp(lang = 'en') {
26
+ return renderHelp({
27
+ usage: 'mf script-pack run repo/security-pattern-scan scan [path...] [options]',
28
+ summary: t(lang, 'securityPatternScan.help.summary'),
29
+ options: [
30
+ { label: '--max-files <count>', description: t(lang, 'securityPatternScan.help.option.maxFiles') },
31
+ { label: '--max-file-bytes <bytes>', description: t(lang, 'securityPatternScan.help.option.maxFileBytes') },
32
+ { label: '--max-findings <count>', description: t(lang, 'securityPatternScan.help.option.maxFindings') },
33
+ { label: '--json', description: t(lang, 'cli.option.json') },
34
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
35
+ ],
36
+ examples: [
37
+ 'mf script-pack run repo/security-pattern-scan scan --json',
38
+ 'mf script-pack run repo/security-pattern-scan scan src .github/workflows --json',
39
+ 'mf script-pack run repo/security-pattern-scan scan src/server.ts --max-findings 50 --json',
40
+ ],
41
+ exitCodes: [
42
+ { label: '0', description: t(lang, 'securityPatternScan.help.exit.ok') },
43
+ { label: '1', description: t(lang, 'securityPatternScan.help.exit.fail') },
44
+ ],
45
+ }, lang);
46
+ }
47
+ function parseSecurityPatternScanOptions(args, lang) {
48
+ const [action, ...rest] = args;
49
+ const parsed = parseCliOptions(rest, SECURITY_PATTERN_SCAN_OPTIONS, { allowPositionals: true });
50
+ const json = hasParsedCliOption(parsed, '--json');
51
+ const maxFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-files'), '--max-files', lang);
52
+ const maxFileBytes = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-file-bytes'), '--max-file-bytes', lang);
53
+ const maxFindings = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-findings'), '--max-findings', lang);
54
+ if (action !== 'scan') {
55
+ return {
56
+ action: 'scan',
57
+ json,
58
+ paths: parsed.positionals,
59
+ maxFiles: maxFiles.value,
60
+ maxFileBytes: maxFileBytes.value,
61
+ maxFindings: maxFindings.value,
62
+ error: action
63
+ ? t(lang, 'securityPatternScan.error.unknownAction', { action })
64
+ : t(lang, 'securityPatternScan.error.missingAction'),
65
+ };
66
+ }
67
+ if (parsed.error) {
68
+ return {
69
+ action,
70
+ json,
71
+ paths: parsed.positionals,
72
+ maxFiles: maxFiles.value,
73
+ maxFileBytes: maxFileBytes.value,
74
+ maxFindings: maxFindings.value,
75
+ error: formatCliOptionParseError(parsed.error, lang),
76
+ };
77
+ }
78
+ for (const candidate of [maxFiles, maxFileBytes, maxFindings]) {
79
+ if (candidate.error) {
80
+ return {
81
+ action,
82
+ json,
83
+ paths: parsed.positionals,
84
+ maxFiles: maxFiles.value,
85
+ maxFileBytes: maxFileBytes.value,
86
+ maxFindings: maxFindings.value,
87
+ error: candidate.error,
88
+ };
89
+ }
90
+ }
91
+ return {
92
+ action,
93
+ json,
94
+ paths: parsed.positionals,
95
+ maxFiles: maxFiles.value,
96
+ maxFileBytes: maxFileBytes.value,
97
+ maxFindings: maxFindings.value,
98
+ };
99
+ }
100
+ function renderSecurityPatternScanSummary(report, lang) {
101
+ const lines = [
102
+ t(lang, 'securityPatternScan.title'),
103
+ `${t(lang, 'scriptPack.label.script')}: ${SECURITY_PATTERN_SCAN_SCRIPT_REF}`,
104
+ `${t(lang, 'label.status')}: ${report.status}`,
105
+ `${t(lang, 'securityPatternScan.label.files')}: ${report.summary.file_count}`,
106
+ `${t(lang, 'securityPatternScan.label.findings')}: ${report.summary.finding_count}`,
107
+ `${t(lang, 'securityPatternScan.label.categories')}: ${report.summary.category_count}`,
108
+ `${t(lang, 'securityPatternScan.label.highOrCritical')}: ${report.summary.high_or_critical_count}`,
109
+ `${t(lang, 'securityPatternScan.label.truncated')}: ${report.truncated ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
110
+ ];
111
+ if (report.findings.length > 0) {
112
+ lines.push(t(lang, 'securityPatternScan.label.findings'));
113
+ for (const finding of report.findings.slice(0, 40)) {
114
+ const line = finding.line ? `:${finding.line}` : '';
115
+ const detector = finding.detector ? ` ${finding.detector}` : '';
116
+ const focus = finding.review_focus ? ` ${t(lang, 'securityPatternScan.label.reviewFocus')}: ${finding.review_focus}` : '';
117
+ lines.push(`- ${finding.path}${line}: ${finding.code}${detector} (${finding.message})${focus}`);
118
+ }
119
+ }
120
+ if (report.issues.length > 0) {
121
+ lines.push(t(lang, 'securityPatternScan.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
122
+ }
123
+ if (report.findings.length === 0 && report.issues.length === 0) {
124
+ lines.push(t(lang, 'securityPatternScan.clean'));
125
+ }
126
+ return lines.join('\n');
127
+ }
128
+ export function runRepoSecurityPatternScanScript(args, reporter, lang = 'en') {
129
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
130
+ reporter.stdout(getRepoSecurityPatternScanHelp(lang));
131
+ return 0;
132
+ }
133
+ const options = parseSecurityPatternScanOptions(args, lang);
134
+ if (options.error) {
135
+ printUsageError(reporter, options.error, 'mf script-pack run repo/security-pattern-scan --help', getRepoSecurityPatternScanHelp(lang), lang);
136
+ return 1;
137
+ }
138
+ const report = inspectSecurityPatternScan(resolveMustflowRoot(), {
139
+ paths: options.paths,
140
+ maxFiles: options.maxFiles ?? undefined,
141
+ maxFileBytes: options.maxFileBytes ?? undefined,
142
+ maxFindings: options.maxFindings ?? undefined,
143
+ });
144
+ if (options.json) {
145
+ reporter.stdout(JSON.stringify(report, null, 2));
146
+ return report.ok ? 0 : 1;
147
+ }
148
+ reporter.stdout(renderSecurityPatternScanSummary(report, lang));
149
+ return report.ok ? 0 : 1;
150
+ }
@@ -195,6 +195,13 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
195
195
  issues.push(`dependency-graph: ${issue}`);
196
196
  }
197
197
  }
198
+ for (const dependencyFinding of dependencyReport.findings) {
199
+ const mappedFinding = mapDependencyGraphTruncationFinding(dependencyFinding);
200
+ if (mappedFinding === null) {
201
+ continue;
202
+ }
203
+ findings.push(mappedFinding);
204
+ }
198
205
  const changedPathSet = new Set(sourcePaths);
199
206
  for (const edge of dependencyReport.edges) {
200
207
  if (!changedPathSet.has(edge.target_path) || changedPathSet.has(edge.source_path)) {
@@ -209,6 +216,15 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
209
216
  }, policy, findings, issues);
210
217
  }
211
218
  }
219
+ function mapDependencyGraphTruncationFinding(finding) {
220
+ if (finding.code === 'dependency_graph_max_files_exceeded') {
221
+ return makeFinding('change_impact_max_files_exceeded', 'high', finding.path, `Dependency graph input was truncated while computing change impact: ${finding.message}`);
222
+ }
223
+ if (finding.code === 'dependency_graph_max_nodes_exceeded' || finding.code === 'dependency_graph_max_edges_exceeded') {
224
+ return makeFinding('change_impact_max_impacts_exceeded', 'high', finding.path, `Dependency graph impact expansion was truncated while computing change impact: ${finding.message}`);
225
+ }
226
+ return null;
227
+ }
212
228
  function createScriptHints(changedFiles) {
213
229
  const sourcePaths = changedFiles.filter((file) => file.surface === 'source').map((file) => file.path);
214
230
  const selectorRelevantFiles = changedFiles.filter((file) => ['source', 'test', 'schema', 'config', 'package', 'template', 'workflow', 'unknown'].includes(file.surface));
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  import { listSourceAnchorFiles, parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
6
7
  export const CODE_PACK_ID = 'code';
@@ -19,17 +20,7 @@ const SVELTE_EXTENSIONS = ['.svelte'];
19
20
  const GO_EXTENSIONS = ['.go'];
20
21
  const RUST_EXTENSIONS = ['.rs'];
21
22
  const PYTHON_EXTENSIONS = ['.py'];
22
- const IGNORED_DIRECTORIES = [
23
- '.git',
24
- '.mustflow/cache',
25
- '.mustflow/state',
26
- 'node_modules',
27
- 'dist',
28
- 'build',
29
- 'coverage',
30
- '.next',
31
- '.turbo',
32
- ];
23
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
33
24
  const ERROR_OUTLINE_CODES = new Set([
34
25
  'code_outline_path_outside_root',
35
26
  'code_outline_unreadable_path',
@@ -97,8 +88,7 @@ export function languageForPath(filePath) {
97
88
  return languageAdapterForPath(filePath)?.languageForPath(filePath) ?? null;
98
89
  }
99
90
  function isIgnoredDirectory(relativePath) {
100
- const normalized = normalizeRelativePath(relativePath);
101
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
91
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
102
92
  }
103
93
  function makeOutlineFinding(code, severity, pathValue, message) {
104
94
  return { code, severity, path: pathValue, message };
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, realpathSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { readString, readStringArray } from './config-loading.js';
4
4
  export const COMMAND_ENV_POLICIES = new Set(['inherit', 'minimal', 'allowlist']);
@@ -31,12 +31,30 @@ export const DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST = new Set(['mf'
31
31
  function getPathEnvKey(env) {
32
32
  return Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
33
33
  }
34
- function sameResolvedPath(left, right) {
35
- const resolvedLeft = path.resolve(left);
36
- const resolvedRight = path.resolve(right);
37
- return process.platform === 'win32'
38
- ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
39
- : resolvedLeft === resolvedRight;
34
+ function normalizePathForComparison(value) {
35
+ const resolved = path.resolve(value);
36
+ if (!existsSync(resolved)) {
37
+ return resolved;
38
+ }
39
+ try {
40
+ return realpathSync.native(resolved);
41
+ }
42
+ catch {
43
+ return resolved;
44
+ }
45
+ }
46
+ function pathsEqual(left, right) {
47
+ return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
48
+ }
49
+ function pathEntryCandidates(entry, projectRoot) {
50
+ if (path.isAbsolute(entry)) {
51
+ return [entry];
52
+ }
53
+ return [path.resolve(projectRoot, entry), path.resolve(entry)];
54
+ }
55
+ function sameResolvedPath(left, right, projectRoot) {
56
+ const resolvedRight = normalizePathForComparison(right);
57
+ return pathEntryCandidates(left, projectRoot).some((candidate) => pathsEqual(normalizePathForComparison(candidate), resolvedRight));
40
58
  }
41
59
  function uniqueEnvNames(values) {
42
60
  return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right));
@@ -112,7 +130,7 @@ function removeProjectLocalBinFromPath(env, projectRoot) {
112
130
  ...env,
113
131
  [pathKey]: currentPath
114
132
  .split(path.delimiter)
115
- .filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath))
133
+ .filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath, projectRoot))
116
134
  .join(path.delimiter),
117
135
  };
118
136
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const CONFIG_CHAIN_PACK_ID = 'repo';
6
7
  export const CONFIG_CHAIN_SCRIPT_ID = 'config-chain';
@@ -37,17 +38,7 @@ const CONFIG_FILE_NAMES = [
37
38
  '.mustflow/config/commands.toml',
38
39
  '.mustflow/config/mustflow.toml',
39
40
  ];
40
- const IGNORED_DIRECTORIES = [
41
- '.git',
42
- '.mustflow/cache',
43
- '.mustflow/state',
44
- 'node_modules',
45
- 'dist',
46
- 'build',
47
- 'coverage',
48
- '.next',
49
- '.turbo',
50
- ];
41
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
51
42
  const ERROR_CODES = new Set([
52
43
  'config_chain_path_outside_root',
53
44
  'config_chain_unreadable_path',
@@ -133,8 +124,7 @@ function isConfigFile(relativePath) {
133
124
  /^tsconfig(?:\..*)?\.json$/u.test(name));
134
125
  }
135
126
  function isIgnoredDirectory(relativePath) {
136
- const normalized = normalizeRelativePath(relativePath);
137
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
127
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
138
128
  }
139
129
  function normalizeTargetPath(projectRoot, targetPath) {
140
130
  const absolutePath = path.resolve(process.cwd(), targetPath);
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const DEPENDENCY_GRAPH_PACK_ID = 'code';
6
7
  export const DEPENDENCY_GRAPH_SCRIPT_ID = 'dependency-graph';
@@ -14,17 +15,7 @@ const MAX_ISSUES = 50;
14
15
  const MAX_CYCLES = 20;
15
16
  const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
16
17
  const RESOLVE_EXTENSIONS = [...SOURCE_EXTENSIONS, '.json'];
17
- const IGNORED_DIRECTORIES = [
18
- '.git',
19
- '.mustflow/cache',
20
- '.mustflow/state',
21
- 'node_modules',
22
- 'dist',
23
- 'build',
24
- 'coverage',
25
- '.next',
26
- '.turbo',
27
- ];
18
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
28
19
  const ERROR_CODES = new Set([
29
20
  'dependency_graph_path_outside_root',
30
21
  'dependency_graph_unreadable_path',
@@ -64,8 +55,7 @@ function isSourceLanguage(language) {
64
55
  return language !== 'json' && language !== 'other';
65
56
  }
66
57
  function isIgnoredDirectory(relativePath) {
67
- const normalized = normalizeRelativePath(relativePath);
68
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
58
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
69
59
  }
70
60
  function makeFinding(code, severity, pathValue, message) {
71
61
  return { code, severity, path: pathValue, message };
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const LINK_INTEGRITY_PACK_ID = 'docs';
6
7
  export const LINK_INTEGRITY_SCRIPT_ID = 'link-integrity';
@@ -11,7 +12,7 @@ const MAX_ISSUES = 50;
11
12
  const DEFAULT_PATHS = ['README.md', 'schemas/README.md', 'docs-site/src/content/docs'];
12
13
  const PATH_FILTERS = ['*.md', '*.mdx'];
13
14
  const CHECKED_LINK_KINDS = ['local_file', 'local_anchor'];
14
- const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.astro']);
15
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
15
16
  const ERROR_CODES = new Set([
16
17
  'link_integrity_path_outside_root',
17
18
  'link_integrity_unreadable_path',
@@ -59,7 +60,7 @@ function addCandidate(candidates, findings, issues, policy, candidate) {
59
60
  }
60
61
  function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
61
62
  const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
62
- if (IGNORED_DIRECTORIES.has(path.basename(relativeDirectory)) || [...IGNORED_DIRECTORIES].some((entry) => relativeDirectory.startsWith(`${entry}/`))) {
63
+ if (isIgnoredDirectoryPath(relativeDirectory, IGNORED_DIRECTORIES)) {
63
64
  return;
64
65
  }
65
66
  let entries;
@@ -173,9 +174,26 @@ function splitTarget(target) {
173
174
  const anchor = rawAnchor === undefined ? null : decodeUriComponentSafe(rawAnchor);
174
175
  return { pathPart, anchor };
175
176
  }
177
+ function stripHtmlTagText(value) {
178
+ let result = '';
179
+ let tagDepth = 0;
180
+ for (const char of value) {
181
+ if (char === '<') {
182
+ tagDepth += 1;
183
+ continue;
184
+ }
185
+ if (char === '>') {
186
+ tagDepth = Math.max(0, tagDepth - 1);
187
+ continue;
188
+ }
189
+ if (tagDepth === 0) {
190
+ result += char;
191
+ }
192
+ }
193
+ return result;
194
+ }
176
195
  function slugHeading(value) {
177
- return value
178
- .replace(/<[^>]+>/gu, '')
196
+ return stripHtmlTagText(value)
179
197
  .replace(/[`*_~]/gu, '')
180
198
  .replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1')
181
199
  .toLocaleLowerCase()
@@ -369,6 +387,7 @@ export function checkLinkIntegrity(projectRoot, options) {
369
387
  max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
370
388
  default_paths: [...DEFAULT_PATHS],
371
389
  path_filters: [...PATH_FILTERS],
390
+ ignored_directories: [...IGNORED_DIRECTORIES],
372
391
  checked_link_kinds: [...CHECKED_LINK_KINDS],
373
392
  };
374
393
  const findings = [];
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const ENV_CONTRACT_PACK_ID = 'repo';
6
7
  export const ENV_CONTRACT_SCRIPT_ID = 'env-contract';
@@ -36,17 +37,7 @@ const ENV_EXAMPLE_NAMES = [
36
37
  '.dev.vars.example',
37
38
  ];
38
39
  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
- ];
40
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
50
41
  const ERROR_CODES = new Set([
51
42
  'env_contract_path_outside_root',
52
43
  'env_contract_unreadable_path',
@@ -66,8 +57,7 @@ function pushIssue(issues, issue) {
66
57
  }
67
58
  }
68
59
  function isIgnoredDirectory(relativePath) {
69
- const normalized = normalizeRelativePath(relativePath);
70
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
60
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
71
61
  }
72
62
  function isEnvExampleFile(relativePath) {
73
63
  const name = path.basename(relativePath).toLowerCase();
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { extractSymbols, languageForPath } from './code-outline.js';
6
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
6
7
  import { ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
7
8
  export const CODE_EXPORT_DIFF_SCRIPT_ID = 'export-diff';
8
9
  export const CODE_EXPORT_DIFF_SCRIPT_REF = `code/${CODE_EXPORT_DIFF_SCRIPT_ID}`;
@@ -10,7 +11,7 @@ const DEFAULT_BASE_REF = 'HEAD';
10
11
  const DEFAULT_MAX_FILES = 100;
11
12
  const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
12
13
  const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
13
- const IGNORED_DIRECTORIES = ['.git', 'node_modules', 'dist', 'build', 'coverage', '.mustflow/cache', '.mustflow/state'];
14
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
14
15
  const ERROR_CODES = new Set([
15
16
  'export_diff_git_unavailable',
16
17
  'export_diff_invalid_ref',
@@ -46,8 +47,7 @@ function makeFinding(code, severity, pathValue, message) {
46
47
  return { code, severity, path: pathValue, message };
47
48
  }
48
49
  function isIgnoredPath(relativePath) {
49
- const normalized = normalizeRelativePath(relativePath);
50
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
50
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
51
51
  }
52
52
  function isSupportedPath(relativePath) {
53
53
  return SUPPORTED_EXTENSIONS.includes(path.extname(relativePath).toLowerCase());