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
@@ -248,6 +248,56 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
248
248
  '--json',
249
249
  ],
250
250
  },
251
+ {
252
+ id: 'dependency-graph-report',
253
+ schemaFile: 'dependency-graph-report.schema.json',
254
+ producer: 'mf script-pack run code/dependency-graph scan <path...> --json',
255
+ packaged: true,
256
+ documented: true,
257
+ installedCommand: [
258
+ 'mf',
259
+ 'script-pack',
260
+ 'run',
261
+ 'code/dependency-graph',
262
+ 'scan',
263
+ 'node_modules/mustflow/dist/cli/index.js',
264
+ '--json',
265
+ ],
266
+ },
267
+ {
268
+ id: 'import-cycle-report',
269
+ schemaFile: 'import-cycle-report.schema.json',
270
+ producer: 'mf script-pack run code/import-cycle check <path...> --json',
271
+ packaged: true,
272
+ documented: true,
273
+ installedCommand: [
274
+ 'mf',
275
+ 'script-pack',
276
+ 'run',
277
+ 'code/import-cycle',
278
+ 'check',
279
+ 'node_modules/mustflow/dist/cli/index.js',
280
+ '--json',
281
+ ],
282
+ expectedExitCodes: [0, 1],
283
+ },
284
+ {
285
+ id: 'change-impact-report',
286
+ schemaFile: 'change-impact-report.schema.json',
287
+ producer: 'mf script-pack run code/change-impact analyze [path...] --json',
288
+ packaged: true,
289
+ documented: true,
290
+ installedCommand: [
291
+ 'mf',
292
+ 'script-pack',
293
+ 'run',
294
+ 'code/change-impact',
295
+ 'analyze',
296
+ '--base',
297
+ 'HEAD',
298
+ '--json',
299
+ ],
300
+ },
251
301
  {
252
302
  id: 'code-symbol-read-report',
253
303
  schemaFile: 'code-symbol-read-report.schema.json',
@@ -309,6 +359,42 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
309
359
  ],
310
360
  expectedExitCodes: [0, 1],
311
361
  },
362
+ {
363
+ id: 'link-integrity-report',
364
+ schemaFile: 'link-integrity-report.schema.json',
365
+ producer: 'mf script-pack run docs/link-integrity check [path...] --json',
366
+ packaged: true,
367
+ documented: true,
368
+ installedCommand: [
369
+ 'mf',
370
+ 'script-pack',
371
+ 'run',
372
+ 'docs/link-integrity',
373
+ 'check',
374
+ 'node_modules/mustflow/README.md',
375
+ 'node_modules/mustflow/schemas/README.md',
376
+ '--json',
377
+ ],
378
+ expectedExitCodes: [0, 1],
379
+ },
380
+ {
381
+ id: 'test-performance-report',
382
+ schemaFile: 'test-performance-report.schema.json',
383
+ producer: 'mf script-pack run test/performance-report summarize --json',
384
+ packaged: true,
385
+ documented: true,
386
+ installedCommand: ['mf', 'script-pack', 'run', 'test/performance-report', 'summarize', '--json'],
387
+ expectedExitCodes: [0, 1],
388
+ },
389
+ {
390
+ id: 'test-regression-selector-report',
391
+ schemaFile: 'test-regression-selector-report.schema.json',
392
+ producer: 'mf script-pack run test/regression-selector select --json',
393
+ packaged: true,
394
+ documented: true,
395
+ installedCommand: ['mf', 'script-pack', 'run', 'test/regression-selector', 'select', '--json'],
396
+ expectedExitCodes: [0, 1],
397
+ },
312
398
  {
313
399
  id: 'text-budget-report',
314
400
  schemaFile: 'text-budget-report.schema.json',
@@ -346,6 +432,69 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
346
432
  ],
347
433
  expectedExitCodes: [0, 1],
348
434
  },
