mustflow 2.103.3 → 2.103.10

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 (38) hide show
  1. package/dist/cli/commands/run.js +11 -0
  2. package/dist/cli/i18n/en.js +2 -0
  3. package/dist/cli/i18n/es.js +2 -0
  4. package/dist/cli/i18n/fr.js +2 -0
  5. package/dist/cli/i18n/hi.js +2 -0
  6. package/dist/cli/i18n/ko.js +2 -0
  7. package/dist/cli/i18n/zh.js +2 -0
  8. package/dist/cli/lib/external-skill-import.js +78 -14
  9. package/dist/cli/lib/local-index/sql.js +9 -1
  10. package/dist/cli/lib/run-plan.js +37 -0
  11. package/dist/core/change-impact.js +16 -0
  12. package/dist/core/code-outline.js +3 -13
  13. package/dist/core/config-chain.js +3 -13
  14. package/dist/core/dependency-graph.js +3 -13
  15. package/dist/core/docs-link-integrity.js +23 -4
  16. package/dist/core/env-contract.js +3 -13
  17. package/dist/core/export-diff.js +3 -3
  18. package/dist/core/ignored-directories.js +40 -0
  19. package/dist/core/reference-drift.js +4 -2
  20. package/dist/core/related-files.js +3 -13
  21. package/dist/core/repo-merge-conflict-scan.js +3 -9
  22. package/dist/core/route-outline.js +3 -13
  23. package/dist/core/script-pack-suggestions.js +23 -12
  24. package/dist/core/secret-risk-scan.js +3 -13
  25. package/dist/core/skill-route-resolution.js +21 -1
  26. package/package.json +2 -2
  27. package/schemas/link-integrity-report.schema.json +1 -0
  28. package/schemas/reference-drift-report.schema.json +1 -0
  29. package/templates/default/i18n.toml +19 -7
  30. package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +30 -7
  31. package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +12 -6
  32. package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +20 -9
  33. package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +20 -15
  34. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +22 -7
  35. package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +21 -19
  36. package/templates/default/locales/en/.mustflow/skills/react-code-change/SKILL.md +54 -8
  37. package/templates/default/locales/en/.mustflow/skills/vertical-slice-tdd/SKILL.md +22 -8
  38. package/templates/default/manifest.toml +1 -1
@@ -79,6 +79,14 @@ function reportRunPlanFailure(plan, reporter, lang) {
79
79
  detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
80
80
  });
81
81
  break;
82
+ case 'network_requires_approval':
83
+ case 'destructive_requires_approval':
84
+ case 'approval_policy_unreadable':
85
+ message = t(lang, 'run.error.approvalRequired', {
86
+ intent: plan.intentName,
87
+ detail: getRunPlanDetail(plan, lang, 'run.error.approvalRequiredDetail'),
88
+ });
89
+ break;
82
90
  case 'cwd_outside_project':