435
+ {
436
+ id: 'repo-merge-conflict-scan-report',
437
+ schemaFile: 'repo-merge-conflict-scan-report.schema.json',
438
+ producer: 'mf script-pack run repo/merge-conflict-scan check [path...] --json',
439
+ packaged: true,
440
+ documented: true,
441
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/merge-conflict-scan', 'check', '--json'],
442
+ expectedExitCodes: [0, 1],
443
+ },
444
+ {
445
+ id: 'repo-git-ignore-audit-report',
446
+ schemaFile: 'repo-git-ignore-audit-report.schema.json',
447
+ producer: 'mf script-pack run repo/git-ignore-audit audit [path...] --json',
448
+ packaged: true,
449
+ documented: true,
450
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/git-ignore-audit', 'audit', 'AGENTS.md', '--json'],
451
+ expectedExitCodes: [0, 1],
452
+ },
453
+ {
454
+ id: 'repo-manifest-lock-drift-report',
455
+ schemaFile: 'repo-manifest-lock-drift-report.schema.json',
456
+ producer: 'mf script-pack run repo/manifest-lock-drift check [path...] --json',
457
+ packaged: true,
458
+ documented: true,
459
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/manifest-lock-drift', 'check', 'AGENTS.md', '--json'],
460
+ expectedExitCodes: [0, 1],
461
+ },
462
+ {
463
+ id: 'skill-route-audit-report',
464
+ schemaFile: 'skill-route-audit-report.schema.json',
465
+ producer: 'mf script-pack run repo/skill-route-audit audit --json',
466
+ packaged: true,
467
+ documented: true,
468
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/skill-route-audit', 'audit', '--json'],
469
+ expectedExitCodes: [0, 1],
470
+ },
471
+ {
472
+ id: 'repo-version-source-report',
473
+ schemaFile: 'repo-version-source-report.schema.json',
474
+ producer: 'mf script-pack run repo/version-source inspect --json',
475
+ packaged: true,
476
+ documented: true,
477
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/version-source', 'inspect', '--json'],
478
+ expectedExitCodes: [0, 1],
479
+ },
480
+ {
481
+ id: 'repo-approval-gate-report',
482
+ schemaFile: 'repo-approval-gate-report.schema.json',
483
+ producer: 'mf script-pack run repo/approval-gate check --action <type> --json',
484
+ packaged: true,
485
+ documented: true,
486
+ installedCommand: [
487
+ 'mf',
488
+ 'script-pack',
489
+ 'run',
490
+ 'repo/approval-gate',
491
+ 'check',
492
+ '--action',
493
+ 'git_commit',
494
+ '--json',
495
+ ],
496
+ expectedExitCodes: [0, 1],
497
+ },
349
498
  {
350
499
  id: 'config-chain-report',
351
500
  schemaFile: 'config-chain-report.schema.json',
@@ -363,6 +512,24 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
363
512
  ],
364
513
  expectedExitCodes: [0, 1],
365
514
  },