83
91
  message = t(lang, 'run.error.cwdOutsideProject', {
84
92
  intent: plan.intentName,
@@ -327,6 +335,9 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
327
335
  return plan.ok ? 0 : 1;
328
336
  }
329
337
  if (!plan.ok) {
338
+ if (json) {
339
+ reporter.stdout(JSON.stringify(createRunPreview(plan, 'plan-only'), null, 2));
340
+ }
330
341
  reportRunPlanFailure(plan, reporter, lang);
331
342
  writeLatestProfile(profiler, options, {
332
343
  projectRoot,
@@ -1238,6 +1238,8 @@ Read these files before working:
1238
1238
  "run.error.blockedShellBackgroundDetail": "Shell commands must not spawn background work.",
1239
1239
  "run.error.blockedLongRunningCommand": 'Intent "{intent}" is blocked. {detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "Command argv must describe a finite one-shot command, not a development server, watcher, shell wrapper, interpreter loop, or background process.",
1241
+ "run.error.approvalRequired": 'Intent "{intent}" requires approval. {detail}',
1242
+ "run.error.approvalRequiredDetail": "The repository approval policy requires explicit approval before this command intent can run.",
1241
1243
  "run.error.cwdOutsideProject": 'Command "{intent}" has an invalid cwd: {detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "Intent cwd must stay inside the current root.",
1243
1245
  "run.error.invalidTestTarget": 'Command "{intent}" received an invalid test target. {detail}',
@@ -1238,6 +1238,8 @@ Lee estos archivos antes de trabajar:
1238
1238
  "run.error.blockedShellBackgroundDetail": "Los comandos de shell no deben iniciar trabajo en segundo plano.",
1239
1239
  "run.error.blockedLongRunningCommand": 'La intención "{intent}" está bloqueada. {detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "argv debe describir un comando finito de una sola ejecución, no un servidor de desarrollo, watcher, envoltorio de shell, bucle de intérprete o proceso en segundo plano.",
1241
+ "run.error.approvalRequired": 'La intención "{intent}" requiere aprobación. {detail}',
1242
+ "run.error.approvalRequiredDetail": "La política de aprobación del repositorio requiere aprobación explícita antes de ejecutar esta intención de comando.",
1241
1243
  "run.error.cwdOutsideProject": 'El comando "{intent}" tiene un cwd no válido: {detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "El cwd de la intención debe permanecer dentro de la raíz actual.",
1243
1245
  "run.error.invalidTestTarget": 'El comando "{intent}" recibió un objetivo de prueba no válido. {detail}',
@@ -1238,6 +1238,8 @@ Lisez ces fichiers avant de travailler :
1238
1238
  "run.error.blockedShellBackgroundDetail": "Les commandes shell ne doivent pas lancer de travail en arrière-plan.",
1239
1239
  "run.error.blockedLongRunningCommand": 'L’intention "{intent}" est bloquée. {detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "argv doit décrire une commande ponctuelle finie, pas un serveur de développement, un watcher, un wrapper shell, une boucle d'interpréteur ou un processus en arrière-plan.",
1241
+ "run.error.approvalRequired": 'L’intention "{intent}" nécessite une approbation. {detail}',
1242
+ "run.error.approvalRequiredDetail": "La politique d’approbation du dépôt exige une approbation explicite avant d’exécuter cette intention de commande.",
1241
1243
  "run.error.cwdOutsideProject": 'La commande "{intent}" a un cwd non valide : {detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "Le cwd de l’intention doit rester dans la racine actuelle.",
1243
1245
  "run.error.invalidTestTarget": 'La commande "{intent}" a reçu une cible de test invalide. {detail}',
@@ -1238,6 +1238,8 @@ export const hiMessages = {
1238
1238
  "run.error.blockedShellBackgroundDetail": "Shell commands background work शुरू नहीं कर सकतीं।",
1239
1239
  "run.error.blockedLongRunningCommand": 'इंटेंट "{intent}" अवरुद्ध है। {detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "argv में finite one-shot command होना चाहिए, development server, watcher, shell wrapper, interpreter loop, या background process नहीं।",
1241
+ "run.error.approvalRequired": 'इंटेंट "{intent}" को approval चाहिए। {detail}',
1242
+ "run.error.approvalRequiredDetail": "Repository approval policy के अनुसार इस command intent को चलाने से पहले explicit approval चाहिए।",
1241
1243
  "run.error.cwdOutsideProject": 'कमांड "{intent}" का cwd अमान्य है: {detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "Intent cwd current root के अंदर रहना चाहिए।",
1243
1245
  "run.error.invalidTestTarget": 'कमांड "{intent}" को अमान्य test target मिला। {detail}',
@@ -1238,6 +1238,8 @@ export const koMessages = {
1238
1238
  "run.error.blockedShellBackgroundDetail": "셸 명령은 백그라운드 작업을 시작하면 안 됩니다.",
1239
1239
  "run.error.blockedLongRunningCommand": '명령 의도 "{intent}"가 차단되었습니다. {detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "argv는 개발 서버, 감시 명령, 셸 래퍼, 인터프리터 반복 작업, 백그라운드 프로세스가 아니라 끝나는 단발성 명령이어야 합니다.",
1241
+ "run.error.approvalRequired": '명령 의도 "{intent}"는 승인이 필요합니다. {detail}',
1242
+ "run.error.approvalRequiredDetail": "저장소 승인 정책상 이 명령 의도를 실행하기 전에 명시적 승인이 필요합니다.",
1241
1243
  "run.error.cwdOutsideProject": '명령 "{intent}"의 실행 위치(cwd)가 올바르지 않습니다: {detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "명령 실행 위치(cwd)는 현재 루트 안에 있어야 합니다.",
1243
1245
  "run.error.invalidTestTarget": '명령 "{intent}"에 올바르지 않은 테스트 대상이 전달되었습니다. {detail}',
@@ -1238,6 +1238,8 @@ export const zhMessages = {
1238
1238
  "run.error.blockedShellBackgroundDetail": "Shell 命令不得启动后台工作。",
1239
1239
  "run.error.blockedLongRunningCommand": '意图 "{intent}" 已被阻止。{detail}',
1240
1240
  "run.error.blockedLongRunningCommandDetail": "argv 必须描述会结束的单次命令,而不是开发服务器、监听命令、shell 包装器、解释器循环或后台进程。",
1241
+ "run.error.approvalRequired": '意图 "{intent}" 需要审批。{detail}',
1242
+ "run.error.approvalRequiredDetail": "仓库审批策略要求在运行此命令意图前获得明确审批。",
1241
1243
  "run.error.cwdOutsideProject": '命令 "{intent}" 的 cwd 无效:{detail}',
1242
1244
  "run.error.cwdOutsideProjectDetail": "意图 cwd 必须位于当前根目录内。",
1243
1245
  "run.error.invalidTestTarget": '命令 "{intent}" 收到无效测试目标。{detail}',
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, renameSync, rmSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { ensureFileTargetInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
@@ -185,6 +185,52 @@ function validateImportRelativeFilePath(relativePath) {
185
185
  function normalizeTextContent(content) {
186
186
  return content.replace(/\r\n?/gu, '\n');
187
187
  }
188
+ function readFrontmatterParts(content) {
189
+ if (!content.startsWith('---')) {
190
+ return {
191
+ name: null,
192
+ description: null,
193
+ body: content,
194
+ };
195
+ }
196
+ const firstLineEnd = content.indexOf('\n');
197
+ const end = firstLineEnd >= 0 ? content.indexOf('\n---', firstLineEnd + 1) : -1;
198
+ if (firstLineEnd < 0 || end < 0) {
199
+ return {
200
+ name: null,
201
+ description: null,
202
+ body: content,
203
+ };
204
+ }
205
+ const frontmatter = content.slice(firstLineEnd + 1, end).split(/\r?\n/u);
206
+ const bodyStart = content.indexOf('\n', end + 1);
207
+ return {
208
+ name: readFrontmatterScalar(content, 'name'),
209
+ description: readFrontmatterScalar(content, 'description'),
210
+ body: bodyStart >= 0 ? content.slice(bodyStart + 1) : '',
211
+ };
212
+ }
213
+ function renderYamlScalar(key, value) {
214
+ return `${key}: ${JSON.stringify(value)}`;
215
+ }
216
+ function sanitizeExternalSkillMarkdown(content) {
217
+ const frontmatter = readFrontmatterParts(content);
218
+ const lines = [
219
+ '---',
220
+ ...(frontmatter.name ? [renderYamlScalar('name', frontmatter.name)] : []),
221
+ ...(frontmatter.description ? [renderYamlScalar('description', frontmatter.description)] : []),
222
+ 'external_authority: untrusted',
223
+ '---',
224
+ frontmatter.body,
225
+ ];
226
+ return lines.join('\n');
227
+ }
228
+ function normalizeImportedSkillFiles(files) {
229
+ return files.map((file) => ({
230
+ ...file,
231
+ content: file.relativePath === 'SKILL.md' ? sanitizeExternalSkillMarkdown(file.content) : file.content,
232
+ }));
233
+ }
188
234
  function hashContent(content) {
189
235
  return `sha256:${createHash('sha256').update(content).digest('hex')}`;
190
236
  }
@@ -314,21 +360,38 @@ function createTarget(skillName) {
314
360
  };
315
361
  }
316
362
  function writeImportedSkillFiles(projectRoot, target, source, files, fileReport, warnings) {
317
- const skillPath = path.join(projectRoot, ...target.skill_dir.split('/'), 'SKILL.md');
318
- if (existsSync(skillPath)) {
363
+ const targetPath = path.join(projectRoot, ...target.skill_dir.split('/'));
364
+ const skillPath = path.join(targetPath, 'SKILL.md');
365
+ if (existsSync(targetPath)) {
319
366
  throw new Error(`External skill already exists: ${target.skill_dir}`);
320
367
  }
321
368
  ensureFileTargetInsideWithoutSymlinks(projectRoot, skillPath, { allowMissingLeaf: true });
322
- for (const file of files) {
323
- writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...target.skill_dir.split('/'), ...file.relativePath.split('/')), file.content);
369
+ const tempSkillDir = `${EXTERNAL_SKILL_ROOT}/.${target.skill_name}.tmp-${process.pid}-${Date.now()}`;
370
+ const tempTarget = {
371
+ ...target,
372
+ skill_dir: tempSkillDir,
373
+ provenance_path: `${tempSkillDir}/${PROVENANCE_FILE}`,
374
+ };
375
+ const tempPath = path.join(projectRoot, ...tempTarget.skill_dir.split('/'));
376
+ const tempSkillPath = path.join(tempPath, 'SKILL.md');
377
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, tempSkillPath, { allowMissingLeaf: true });
378
+ try {
379
+ for (const file of files) {
380
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempTarget.skill_dir.split('/'), ...file.relativePath.split('/')), file.content);
381
+ }
382
+ writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempTarget.provenance_path.split('/')), {
383
+ schema_version: '1',
384
+ kind: 'external_skill_source',
385
+ source,
386
+ files: fileReport,
387
+ warnings,
388
+ });
389
+ renameSync(tempPath, targetPath);
390
+ }
391
+ catch (error) {
392
+ rmSync(tempPath, { recursive: true, force: true });
393
+ throw error;
324
394
  }
325
- writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...target.provenance_path.split('/')), {
326
- schema_version: '1',
327
- kind: 'external_skill_source',
328
- source,
329
- files: fileReport,
330
- warnings,
331
- });
332
395
  }
333
396
  function rejectionReport(mode, issue) {
334
397
  return {
@@ -355,10 +418,11 @@ export async function createExternalSkillImportReport(projectRoot, inputUrl, opt
355
418
  if (typeof fetchImpl !== 'function') {
356
419
  throw new Error('This runtime does not provide fetch.');
357
420
  }
358
- const files = await loadExternalSkillFiles(fetchImpl, parsed);
359
- const skillName = targetNameForSkill(files, parsed, options.name);
421
+ const sourceFiles = await loadExternalSkillFiles(fetchImpl, parsed);
422
+ const skillName = targetNameForSkill(sourceFiles, parsed, options.name);
360
423
  const target = createTarget(skillName);
361
424
  const source = externalSkillSourceFromParsed(inputUrl, parsed);
425
+ const files = normalizeImportedSkillFiles(sourceFiles);
362
426
  const reports = fileReports(files);
363
427
  const warnings = [
364
428
  ...(reports.some((file) => file.kind === 'script')
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
- export async function loadSqlJs() {
2
+ let sqlJsPromise = null;
3
+ async function initializeSqlJs() {
3
4
  const require = createRequire(import.meta.url);
4
5
  const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
5
6
  const sqlJsModule = (await import('sql.js'));
@@ -13,3 +14,10 @@ export async function loadSqlJs() {
13
14
  },
14
15
  });
15
16
  }
17
+ export async function loadSqlJs() {
18
+ sqlJsPromise ??= initializeSqlJs().catch((error) => {
19
+ sqlJsPromise = null;
20
+ throw error;
21
+ });
22
+ return sqlJsPromise;
23
+ }
@@ -7,6 +7,7 @@ import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eli
7
7
  import { inspectActiveRunLocks, } from '../../core/active-run-locks.js';
8
8
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
9
9
  import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, } from '../../core/command-output-limits.js';
10
+ import { checkRepoApprovalGate } from '../../core/repo-approval-gate.js';
10
11
  import { normalizeSuccessExitCodes } from '../../core/success-exit-codes.js';
11
12
  import { normalizeSafeTestTargetPath, TEST_TARGET_PATH_ERROR } from '../../core/test-target-paths.js';
12
13
  import { evaluateCommandPreconditions, } from '../../core/command-preconditions.js';
@@ -124,6 +125,38 @@ function readRunIntentMetadata(contract, intent) {
124
125
  relatedOneshotChecks: readStringArray(intent, 'related_oneshot_checks') ?? [],
125
126
  };
126
127
  }
128
+ function createApprovalBlock(projectRoot, metadata) {
129
+ const actionTypes = [];
130
+ if (metadata.network === true) {
131
+ actionTypes.push('network_access');
132
+ }
133
+ if (metadata.destructive === true) {
134
+ actionTypes.push('destructive_command');
135
+ }
136
+ if (actionTypes.length === 0) {
137
+ return null;
138
+ }
139
+ const approvalReport = checkRepoApprovalGate(projectRoot, actionTypes);
140
+ if (approvalReport.issues.length > 0) {
141
+ return {
142
+ reasonCode: 'approval_policy_unreadable',
143
+ detail: `Could not evaluate ${approvalReport.input.policy_path}: ${approvalReport.issues.join(' ')}`,
144
+ };
145
+ }
146
+ const requiredActions = approvalReport.decisions
147
+ .filter((decision) => decision.approval_required)
148
+ .map((decision) => decision.action_type);
149
+ if (requiredActions.length === 0) {
150
+ return null;
151
+ }
152
+ const reasonCode = requiredActions.includes('destructive_command')
153
+ ? 'destructive_requires_approval'
154
+ : 'network_requires_approval';
155
+ return {
156
+ reasonCode,
157
+ detail: `Action ${requiredActions.map((action) => JSON.stringify(action)).join(', ')} requires explicit approval before mf run can execute this intent.`,
158
+ };
159
+ }
127
160
  function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonCode, detail, preconditions = []) {
128
161
  const metadata = intent ? readRunIntentMetadata(contract, intent) : null;
129
162
  return {
@@ -177,6 +210,10 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
177
210
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, eligibility.code, eligibility.detail, preconditions);
178
211
  }
179
212
  const metadata = readRunIntentMetadata(contract, rawIntent);
213
+ const approvalBlock = createApprovalBlock(projectRoot, metadata);
214
+ if (approvalBlock) {
215
+ return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, approvalBlock.reasonCode, approvalBlock.detail, preconditions);
216
+ }
180
217
  const maxOutputBytesLimitDetail = getCommandMaxOutputBytesLimitDetail(contract, rawIntent);
181
218
  if (maxOutputBytesLimitDetail) {
182
219
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'max_output_bytes_exceeds_limit', maxOutputBytesLimitDetail, preconditions);
@@ -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,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());
@@ -0,0 +1,40 @@
1
+ export const DEFAULT_IGNORED_DIRECTORIES = [
2
+ '.git',
3
+ '.mustflow/cache',
4
+ '.mustflow/state',
5
+ 'node_modules',
6
+ 'dist',
7
+ 'build',
8
+ 'coverage',
9
+ '.next',
10
+ '.turbo',
11
+ '.astro',
12
+ ];
13
+ function normalizeDirectoryPath(value) {
14
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
15
+ }
16
+ function containsSegmentSequence(segments, sequence) {
17
+ if (sequence.length === 0 || sequence.length > segments.length) {
18
+ return false;
19
+ }
20
+ for (let index = 0; index <= segments.length - sequence.length; index += 1) {
21
+ if (sequence.every((segment, offset) => segments[index + offset] === segment)) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+ export function isIgnoredDirectoryPath(relativePath, ignoredDirectories = DEFAULT_IGNORED_DIRECTORIES) {
28
+ const normalized = normalizeDirectoryPath(relativePath);
29
+ if (normalized === '.') {
30
+ return false;
31
+ }
32
+ const segments = normalized.split('/').filter((segment) => segment.length > 0);
33
+ return ignoredDirectories.some((entry) => {
34
+ const ignoredSegments = normalizeDirectoryPath(entry).split('/').filter((segment) => segment.length > 0);
35
+ if (ignoredSegments.length === 1) {
36
+ return segments.includes(ignoredSegments[0] ?? '');
37
+ }
38
+ return containsSegmentSequence(segments, ignoredSegments);
39
+ });
40
+ }
@@ -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 REFERENCE_DRIFT_PACK_ID = 'docs';
6
7
  export const REFERENCE_DRIFT_SCRIPT_ID = 'reference-drift';
@@ -16,7 +17,7 @@ const CHECKED_REFERENCE_KINDS = [
16
17
  'schema_file',
17
18
  'repo_path',
18
19
  ];
19
- const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.astro']);
20
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
20
21
  const ERROR_CODES = new Set([
21
22
  'reference_drift_path_outside_root',
22
23
  'reference_drift_unreadable_path',
@@ -64,7 +65,7 @@ function addCandidate(candidates, findings, issues, policy, candidate) {
64
65
  }
65
66
  function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
66
67
  const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
67
- if (IGNORED_DIRECTORIES.has(path.basename(relativeDirectory)) || [...IGNORED_DIRECTORIES].some((entry) => relativeDirectory.startsWith(`${entry}/`))) {
68
+ if (isIgnoredDirectoryPath(relativeDirectory, IGNORED_DIRECTORIES)) {
68
69
  return;
69
70
  }
70
71
  let entries;
@@ -318,6 +319,7 @@ export function checkReferenceDrift(projectRoot, options) {
318
319
  max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
319
320
  default_paths: [...DEFAULT_PATHS],
320
321
  path_filters: [...PATH_FILTERS],
322
+ ignored_directories: [...IGNORED_DIRECTORIES],
321
323
  checked_reference_kinds: [...CHECKED_REFERENCE_KINDS],
322
324
  };
323
325
  const commandNames = new Set(options.commandNames);