515
+ {
516
+ id: 'env-contract-report',
517
+ schemaFile: 'env-contract-report.schema.json',
518
+ producer: 'mf script-pack run repo/env-contract scan [path...] --json',
519
+ packaged: true,
520
+ documented: true,
521
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/env-contract', 'scan', 'AGENTS.md', '--json'],
522
+ expectedExitCodes: [0, 1],
523
+ },
524
+ {
525
+ id: 'secret-risk-scan-report',
526
+ schemaFile: 'secret-risk-scan-report.schema.json',
527
+ producer: 'mf script-pack run repo/secret-risk-scan scan [path...] --json',
528
+ packaged: true,
529
+ documented: true,
530
+ installedCommand: ['mf', 'script-pack', 'run', 'repo/secret-risk-scan', 'scan', 'AGENTS.md', '--json'],
531
+ expectedExitCodes: [0, 1],
532
+ },
366
533
  {
367
534
  id: 'related-files-report',
368
535
  schemaFile: 'related-files-report.schema.json',
@@ -0,0 +1,116 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { isRecord, MUSTFLOW_CONFIG_RELATIVE_PATH, readMustflowConfigIfExists, readStringArray, } from './config-loading.js';
4
+ export const REPO_APPROVAL_GATE_PACK_ID = 'repo';
5
+ export const REPO_APPROVAL_GATE_SCRIPT_ID = 'approval-gate';
6
+ export const REPO_APPROVAL_GATE_SCRIPT_REF = `${REPO_APPROVAL_GATE_PACK_ID}/${REPO_APPROVAL_GATE_SCRIPT_ID}`;
7
+ export const REPO_APPROVAL_GATE_POLICY_PATH = MUSTFLOW_CONFIG_RELATIVE_PATH;
8
+ function sha256(value) {
9
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
10
+ }
11
+ function uniqueActions(actions) {
12
+ const normalized = [];
13
+ const seen = new Set();
14
+ for (const action of actions) {
15
+ const trimmed = action.trim();
16
+ if (trimmed.length === 0 || seen.has(trimmed)) {
17
+ continue;
18
+ }
19
+ seen.add(trimmed);
20
+ normalized.push(trimmed);
21
+ }
22
+ return normalized;
23
+ }
24
+ function readApprovalPolicy(projectRoot, issues) {
25
+ let config;
26
+ try {
27
+ config = readMustflowConfigIfExists(projectRoot);
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ issues.push(`Could not read ${REPO_APPROVAL_GATE_POLICY_PATH}: ${message}`);
32
+ }
33
+ if (!config) {
34
+ issues.push(`${REPO_APPROVAL_GATE_POLICY_PATH} is missing.`);
35
+ return { required_for: [], on_required: null };
36
+ }
37
+ if (!isRecord(config.approval)) {
38
+ issues.push(`[approval] in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a TOML table.`);
39
+ return { required_for: [], on_required: null };
40
+ }
41
+ const approval = config.approval;
42
+ const requiredFor = readStringArray(approval, 'required_for');
43
+ if (!requiredFor) {
44
+ issues.push(`[approval].required_for in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a string array.`);
45
+ }
46
+ const onRequired = approval.on_required;
47
+ if (onRequired !== undefined && typeof onRequired !== 'string') {
48
+ issues.push(`[approval].on_required in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a string.`);
49
+ }
50
+ return {
51
+ required_for: requiredFor ?? [],
52
+ on_required: typeof onRequired === 'string' ? onRequired : null,
53
+ };
54
+ }
55
+ function createDecisions(actionTypes, policy) {
56
+ const required = new Set(policy.required_for);
57
+ return actionTypes.map((actionType) => {
58
+ const approvalRequired = required.has(actionType);
59
+ return {
60
+ action_type: actionType,
61
+ approval_required: approvalRequired,
62
+ policy_source: approvalRequired ? `${REPO_APPROVAL_GATE_POLICY_PATH}#[approval].required_for` : null,
63
+ reason: approvalRequired
64
+ ? `Action "${actionType}" is listed in [approval].required_for.`
65
+ : `Action "${actionType}" is not listed in [approval].required_for.`,
66
+ };
67
+ });
68
+ }
69
+ function createFindings(decisions) {
70
+ return decisions
71
+ .filter((decision) => decision.approval_required)
72
+ .map((decision) => ({
73
+ code: 'approval_required_for_action',
74
+ severity: 'high',
75
+ path: REPO_APPROVAL_GATE_POLICY_PATH,
76
+ message: `Action "${decision.action_type}" requires explicit approval before proceeding.`,
77
+ json_pointer: '/approval/required_for',
78
+ metric: null,
79
+ actual: null,
80
+ expected: null,
81
+ }));
82
+ }
83
+ function createInputHash(reportInput) {
84
+ return sha256(JSON.stringify(reportInput));
85
+ }
86
+ export function checkRepoApprovalGate(projectRoot, actionTypes) {
87
+ const root = path.resolve(projectRoot);
88
+ const actions = uniqueActions(actionTypes);
89
+ const issues = [];
90
+ const policy = readApprovalPolicy(root, issues);
91
+ const decisions = issues.length > 0 ? [] : createDecisions(actions, policy);
92
+ const findings = createFindings(decisions);
93
+ const approvalRequired = findings.length > 0;
94
+ const status = issues.length > 0 ? 'error' : approvalRequired ? 'failed' : 'passed';
95
+ return {
96
+ schema_version: '1',
97
+ command: 'script-pack',
98
+ pack_id: REPO_APPROVAL_GATE_PACK_ID,
99
+ script_id: REPO_APPROVAL_GATE_SCRIPT_ID,
100
+ script_ref: REPO_APPROVAL_GATE_SCRIPT_REF,
101
+ action: 'check',
102
+ status,
103
+ ok: status === 'passed',
104
+ mustflow_root: root,
105
+ input: {
106
+ action_types: actions,
107
+ policy_path: REPO_APPROVAL_GATE_POLICY_PATH,
108
+ },
109
+ input_hash: createInputHash({ actionTypes: actions, policy, decisions, findings, issues }),
110
+ approval_required: approvalRequired,
111
+ policy,
112
+ decisions,
113
+ findings,
114
+ issues,
115
+ };
116
+ }
@@ -0,0 +1,302 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { ensureInside, ensureInsideWithoutSymlinks } from './safe-filesystem.js';
7
+ export const REPO_GIT_IGNORE_AUDIT_PACK_ID = 'repo';
8
+ export const REPO_GIT_IGNORE_AUDIT_SCRIPT_ID = 'git-ignore-audit';
9
+ export const REPO_GIT_IGNORE_AUDIT_SCRIPT_REF = `${REPO_GIT_IGNORE_AUDIT_PACK_ID}/${REPO_GIT_IGNORE_AUDIT_SCRIPT_ID}`;
10
+ const DEFAULT_MAX_PATHS = 200;
11
+ const SOURCE_KINDS = [
12
+ 'repo_gitignore',
13
+ 'git_info_exclude',
14
+ 'core_excludes_file',
15
+ ];
16
+ function normalizeRelativePath(value) {
17
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
18
+ }
19
+ function sha256(value) {
20
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
21
+ }
22
+ function positiveInteger(value, fallback) {
23
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
24
+ }
25
+ function runGit(root, args) {
26
+ const result = spawnSync('git', [...args], {
27
+ cwd: root,
28
+ encoding: 'utf8',
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ windowsHide: true,
31
+ maxBuffer: 16 * 1024 * 1024,
32
+ });
33
+ return {
34
+ ok: result.status === 0,
35
+ status: result.status,
36
+ stdout: result.stdout ?? '',
37
+ stderr: result.stderr ?? '',
38
+ };
39
+ }
40
+ function isInsideGitWorktree(root) {
41
+ const result = runGit(root, ['rev-parse', '--is-inside-work-tree']);
42
+ return result.ok && result.stdout.trim() === 'true';
43
+ }
44
+ function parseGitPaths(stdout) {
45
+ const paths = [];
46
+ const seen = new Set();
47
+ for (const line of stdout.split(/\r?\n/u)) {
48
+ const relativePath = normalizeRelativePath(line.trim());
49
+ if (!relativePath || relativePath === '.' || seen.has(relativePath)) {
50
+ continue;
51
+ }
52
+ seen.add(relativePath);
53
+ paths.push(relativePath);
54
+ }
55
+ return paths.sort((left, right) => left.localeCompare(right));
56
+ }
57
+ function collectGitChangedFiles(root, issues, findings) {
58
+ if (!isInsideGitWorktree(root)) {
59
+ const message = 'Git worktree is unavailable; provide explicit paths to audit.';
60
+ issues.push(message);
61
+ findings.push(makeFinding('git_ignore_audit_git_unavailable', 'low', '.', message, null));
62
+ return [];
63
+ }
64
+ const tracked = runGit(root, ['diff', '--name-only', '--diff-filter=ACMRTUXB', 'HEAD', '--']);
65
+ const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard', '--']);
66
+ const paths = new Set();
67
+ if (tracked.ok) {
68
+ for (const entry of parseGitPaths(tracked.stdout)) {
69
+ paths.add(entry);
70
+ }
71
+ }
72
+ else {
73
+ issues.push(`Could not inspect changed tracked files: ${tracked.stderr.trim() || 'git diff failed'}`);
74
+ }
75
+ if (untracked.ok) {
76
+ for (const entry of parseGitPaths(untracked.stdout)) {
77
+ paths.add(entry);
78
+ }
79
+ }
80
+ else {
81
+ issues.push(`Could not inspect untracked files: ${untracked.stderr.trim() || 'git ls-files failed'}`);
82
+ }
83
+ return [...paths].sort((left, right) => left.localeCompare(right));
84
+ }
85
+ function resolveAuditPaths(root, inputPaths, maxPaths, findings, issues) {
86
+ const paths = [];
87
+ const seen = new Set();
88
+ for (const inputPath of inputPaths) {
89
+ const absolutePath = path.resolve(root, inputPath);
90
+ let relativePath;
91
+ try {
92
+ ensureInside(root, absolutePath);
93
+ ensureInsideWithoutSymlinks(root, existsSync(absolutePath) ? absolutePath : path.dirname(absolutePath), {
94
+ allowMissingDescendant: true,
95
+ });
96
+ relativePath = normalizeRelativePath(path.relative(root, absolutePath));
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ issues.push(`${inputPath}: ${message}`);
101
+ findings.push(makeFinding('git_ignore_audit_path_outside_root', 'high', inputPath, message, null));
102
+ continue;
103
+ }
104
+ if (seen.has(relativePath)) {
105
+ continue;
106
+ }
107
+ if (paths.length >= maxPaths) {
108
+ const message = `Git ignore audit reached max_paths ${maxPaths}; remaining paths were skipped.`;
109
+ issues.push(message);
110
+ findings.push(makeFinding('git_ignore_audit_max_paths_exceeded', 'high', relativePath, message, null));
111
+ return paths;
112
+ }
113
+ seen.add(relativePath);
114
+ paths.push(relativePath);
115
+ }
116
+ return paths.sort((left, right) => left.localeCompare(right));
117
+ }
118
+ function expandHome(value) {
119
+ if (value === '~') {
120
+ return os.homedir();
121
+ }
122
+ if (value.startsWith('~/') || value.startsWith('~\\')) {
123
+ return path.join(os.homedir(), value.slice(2));
124
+ }
125
+ return value;
126
+ }
127
+ function readExternalCoreExcludesPath(root) {
128
+ const local = runGit(root, ['config', '--get', 'core.excludesFile']);
129
+ if (!local.ok || local.stdout.trim().length === 0) {
130
+ return null;
131
+ }
132
+ return expandHome(local.stdout.trim());
133
+ }
134
+ function makeSource(root, sourcePath, kind, scope) {
135
+ const absolutePath = scope === 'external' ? path.resolve(expandHome(sourcePath)) : path.join(root, ...sourcePath.split('/'));
136
+ const exists = existsSync(absolutePath);
137
+ if (!exists || scope === 'external') {
138
+ return {
139
+ path: scope === 'external' ? sourcePath : normalizeRelativePath(sourcePath),
140
+ kind,
141
+ exists,
142
+ scope,
143
+ sha256: null,
144
+ };
145
+ }
146
+ try {
147
+ const stats = lstatSync(absolutePath);
148
+ if (!stats.isFile()) {
149
+ return { path: normalizeRelativePath(sourcePath), kind, exists: true, scope, sha256: null };
150
+ }
151
+ return {
152
+ path: normalizeRelativePath(sourcePath),
153
+ kind,
154
+ exists: true,
155
+ scope,
156
+ sha256: sha256(readFileSync(absolutePath)),
157
+ };
158
+ }
159
+ catch {
160
+ return { path: normalizeRelativePath(sourcePath), kind, exists: true, scope, sha256: null };
161
+ }
162
+ }
163
+ function inspectIgnoreSources(root) {
164
+ const sources = [
165
+ makeSource(root, '.gitignore', 'repo_gitignore', 'repository'),
166
+ makeSource(root, '.git/info/exclude', 'git_info_exclude', 'local_git'),
167
+ ];
168
+ const coreExcludesPath = readExternalCoreExcludesPath(root);
169
+ if (coreExcludesPath) {
170
+ sources.push(makeSource(root, coreExcludesPath, 'core_excludes_file', 'external'));
171
+ }
172
+ return sources;
173
+ }
174
+ function parseIgnoreMatch(output) {
175
+ const line = output.split(/\r?\n/u).find((entry) => entry.trim().length > 0);
176
+ if (!line) {
177
+ return null;
178
+ }
179
+ const tabIndex = line.indexOf('\t');
180
+ const evidence = tabIndex >= 0 ? line.slice(0, tabIndex) : line;
181
+ const match = /^(?<sourcePath>.*):(?<sourceLine>\d+):(?<pattern>.*)$/u.exec(evidence);
182
+ const sourcePath = match?.groups?.sourcePath ?? '';
183
+ const sourceLine = Number(match?.groups?.sourceLine);
184
+ const pattern = match?.groups?.pattern ?? '';
185
+ if (!Number.isInteger(sourceLine) || sourceLine < 1 || pattern.length === 0) {
186
+ return null;
187
+ }
188
+ return { sourcePath: normalizeRelativePath(sourcePath), sourceLine, pattern };
189
+ }
190
+ function checkIgnoreMatch(root, relativePath, issues) {
191
+ const result = runGit(root, ['check-ignore', '-v', '--no-index', '--', relativePath]);
192
+ if (result.ok) {
193
+ return parseIgnoreMatch(result.stdout);
194
+ }
195
+ if (result.status !== 1 && result.stderr.trim().length > 0) {
196
+ issues.push(`${relativePath}: ${result.stderr.trim()}`);
197
+ }
198
+ return null;
199
+ }
200
+ function isTrackedPath(root, relativePath) {
201
+ const result = runGit(root, ['ls-files', '--error-unmatch', '--', relativePath]);
202
+ return result.ok;
203
+ }
204
+ function pathExists(root, relativePath) {
205
+ return existsSync(path.join(root, ...relativePath.split('/')));
206
+ }
207
+ function makeFinding(code, severity, pathValue, message, match) {
208
+ return {
209
+ code,
210
+ severity,
211
+ path: pathValue,
212
+ source_path: match?.sourcePath ?? null,
213
+ source_line: match?.sourceLine ?? null,
214
+ pattern: match?.pattern ?? null,
215
+ message,
216
+ json_pointer: null,
217
+ metric: null,
218
+ actual: null,
219
+ expected: null,
220
+ };
221
+ }
222
+ function auditPath(root, relativePath, findings, issues) {
223
+ const match = checkIgnoreMatch(root, relativePath, issues);
224
+ const tracked = isTrackedPath(root, relativePath);
225
+ const exists = pathExists(root, relativePath);
226
+ const ignored = match !== null;
227
+ const status = tracked ? 'tracked' : ignored ? 'ignored' : exists ? 'untracked' : 'missing';
228
+ if (ignored && tracked) {
229
+ findings.push(makeFinding('git_ignore_audit_tracked_path_matches_ignore', 'low', relativePath, `Tracked path ${relativePath} matches an ignore rule, but Git will keep tracking it.`, match));
230
+ }
231
+ else if (ignored) {
232
+ findings.push(makeFinding('git_ignore_audit_ignored_path', 'medium', relativePath, `Path ${relativePath} is ignored by ${match.sourcePath}:${match.sourceLine}.`, match));
233
+ }
234
+ return {
235
+ path: relativePath,
236
+ status,
237
+ tracked,
238
+ ignored,
239
+ exists,
240
+ source_path: match?.sourcePath ?? null,
241
+ source_line: match?.sourceLine ?? null,
242
+ pattern: match?.pattern ?? null,
243
+ };
244
+ }
245
+ function summarize(paths, sources, findings) {
246
+ return {
247
+ paths_checked: paths.length,
248
+ ignored_paths: paths.filter((entry) => entry.ignored).length,
249
+ tracked_paths: paths.filter((entry) => entry.tracked).length,
250
+ untracked_paths: paths.filter((entry) => entry.status === 'untracked').length,
251
+ missing_paths: paths.filter((entry) => entry.status === 'missing').length,
252
+ ignore_sources: sources.filter((source) => source.exists).length,
253
+ findings: findings.length,
254
+ };
255
+ }
256
+ function createInputHash(reportInput) {
257
+ return sha256(JSON.stringify(reportInput));
258
+ }
259
+ export function auditRepoGitIgnore(projectRoot, options) {
260
+ const root = path.resolve(projectRoot);
261
+ const requestedPaths = options.paths.length > 0 ? [...options.paths] : [];
262
+ const findings = [];
263
+ const issues = [];
264
+ const policy = {
265
+ input_mode: requestedPaths.length > 0 ? 'explicit_paths' : 'git_changed_files',
266
+ max_paths: positiveInteger(options.maxPaths, DEFAULT_MAX_PATHS),
267
+ source_kinds: SOURCE_KINDS,
268
+ tracked_paths_can_match_ignore: true,
269
+ };
270
+ const effectivePaths = requestedPaths.length > 0 ? requestedPaths : collectGitChangedFiles(root, issues, findings);
271
+ const auditTargets = resolveAuditPaths(root, effectivePaths, policy.max_paths, findings, issues);
272
+ const sources = inspectIgnoreSources(root);
273
+ const paths = auditTargets.map((entry) => auditPath(root, entry, findings, issues));
274
+ if (effectivePaths.length === 0 && issues.length === 0) {
275
+ const message = 'No input paths were available for Git ignore auditing.';
276
+ findings.push(makeFinding('git_ignore_audit_no_input_paths', 'low', '.', message, null));
277
+ }
278
+ const summary = summarize(paths, sources, findings);
279
+ const hasHighFinding = findings.some((finding) => finding.severity === 'high' || finding.severity === 'critical');
280
+ const status = hasHighFinding || issues.length > 0 ? 'error' : findings.length > 0 ? 'failed' : 'passed';
281
+ return {
282
+ schema_version: '1',
283
+ command: 'script-pack',
284
+ pack_id: REPO_GIT_IGNORE_AUDIT_PACK_ID,
285
+ script_id: REPO_GIT_IGNORE_AUDIT_SCRIPT_ID,
286
+ script_ref: REPO_GIT_IGNORE_AUDIT_SCRIPT_REF,
287
+ action: 'audit',
288
+ status,
289
+ ok: status === 'passed',
290
+ mustflow_root: root,
291
+ input: {
292
+ paths: requestedPaths,
293
+ },
294
+ policy,
295
+ input_hash: createInputHash({ inputPaths: requestedPaths, policy, sources, paths, findings, issues }),
296
+ summary,
297
+ sources,
298
+ paths,
299
+ findings,
300
+ issues,
301
+ };
302
+ }