maestro-flow 0.4.20 → 0.4.22

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 (151) hide show
  1. package/.agents/skills/maestro-ralph-execute/SKILL.md +2 -1
  2. package/.agents/skills/maestro-swarm-workflow/SKILL.md +27 -19
  3. package/.agents/skills/maestro-universal-workflow/SKILL.md +563 -0
  4. package/.agents/skills/team-adversarial-swarm/SKILL.md +235 -0
  5. package/.agents/skills/team-adversarial-swarm/scripts/__pycache__/pheromone.cpython-313.pyc +0 -0
  6. package/.agents/skills/team-adversarial-swarm/scripts/__pycache__/scoring.cpython-313.pyc +0 -0
  7. package/.agents/skills/team-adversarial-swarm/scripts/aco.py +473 -0
  8. package/.agents/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
  9. package/.agents/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
  10. package/.agents/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
  11. package/.agents/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
  12. package/.agents/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
  13. package/.agents/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
  14. package/.agents/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
  15. package/.agents/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
  16. package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
  17. package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
  18. package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
  19. package/.agents/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
  20. package/.agents/skills/team-swarm/scripts/aco.py +15 -15
  21. package/.agents/skills/team-swarm/scripts/pheromone.py +2 -2
  22. package/.agents/skills/team-swarm/scripts/scoring.py +1 -1
  23. package/.agy/skills/maestro-ralph-execute/SKILL.md +2 -1
  24. package/.agy/skills/maestro-swarm-workflow/SKILL.md +27 -19
  25. package/.agy/skills/maestro-universal-workflow/SKILL.md +560 -0
  26. package/.agy/skills/team-adversarial-swarm/SKILL.md +244 -0
  27. package/.agy/skills/team-adversarial-swarm/scripts/__pycache__/pheromone.cpython-313.pyc +0 -0
  28. package/.agy/skills/team-adversarial-swarm/scripts/__pycache__/scoring.cpython-313.pyc +0 -0
  29. package/.agy/skills/team-adversarial-swarm/scripts/aco.py +473 -0
  30. package/.agy/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
  31. package/.agy/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
  32. package/.agy/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
  33. package/.agy/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
  34. package/.agy/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
  35. package/.agy/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
  36. package/.agy/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
  37. package/.agy/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
  38. package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
  39. package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
  40. package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
  41. package/.agy/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
  42. package/.agy/skills/team-swarm/scripts/aco.py +15 -15
  43. package/.agy/skills/team-swarm/scripts/pheromone.py +2 -2
  44. package/.agy/skills/team-swarm/scripts/scoring.py +1 -1
  45. package/.claude/commands/maestro-ralph-execute.md +2 -1
  46. package/.claude/commands/maestro-swarm-workflow.md +27 -19
  47. package/.claude/commands/maestro-universal-workflow.md +561 -0
  48. package/.claude/skills/team-adversarial-swarm/SKILL.md +233 -0
  49. package/.claude/skills/team-adversarial-swarm/scripts/__pycache__/pheromone.cpython-313.pyc +0 -0
  50. package/.claude/skills/team-adversarial-swarm/scripts/__pycache__/scoring.cpython-313.pyc +0 -0
  51. package/.claude/skills/team-adversarial-swarm/scripts/aco.py +473 -0
  52. package/.claude/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
  53. package/.claude/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
  54. package/.claude/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
  55. package/.claude/skills/team-adversarial-swarm/specs/ant-output-schema.md +115 -0
  56. package/.claude/skills/team-adversarial-swarm/specs/convergence-criteria.md +75 -0
  57. package/.claude/skills/team-adversarial-swarm/specs/pheromone-schema.md +90 -0
  58. package/.claude/skills/team-adversarial-swarm/specs/swarm-config-template.json +66 -0
  59. package/.claude/skills/team-adversarial-swarm/specs/swarm-protocol.md +105 -0
  60. package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-converge.js +197 -0
  61. package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-explore.js +194 -0
  62. package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-score.js +188 -0
  63. package/.claude/skills/team-adversarial-swarm/workflows/wf-swarm-synthesize.js +248 -0
  64. package/.claude/skills/team-swarm/scripts/aco.py +15 -15
  65. package/.claude/skills/team-swarm/scripts/pheromone.py +2 -2
  66. package/.claude/skills/team-swarm/scripts/scoring.py +1 -1
  67. package/dashboard/dist-server/dashboard/src/server/wiki/graph-analysis.js +1 -1
  68. package/dashboard/dist-server/dashboard/src/server/wiki/graph-analysis.js.map +1 -1
  69. package/dashboard/dist-server/dashboard/src/server/wiki/search.js +1 -1
  70. package/dashboard/dist-server/dashboard/src/server/wiki/search.js.map +1 -1
  71. package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.d.ts +1 -1
  72. package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.js +5 -5
  73. package/dashboard/dist-server/dashboard/src/server/wiki/virtual-wiki-adapters.js.map +1 -1
  74. package/dashboard/dist-server/dashboard/src/server/wiki/wiki-indexer.js +3 -3
  75. package/dashboard/dist-server/dashboard/src/server/wiki/wiki-indexer.js.map +1 -1
  76. package/dashboard/dist-server/src/graph/types.d.ts +111 -0
  77. package/dashboard/dist-server/src/graph/types.js +2 -0
  78. package/dashboard/dist-server/src/graph/types.js.map +1 -0
  79. package/dist/src/commands/install-backend.d.ts +0 -7
  80. package/dist/src/commands/install-backend.d.ts.map +1 -1
  81. package/dist/src/commands/install-backend.js +0 -14
  82. package/dist/src/commands/install-backend.js.map +1 -1
  83. package/dist/src/commands/install.d.ts.map +1 -1
  84. package/dist/src/commands/install.js +0 -18
  85. package/dist/src/commands/install.js.map +1 -1
  86. package/dist/src/commands/kg.d.ts +2 -2
  87. package/dist/src/commands/kg.d.ts.map +1 -1
  88. package/dist/src/commands/kg.js +150 -179
  89. package/dist/src/commands/kg.js.map +1 -1
  90. package/dist/src/graph/analyzers/fs-analyzer.d.ts +10 -0
  91. package/dist/src/graph/analyzers/fs-analyzer.d.ts.map +1 -0
  92. package/dist/src/graph/analyzers/fs-analyzer.js +959 -0
  93. package/dist/src/graph/analyzers/fs-analyzer.js.map +1 -0
  94. package/dist/src/graph/index.d.ts +6 -0
  95. package/dist/src/graph/index.d.ts.map +1 -0
  96. package/dist/src/graph/index.js +6 -0
  97. package/dist/src/graph/index.js.map +1 -0
  98. package/dist/src/graph/loader.d.ts +3 -0
  99. package/dist/src/graph/loader.d.ts.map +1 -0
  100. package/dist/src/graph/loader.js +12 -0
  101. package/dist/src/graph/loader.js.map +1 -0
  102. package/dist/src/graph/merger.d.ts +56 -0
  103. package/dist/src/graph/merger.d.ts.map +1 -0
  104. package/dist/src/graph/merger.js +896 -0
  105. package/dist/src/graph/merger.js.map +1 -0
  106. package/dist/src/graph/query.d.ts +7 -0
  107. package/dist/src/graph/query.d.ts.map +1 -0
  108. package/dist/src/graph/query.js +126 -0
  109. package/dist/src/graph/query.js.map +1 -0
  110. package/dist/src/graph/types.d.ts +112 -0
  111. package/dist/src/graph/types.d.ts.map +1 -0
  112. package/dist/src/graph/types.js +2 -0
  113. package/dist/src/graph/types.js.map +1 -0
  114. package/dist/src/i18n/locales/en.d.ts.map +1 -1
  115. package/dist/src/i18n/locales/en.js +0 -10
  116. package/dist/src/i18n/locales/en.js.map +1 -1
  117. package/dist/src/i18n/locales/zh.d.ts.map +1 -1
  118. package/dist/src/i18n/locales/zh.js +0 -10
  119. package/dist/src/i18n/locales/zh.js.map +1 -1
  120. package/dist/src/i18n/types.d.ts +0 -9
  121. package/dist/src/i18n/types.d.ts.map +1 -1
  122. package/dist/src/tui/install-ui/InstallConfirm.d.ts +0 -1
  123. package/dist/src/tui/install-ui/InstallConfirm.d.ts.map +1 -1
  124. package/dist/src/tui/install-ui/InstallConfirm.js +1 -1
  125. package/dist/src/tui/install-ui/InstallConfirm.js.map +1 -1
  126. package/dist/src/tui/install-ui/InstallExecution.d.ts +0 -1
  127. package/dist/src/tui/install-ui/InstallExecution.d.ts.map +1 -1
  128. package/dist/src/tui/install-ui/InstallExecution.js +0 -22
  129. package/dist/src/tui/install-ui/InstallExecution.js.map +1 -1
  130. package/dist/src/tui/install-ui/InstallFlow.d.ts +1 -1
  131. package/dist/src/tui/install-ui/InstallFlow.d.ts.map +1 -1
  132. package/dist/src/tui/install-ui/InstallFlow.js +5 -23
  133. package/dist/src/tui/install-ui/InstallFlow.js.map +1 -1
  134. package/dist/src/tui/install-ui/InstallHub.d.ts +0 -2
  135. package/dist/src/tui/install-ui/InstallHub.d.ts.map +1 -1
  136. package/dist/src/tui/install-ui/InstallHub.js +0 -6
  137. package/dist/src/tui/install-ui/InstallHub.js.map +1 -1
  138. package/dist/src/tui/install-ui/InstallResult.d.ts.map +1 -1
  139. package/dist/src/tui/install-ui/InstallResult.js +1 -1
  140. package/dist/src/tui/install-ui/InstallResult.js.map +1 -1
  141. package/dist/src/utils/update-notices.js +12 -0
  142. package/dist/src/utils/update-notices.js.map +1 -1
  143. package/package.json +1 -1
  144. package/workflows/swarm/wf-analyze.js +195 -34
  145. package/workflows/swarm/wf-brainstorm.js +225 -53
  146. package/workflows/swarm/wf-execute.js +199 -23
  147. package/workflows/swarm/wf-grill.js +181 -20
  148. package/workflows/swarm/wf-milestone-audit.js +178 -29
  149. package/workflows/swarm/wf-plan.js +288 -53
  150. package/workflows/swarm/wf-review.js +195 -80
  151. package/workflows/swarm/wf-verify.js +125 -28
@@ -0,0 +1,959 @@
1
+ // ---------------------------------------------------------------------------
2
+ // fs-analyzer.ts -- Filesystem-based code analyzer.
3
+ //
4
+ // Recursively walks a project directory, extracts file nodes, import edges,
5
+ // exported symbols, module groupings, and architectural layers.
6
+ //
7
+ // No external dependencies -- uses only Node.js built-in modules.
8
+ // ---------------------------------------------------------------------------
9
+ import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
10
+ import { join, relative, extname, basename, dirname, sep, posix } from 'node:path';
11
+ import { execSync } from 'node:child_process';
12
+ // ---------------------------------------------------------------------------
13
+ // Constants
14
+ // ---------------------------------------------------------------------------
15
+ const SOURCE_EXTENSIONS = new Set([
16
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
17
+ '.vue', '.py', '.go', '.java', '.rs',
18
+ ]);
19
+ /** Extensions that are recognized as config/doc/infra (non-source) files. */
20
+ const NON_SOURCE_EXTENSIONS = new Set([
21
+ '.json', '.yaml', '.yml', '.toml', '.ini',
22
+ '.md', '.txt', '.rst',
23
+ '.dockerfile',
24
+ ]);
25
+ const DEFAULT_EXCLUDES = [
26
+ 'node_modules', 'dist', '.git', '.workflow',
27
+ ];
28
+ const LAYER_PATTERNS = {
29
+ 'commands': { name: 'CLI Commands', description: 'Command-line interface entry points' },
30
+ 'coordinator': { name: 'Workflow Coordinator', description: 'Workflow orchestration and coordination' },
31
+ 'hooks': { name: 'Hook System', description: 'Plugin and extensibility hooks' },
32
+ 'tools': { name: 'Tool Layer', description: 'External tool integrations' },
33
+ 'core': { name: 'Core Infrastructure', description: 'Core modules and shared infrastructure' },
34
+ 'graph': { name: 'Graph Module', description: 'Knowledge graph data structures and queries' },
35
+ 'agents': { name: 'Agent Management', description: 'Agent lifecycle and orchestration' },
36
+ 'async': { name: 'Async Delegation', description: 'Asynchronous task delegation' },
37
+ 'tui': { name: 'Terminal UI', description: 'Terminal user interface components' },
38
+ 'db': { name: 'Backend', description: 'Backend services, data, and middleware' },
39
+ 'services': { name: 'Backend', description: 'Backend services, data, and middleware' },
40
+ 'routes': { name: 'Backend', description: 'Backend services, data, and middleware' },
41
+ 'middleware': { name: 'Backend', description: 'Backend services, data, and middleware' },
42
+ 'config': { name: 'Utilities', description: 'Configuration, utilities, and i18n' },
43
+ 'utils': { name: 'Utilities', description: 'Configuration, utilities, and i18n' },
44
+ 'i18n': { name: 'Utilities', description: 'Configuration, utilities, and i18n' },
45
+ };
46
+ const EXT_LANGUAGE = {
47
+ '.ts': 'TypeScript', '.tsx': 'TypeScript',
48
+ '.js': 'JavaScript', '.jsx': 'JavaScript',
49
+ '.mjs': 'JavaScript', '.cjs': 'JavaScript',
50
+ '.vue': 'Vue', '.py': 'Python',
51
+ '.go': 'Go', '.java': 'Java', '.rs': 'Rust',
52
+ };
53
+ // ---------------------------------------------------------------------------
54
+ // File category classification
55
+ // ---------------------------------------------------------------------------
56
+ /** Config file names and patterns. */
57
+ const CONFIG_NAMES = new Set([
58
+ 'package.json', 'tsconfig.json', 'tsconfig.base.json',
59
+ '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.cjs',
60
+ 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs',
61
+ '.prettierrc', '.prettierrc.js', '.prettierrc.json',
62
+ 'jest.config.js', 'jest.config.ts', 'vitest.config.ts',
63
+ 'webpack.config.js', 'vite.config.ts', 'rollup.config.js',
64
+ '.babelrc', 'babel.config.js',
65
+ 'Makefile', 'CMakeLists.txt',
66
+ 'pyproject.toml', 'setup.py', 'setup.cfg',
67
+ 'go.mod', 'go.sum', 'Cargo.toml', 'Cargo.lock',
68
+ 'pom.xml', 'build.gradle', 'build.gradle.kts',
69
+ ]);
70
+ /** Infra directory patterns. */
71
+ const INFRA_DIRS = new Set([
72
+ '.github', '.gitlab', '.circleci', 'k8s', 'kubernetes',
73
+ 'terraform', 'helm', 'deploy', 'docker', 'infra',
74
+ ]);
75
+ const INFRA_NAMES = new Set([
76
+ 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
77
+ '.dockerignore', 'Vagrantfile',
78
+ ]);
79
+ /** Test directory patterns. */
80
+ const TEST_DIRS = new Set([
81
+ '__tests__', 'test', 'tests', 'spec', 'specs',
82
+ ]);
83
+ const TEST_INFIXES = ['.test.', '.spec.', '_test.', 'test_'];
84
+ /** Classify a file into a category based on name, extension, and path. */
85
+ function classifyFileCategory(relPath, name, ext) {
86
+ // Test files
87
+ const parts = relPath.split(posix.sep);
88
+ if (parts.some(p => TEST_DIRS.has(p)))
89
+ return 'test';
90
+ if (TEST_INFIXES.some(infix => name.includes(infix)))
91
+ return 'test';
92
+ if (name.startsWith('test_') || name.endsWith('_test' + ext))
93
+ return 'test';
94
+ // Config files
95
+ if (CONFIG_NAMES.has(name))
96
+ return 'config';
97
+ if (name.startsWith('.') && (ext === '.json' || ext === '.js' || ext === '.cjs' || ext === '.yaml' || ext === '.yml'))
98
+ return 'config';
99
+ // Infra files
100
+ if (INFRA_NAMES.has(name))
101
+ return 'infra';
102
+ if (parts.some(p => INFRA_DIRS.has(p)))
103
+ return 'infra';
104
+ // Docs
105
+ if (ext === '.md' || ext === '.txt' || ext === '.rst')
106
+ return 'docs';
107
+ // Default: code
108
+ return 'code';
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // Test file detection (aligned with merger.ts)
112
+ // ---------------------------------------------------------------------------
113
+ /** Check if a relative path looks like a test file (aligned with merger.ts isTestPath). */
114
+ function isTestFile(relPath) {
115
+ const name = basename(relPath);
116
+ const ext = extname(name);
117
+ const stem = name.slice(0, name.length - ext.length);
118
+ // JS/TS family: infix pattern
119
+ if (SOURCE_EXTENSIONS.has(ext)) {
120
+ if (stem.endsWith('.test') || stem.endsWith('.spec'))
121
+ return true;
122
+ }
123
+ // Go
124
+ if (ext === '.go' && stem.endsWith('_test'))
125
+ return true;
126
+ // Python
127
+ if (ext === '.py' && (stem.startsWith('test_') || stem.endsWith('_test')))
128
+ return true;
129
+ // Java/Kotlin/C#
130
+ if ((ext === '.java' || ext === '.kt' || ext === '.cs') &&
131
+ (stem.endsWith('Test') || stem.endsWith('Tests') || stem.endsWith('IT')))
132
+ return true;
133
+ // Directory-based
134
+ const parts = relPath.split(posix.sep);
135
+ if (parts.some(p => TEST_DIRS.has(p)))
136
+ return true;
137
+ return false;
138
+ }
139
+ /**
140
+ * For a test file, compute candidate production file paths.
141
+ * Simplified version of merger.ts productionCandidates, covering the
142
+ * most common patterns: sibling de-infix, walk out of test dir, mirrored tree.
143
+ */
144
+ function findProductionFile(testPath, fileSet) {
145
+ const name = basename(testPath);
146
+ const ext = extname(name);
147
+ const stem = name.slice(0, name.length - ext.length);
148
+ const dir = dirname(testPath);
149
+ const dirParts = dir.split(posix.sep).filter(s => s !== '.' && s !== '');
150
+ const JS_TS_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
151
+ // Helper: try candidate path
152
+ function tryPath(candidate) {
153
+ const norm = posix.normalize(candidate);
154
+ return fileSet.has(norm) ? norm : null;
155
+ }
156
+ // Helper: try all JS/TS extensions for a stem in a directory
157
+ function tryStem(dir, baseStem) {
158
+ for (const e of JS_TS_EXTS) {
159
+ const result = tryPath(dir ? `${dir}/${baseStem}${e}` : `${baseStem}${e}`);
160
+ if (result)
161
+ return result;
162
+ }
163
+ return null;
164
+ }
165
+ // JS/TS family: strip .test / .spec infix
166
+ if (SOURCE_EXTENSIONS.has(ext)) {
167
+ let baseStem = null;
168
+ if (stem.endsWith('.test'))
169
+ baseStem = stem.slice(0, -5);
170
+ else if (stem.endsWith('.spec'))
171
+ baseStem = stem.slice(0, -5);
172
+ if (baseStem) {
173
+ // 1. Sibling
174
+ const sibling = tryStem(dir === '.' ? '' : dir, baseStem);
175
+ if (sibling)
176
+ return sibling;
177
+ // 2. Walk out of __tests__ / test / spec directory
178
+ if (dirParts.length > 0 && TEST_DIRS.has(dirParts[dirParts.length - 1])) {
179
+ const parentDir = dirParts.slice(0, -1).join('/');
180
+ const result = tryStem(parentDir, baseStem);
181
+ if (result)
182
+ return result;
183
+ }
184
+ // 3. Mirrored tree (tests/... -> src/...)
185
+ if (dirParts.length > 0 && TEST_DIRS.has(dirParts[0])) {
186
+ const tailPath = dirParts.slice(1).join('/');
187
+ for (const root of ['src', 'app', 'lib', '']) {
188
+ const newDir = [root, tailPath].filter(Boolean).join('/');
189
+ const result = tryStem(newDir, baseStem);
190
+ if (result)
191
+ return result;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // Go
197
+ if (ext === '.go' && stem.endsWith('_test')) {
198
+ const baseStem = stem.slice(0, -5);
199
+ return tryPath(dir === '.' ? `${baseStem}.go` : `${dir}/${baseStem}.go`);
200
+ }
201
+ // Python
202
+ if (ext === '.py') {
203
+ let baseStem = null;
204
+ if (stem.startsWith('test_'))
205
+ baseStem = stem.slice(5);
206
+ else if (stem.endsWith('_test'))
207
+ baseStem = stem.slice(0, -5);
208
+ if (baseStem) {
209
+ const candidate = dir === '.' ? `${baseStem}.py` : `${dir}/${baseStem}.py`;
210
+ return tryPath(candidate);
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Helpers
217
+ // ---------------------------------------------------------------------------
218
+ /** Normalize path separators to forward slashes. */
219
+ function toForward(p) {
220
+ return p.split(sep).join(posix.sep);
221
+ }
222
+ /** Simple glob-like match: supports leading *, trailing *, and exact. */
223
+ function simpleMatch(pattern, value) {
224
+ if (pattern === value)
225
+ return true;
226
+ if (pattern.startsWith('*') && value.endsWith(pattern.slice(1)))
227
+ return true;
228
+ if (pattern.endsWith('*') && value.startsWith(pattern.slice(0, -1)))
229
+ return true;
230
+ if (pattern.startsWith('*') && pattern.endsWith('*')) {
231
+ return value.includes(pattern.slice(1, -1));
232
+ }
233
+ // Support *.test.* style patterns
234
+ if (pattern.includes('*')) {
235
+ const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
236
+ return regex.test(value);
237
+ }
238
+ return false;
239
+ }
240
+ /** Check whether a file or directory should be excluded. */
241
+ function shouldExclude(name, relPath, excludes) {
242
+ for (const pattern of excludes) {
243
+ if (simpleMatch(pattern, name))
244
+ return true;
245
+ if (simpleMatch(pattern, relPath))
246
+ return true;
247
+ // Also check if any path segment matches (e.g. "node_modules" deep in tree)
248
+ const segments = relPath.split(posix.sep);
249
+ if (segments.some(seg => simpleMatch(pattern, seg)))
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+ /** Determine complexity heuristic from line count (legacy, kept for backward compat). */
255
+ function complexityFromLines(lineCount) {
256
+ if (lineCount < 100)
257
+ return 'simple';
258
+ if (lineCount <= 300)
259
+ return 'moderate';
260
+ return 'complex';
261
+ }
262
+ /**
263
+ * Enhanced complexity heuristic factoring in line count, exports, imports,
264
+ * and nesting depth. Returns a score-based classification.
265
+ */
266
+ function enhancedComplexity(lineCount, exportCount, importCount, content) {
267
+ // Base score from lines
268
+ let score = 0;
269
+ if (lineCount >= 300)
270
+ score += 3;
271
+ else if (lineCount >= 100)
272
+ score += 2;
273
+ else
274
+ score += 1;
275
+ // Export complexity: more public surface = more complex interface
276
+ if (exportCount >= 10)
277
+ score += 2;
278
+ else if (exportCount >= 5)
279
+ score += 1;
280
+ // Import coupling: many dependencies = higher complexity
281
+ if (importCount >= 10)
282
+ score += 2;
283
+ else if (importCount >= 5)
284
+ score += 1;
285
+ // Nesting depth heuristic: count deeply nested blocks
286
+ let depth = 0;
287
+ let maxDepth = 0;
288
+ for (let i = 0; i < content.length; i++) {
289
+ if (content[i] === '{') {
290
+ depth++;
291
+ if (depth > maxDepth)
292
+ maxDepth = depth;
293
+ }
294
+ else if (content[i] === '}') {
295
+ depth = Math.max(0, depth - 1);
296
+ }
297
+ }
298
+ if (maxDepth >= 6)
299
+ score += 2;
300
+ else if (maxDepth >= 4)
301
+ score += 1;
302
+ if (score <= 2)
303
+ return 'simple';
304
+ if (score <= 5)
305
+ return 'moderate';
306
+ return 'complex';
307
+ }
308
+ /** Derive tags from directory name and file extension. */
309
+ function deriveTags(relPath, ext) {
310
+ const tags = [];
311
+ const parts = relPath.split(posix.sep);
312
+ // Add first meaningful directory as tag
313
+ if (parts.length > 1) {
314
+ tags.push(parts[0]);
315
+ }
316
+ // Add language tag
317
+ const lang = EXT_LANGUAGE[ext];
318
+ if (lang)
319
+ tags.push(lang.toLowerCase());
320
+ return tags;
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Git-aware file enumeration
324
+ // ---------------------------------------------------------------------------
325
+ /**
326
+ * Use `git ls-files` to enumerate tracked + untracked (non-ignored) files.
327
+ * Returns null if git is unavailable or the directory is not a git repo.
328
+ */
329
+ function gitLsFiles(root) {
330
+ try {
331
+ const output = execSync('git ls-files -z -co --exclude-standard', {
332
+ cwd: root,
333
+ encoding: 'utf-8',
334
+ timeout: 10000,
335
+ stdio: ['pipe', 'pipe', 'pipe'],
336
+ });
337
+ return output.split('\0').filter(f => f.length > 0);
338
+ }
339
+ catch {
340
+ return null;
341
+ }
342
+ }
343
+ // ---------------------------------------------------------------------------
344
+ // Import extraction
345
+ // ---------------------------------------------------------------------------
346
+ /** Extract import targets from source code. */
347
+ function extractImports(content) {
348
+ const targets = [];
349
+ // ESM: import ... from '...'
350
+ const esmRegex = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
351
+ let match;
352
+ while ((match = esmRegex.exec(content)) !== null) {
353
+ targets.push(match[1]);
354
+ }
355
+ // CJS: require('...')
356
+ const cjsRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
357
+ while ((match = cjsRegex.exec(content)) !== null) {
358
+ targets.push(match[1]);
359
+ }
360
+ return targets;
361
+ }
362
+ /** Extract imports with named symbols for call graph analysis. */
363
+ function extractImportsWithSymbols(content) {
364
+ const results = [];
365
+ // ESM: import { foo, bar } from '...'
366
+ // ESM: import DefaultName from '...'
367
+ // ESM: import DefaultName, { foo } from '...'
368
+ const esmRegex = /import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
369
+ let match;
370
+ while ((match = esmRegex.exec(content)) !== null) {
371
+ const clause = match[1];
372
+ const specifier = match[2];
373
+ const symbols = [];
374
+ let defaultImport;
375
+ // Extract named imports: { foo, bar, baz as qux }
376
+ const namedMatch = clause.match(/\{([^}]+)\}/);
377
+ if (namedMatch) {
378
+ const parts = namedMatch[1].split(',');
379
+ for (const p of parts) {
380
+ const trimmed = p.trim();
381
+ if (!trimmed)
382
+ continue;
383
+ // handle "foo as bar" -> use the local name "bar"
384
+ const asMatch = trimmed.match(/(\w+)\s+as\s+(\w+)/);
385
+ if (asMatch) {
386
+ symbols.push(asMatch[2]);
387
+ }
388
+ else if (/^\w+$/.test(trimmed)) {
389
+ symbols.push(trimmed);
390
+ }
391
+ }
392
+ }
393
+ // Extract default import
394
+ const defaultMatch = clause.match(/^(\w+)/);
395
+ if (defaultMatch && defaultMatch[1] !== 'type') {
396
+ defaultImport = defaultMatch[1];
397
+ }
398
+ results.push({ specifier, symbols, defaultImport });
399
+ }
400
+ return results;
401
+ }
402
+ /** Extract exported symbol names from source code. */
403
+ function extractExports(content) {
404
+ const exports = [];
405
+ const seen = new Set();
406
+ // export function/class/interface/type/const/enum
407
+ const namedRegex = /export\s+(?:default\s+)?(?:async\s+)?(function|class|interface|type|const|let|var|enum)\s+(\w+)/g;
408
+ let match;
409
+ while ((match = namedRegex.exec(content)) !== null) {
410
+ const kind = match[1];
411
+ const name = match[2];
412
+ if (!seen.has(name)) {
413
+ seen.add(name);
414
+ exports.push({ name, kind });
415
+ }
416
+ }
417
+ return exports;
418
+ }
419
+ // ---------------------------------------------------------------------------
420
+ // Call graph extraction
421
+ // ---------------------------------------------------------------------------
422
+ /**
423
+ * Find call sites in content that reference imported symbols.
424
+ * Returns the set of symbol names that are actually called.
425
+ */
426
+ function extractCallSites(content, importedSymbols) {
427
+ const called = new Set();
428
+ if (importedSymbols.size === 0)
429
+ return called;
430
+ // Build a regex that matches any imported symbol followed by '('
431
+ // This catches: symbolName(, obj.symbolName( patterns
432
+ for (const sym of importedSymbols) {
433
+ // Match: word boundary + symbol + optional whitespace + '('
434
+ // Exclude: import/export/from/type keywords followed by the symbol
435
+ const pattern = new RegExp(`(?<!\\.)\\b${escapeRegex(sym)}\\s*\\(`, 'g');
436
+ if (pattern.test(content)) {
437
+ called.add(sym);
438
+ }
439
+ }
440
+ return called;
441
+ }
442
+ /** Escape special regex characters. */
443
+ function escapeRegex(str) {
444
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
445
+ }
446
+ // ---------------------------------------------------------------------------
447
+ // Topological sort (Kahn's algorithm)
448
+ // ---------------------------------------------------------------------------
449
+ /** Entry-point file names that should appear first in tour. */
450
+ const ENTRY_POINT_NAMES = new Set([
451
+ 'index.ts', 'index.js', 'index.tsx', 'index.jsx',
452
+ 'main.ts', 'main.js', 'cli.ts', 'cli.js',
453
+ 'app.ts', 'app.js', 'server.ts', 'server.js',
454
+ ]);
455
+ /**
456
+ * Topological sort of module names using Kahn's algorithm.
457
+ * Modules with no incoming edges (entry points) come first.
458
+ * Falls back to alphabetical for cycles.
459
+ */
460
+ function topologicalSortModules(modules, moduleEdges, entryModules) {
461
+ const inDegree = new Map();
462
+ const adjacency = new Map();
463
+ for (const m of modules) {
464
+ inDegree.set(m, 0);
465
+ adjacency.set(m, []);
466
+ }
467
+ for (const edge of moduleEdges) {
468
+ if (!inDegree.has(edge.source) || !inDegree.has(edge.target))
469
+ continue;
470
+ if (edge.source === edge.target)
471
+ continue;
472
+ adjacency.get(edge.source).push(edge.target);
473
+ inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
474
+ }
475
+ // Priority queue: entry modules first, then by in-degree (ascending)
476
+ const queue = [];
477
+ const result = [];
478
+ const visited = new Set();
479
+ // Seed with zero-degree nodes, prioritizing entry modules
480
+ const zeroDegree = modules.filter(m => (inDegree.get(m) ?? 0) === 0);
481
+ const entryFirst = zeroDegree.filter(m => entryModules.has(m));
482
+ const rest = zeroDegree.filter(m => !entryModules.has(m)).sort();
483
+ queue.push(...entryFirst, ...rest);
484
+ while (queue.length > 0) {
485
+ const current = queue.shift();
486
+ if (visited.has(current))
487
+ continue;
488
+ visited.add(current);
489
+ result.push(current);
490
+ const neighbors = (adjacency.get(current) ?? []).sort();
491
+ for (const neighbor of neighbors) {
492
+ const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
493
+ inDegree.set(neighbor, newDeg);
494
+ if (newDeg === 0 && !visited.has(neighbor)) {
495
+ queue.push(neighbor);
496
+ }
497
+ }
498
+ }
499
+ // Add any remaining nodes (cycles) in alphabetical order
500
+ for (const m of modules.sort()) {
501
+ if (!visited.has(m)) {
502
+ result.push(m);
503
+ }
504
+ }
505
+ return result;
506
+ }
507
+ // ---------------------------------------------------------------------------
508
+ // File resolution
509
+ // ---------------------------------------------------------------------------
510
+ /** Known source extensions for resolution attempts. */
511
+ const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
512
+ /**
513
+ * Resolve a relative import specifier to a file: node ID.
514
+ * Returns null if the import is a package (not relative).
515
+ */
516
+ function resolveImport(importSpecifier, sourceRelPath, fileSet) {
517
+ // Only resolve relative imports
518
+ if (!importSpecifier.startsWith('.'))
519
+ return null;
520
+ const sourceDir = dirname(sourceRelPath);
521
+ let resolved = posix.normalize(posix.join(sourceDir, importSpecifier));
522
+ // Strip .js extension that TypeScript uses in ESM imports
523
+ if (resolved.endsWith('.js')) {
524
+ resolved = resolved.slice(0, -3);
525
+ }
526
+ // Try exact match first
527
+ if (fileSet.has(resolved))
528
+ return `file:${resolved}`;
529
+ // Try adding extensions
530
+ for (const ext of RESOLVE_EXTENSIONS) {
531
+ if (fileSet.has(resolved + ext))
532
+ return `file:${resolved + ext}`;
533
+ }
534
+ // Try index file in directory
535
+ for (const ext of RESOLVE_EXTENSIONS) {
536
+ const indexPath = posix.join(resolved, `index${ext}`);
537
+ if (fileSet.has(indexPath))
538
+ return `file:${indexPath}`;
539
+ }
540
+ return null;
541
+ }
542
+ function walkDirectory(root, options) {
543
+ const entries = [];
544
+ function walk(dir) {
545
+ let items;
546
+ try {
547
+ items = readdirSync(dir);
548
+ }
549
+ catch {
550
+ return;
551
+ }
552
+ for (const item of items) {
553
+ const fullPath = join(dir, item);
554
+ const rel = toForward(relative(root, fullPath));
555
+ let stat;
556
+ try {
557
+ stat = statSync(fullPath);
558
+ }
559
+ catch {
560
+ continue;
561
+ }
562
+ if (stat.isDirectory()) {
563
+ if (!shouldExclude(item, rel, options.excludes)) {
564
+ walk(fullPath);
565
+ }
566
+ continue;
567
+ }
568
+ if (!stat.isFile())
569
+ continue;
570
+ const ext = extname(item).toLowerCase();
571
+ if (!SOURCE_EXTENSIONS.has(ext) && !NON_SOURCE_EXTENSIONS.has(ext))
572
+ continue;
573
+ if (shouldExclude(item, rel, options.excludes))
574
+ continue;
575
+ // Apply include filter if specified
576
+ if (options.includes.length > 0) {
577
+ const matched = options.includes.some(p => simpleMatch(p, rel) || simpleMatch(p, item));
578
+ if (!matched)
579
+ continue;
580
+ }
581
+ entries.push({ absolutePath: fullPath, relPath: rel });
582
+ }
583
+ }
584
+ walk(root);
585
+ return entries;
586
+ }
587
+ /**
588
+ * Git-aware file enumeration. Uses `git ls-files` for accurate file listing
589
+ * that respects .gitignore. Falls back to walkDirectory on failure.
590
+ */
591
+ function gitWalkDirectory(root, options) {
592
+ const gitFiles = gitLsFiles(root);
593
+ if (!gitFiles)
594
+ return walkDirectory(root, options);
595
+ const entries = [];
596
+ for (const relFile of gitFiles) {
597
+ const rel = toForward(relFile);
598
+ const name = basename(rel);
599
+ const ext = extname(name).toLowerCase();
600
+ // Filter by known extensions
601
+ if (!SOURCE_EXTENSIONS.has(ext) && !NON_SOURCE_EXTENSIONS.has(ext))
602
+ continue;
603
+ // Apply exclusion rules
604
+ if (shouldExclude(name, rel, options.excludes))
605
+ continue;
606
+ // Apply include filter
607
+ if (options.includes.length > 0) {
608
+ const matched = options.includes.some(p => simpleMatch(p, rel) || simpleMatch(p, name));
609
+ if (!matched)
610
+ continue;
611
+ }
612
+ entries.push({ absolutePath: join(root, relFile), relPath: rel });
613
+ }
614
+ return entries;
615
+ }
616
+ // ---------------------------------------------------------------------------
617
+ // FsAnalyzer
618
+ // ---------------------------------------------------------------------------
619
+ export class FsAnalyzer {
620
+ name = 'fs-analyzer';
621
+ async analyze(projectRoot, options) {
622
+ const root = projectRoot;
623
+ const excludes = options?.exclude ?? DEFAULT_EXCLUDES;
624
+ const includes = options?.include ?? [];
625
+ // 1. Walk filesystem -- prefer git ls-files when available
626
+ const files = gitWalkDirectory(root, { includes, excludes });
627
+ const fileSet = new Set(files.map(f => f.relPath));
628
+ // 2. Build nodes, edges, and collect metadata
629
+ const nodes = [];
630
+ const edges = [];
631
+ const languagesFound = new Set();
632
+ const moduleFiles = new Map(); // module dir -> file node IDs
633
+ // Track exported symbols per file for call graph extraction
634
+ const fileExportedSymbols = new Map(); // fileId -> Set<symbolName>
635
+ // Track imports with symbols per file for call graph
636
+ const fileImportSymbols = new Map();
637
+ // Track test files for tested_by linking
638
+ const testFiles = [];
639
+ // Track module-level import edges for topological sort
640
+ const moduleImportEdges = [];
641
+ // Track entry-point modules
642
+ const entryModules = new Set();
643
+ for (const file of files) {
644
+ const ext = extname(file.relPath).toLowerCase();
645
+ const name = basename(file.relPath);
646
+ const parts = file.relPath.split(posix.sep);
647
+ const moduleDir = parts.length > 1 ? parts[0] : '_root';
648
+ const isSource = SOURCE_EXTENSIONS.has(ext);
649
+ // Track language
650
+ const lang = EXT_LANGUAGE[ext];
651
+ if (lang)
652
+ languagesFound.add(lang);
653
+ // Classify file category
654
+ const category = classifyFileCategory(file.relPath, name, ext);
655
+ // For non-source files, create lightweight nodes without parsing
656
+ if (!isSource) {
657
+ const fileId = `file:${file.relPath}`;
658
+ const tags = deriveTags(file.relPath, ext);
659
+ tags.push(category);
660
+ nodes.push({
661
+ id: fileId,
662
+ type: 'file',
663
+ name,
664
+ filePath: file.relPath,
665
+ summary: `${category} file: ${name}`,
666
+ tags,
667
+ complexity: 'simple',
668
+ });
669
+ if (!moduleFiles.has(moduleDir))
670
+ moduleFiles.set(moduleDir, []);
671
+ moduleFiles.get(moduleDir).push(fileId);
672
+ continue;
673
+ }
674
+ // Read file content (source files only)
675
+ let content;
676
+ try {
677
+ content = readFileSync(file.absolutePath, 'utf-8');
678
+ }
679
+ catch {
680
+ continue;
681
+ }
682
+ const lineCount = content.split('\n').length;
683
+ // Extract exports and imports for enhanced complexity
684
+ const exportedSymbols = extractExports(content);
685
+ const importTargets = extractImports(content);
686
+ const importInfos = extractImportsWithSymbols(content);
687
+ // Create file node with enhanced complexity and category tag
688
+ const fileId = `file:${file.relPath}`;
689
+ const tags = deriveTags(file.relPath, ext);
690
+ tags.push(category);
691
+ const complexity = enhancedComplexity(lineCount, exportedSymbols.length, importTargets.length, content);
692
+ // Detect entry points
693
+ if (ENTRY_POINT_NAMES.has(name)) {
694
+ entryModules.add(moduleDir);
695
+ }
696
+ // Track test files for tested_by linking
697
+ const isTest = isTestFile(file.relPath);
698
+ if (isTest && !tags.includes('test')) {
699
+ tags.push('test');
700
+ }
701
+ nodes.push({
702
+ id: fileId,
703
+ type: 'file',
704
+ name,
705
+ filePath: file.relPath,
706
+ summary: `${lang ?? 'Source'} file in ${moduleDir} module`,
707
+ tags,
708
+ complexity,
709
+ });
710
+ if (isTest) {
711
+ testFiles.push({ relPath: file.relPath, fileId });
712
+ }
713
+ // Track module membership
714
+ if (!moduleFiles.has(moduleDir))
715
+ moduleFiles.set(moduleDir, []);
716
+ moduleFiles.get(moduleDir).push(fileId);
717
+ // Track exported symbols for call graph
718
+ const exportNames = new Set(exportedSymbols.map(s => s.name));
719
+ fileExportedSymbols.set(fileId, exportNames);
720
+ // Extract exports as child nodes
721
+ for (const sym of exportedSymbols) {
722
+ const symId = `${sym.kind}:${file.relPath}:${sym.name}`;
723
+ nodes.push({
724
+ id: symId,
725
+ type: sym.kind,
726
+ name: sym.name,
727
+ filePath: file.relPath,
728
+ summary: `Exported ${sym.kind} "${sym.name}" in ${name}`,
729
+ tags: [...tags.filter(t => t !== 'test' && t !== category), sym.kind],
730
+ });
731
+ edges.push({
732
+ source: fileId,
733
+ target: symId,
734
+ type: 'contains',
735
+ direction: 'forward',
736
+ weight: 1,
737
+ });
738
+ }
739
+ // Extract imports as edges + track for call graph
740
+ const importSymbolsForFile = [];
741
+ for (const target of importTargets) {
742
+ const resolvedId = resolveImport(target, file.relPath, fileSet);
743
+ if (resolvedId) {
744
+ edges.push({
745
+ source: fileId,
746
+ target: resolvedId,
747
+ type: 'imports',
748
+ direction: 'forward',
749
+ weight: 1,
750
+ });
751
+ // Track module-level dependencies for topological sort
752
+ const targetRelPath = resolvedId.slice('file:'.length);
753
+ const targetParts = targetRelPath.split(posix.sep);
754
+ const targetModule = targetParts.length > 1 ? targetParts[0] : '_root';
755
+ if (moduleDir !== targetModule && moduleDir !== '_root' && targetModule !== '_root') {
756
+ moduleImportEdges.push({ source: moduleDir, target: targetModule });
757
+ }
758
+ }
759
+ }
760
+ // Track named import symbols for call graph extraction
761
+ for (const info of importInfos) {
762
+ const resolvedId = resolveImport(info.specifier, file.relPath, fileSet);
763
+ if (resolvedId && info.symbols.length > 0) {
764
+ importSymbolsForFile.push({ targetFileId: resolvedId, symbols: info.symbols });
765
+ }
766
+ }
767
+ if (importSymbolsForFile.length > 0) {
768
+ fileImportSymbols.set(fileId, importSymbolsForFile);
769
+ }
770
+ }
771
+ // 2b. Call graph extraction: find call sites for imported symbols
772
+ for (const file of files) {
773
+ const ext = extname(file.relPath).toLowerCase();
774
+ if (!SOURCE_EXTENSIONS.has(ext))
775
+ continue;
776
+ const fileId = `file:${file.relPath}`;
777
+ const importedRefs = fileImportSymbols.get(fileId);
778
+ if (!importedRefs || importedRefs.length === 0)
779
+ continue;
780
+ let content;
781
+ try {
782
+ content = readFileSync(file.absolutePath, 'utf-8');
783
+ }
784
+ catch {
785
+ continue;
786
+ }
787
+ // Collect all imported symbols and their source files
788
+ const symbolToFile = new Map();
789
+ for (const ref of importedRefs) {
790
+ for (const sym of ref.symbols) {
791
+ symbolToFile.set(sym, ref.targetFileId);
792
+ }
793
+ }
794
+ const calledSymbols = extractCallSites(content, new Set(symbolToFile.keys()));
795
+ for (const sym of calledSymbols) {
796
+ const targetFileId = symbolToFile.get(sym);
797
+ if (!targetFileId)
798
+ continue;
799
+ edges.push({
800
+ source: fileId,
801
+ target: targetFileId,
802
+ type: 'calls',
803
+ direction: 'forward',
804
+ weight: 0.9,
805
+ description: `Calls imported symbol "${sym}"`,
806
+ });
807
+ }
808
+ }
809
+ // 2c. Test file pairing: create tested_by edges
810
+ for (const test of testFiles) {
811
+ const prodPath = findProductionFile(test.relPath, fileSet);
812
+ if (prodPath) {
813
+ const prodFileId = `file:${prodPath}`;
814
+ edges.push({
815
+ source: prodFileId,
816
+ target: test.fileId,
817
+ type: 'tested_by',
818
+ direction: 'forward',
819
+ weight: 0.8,
820
+ });
821
+ // Add "tested" tag to the production node
822
+ const prodNode = nodes.find(n => n.id === prodFileId);
823
+ if (prodNode && !prodNode.tags.includes('tested')) {
824
+ prodNode.tags.push('tested');
825
+ }
826
+ }
827
+ }
828
+ // 3. Create module nodes
829
+ for (const [moduleDir, memberIds] of moduleFiles) {
830
+ if (moduleDir === '_root')
831
+ continue;
832
+ const moduleId = `module:${moduleDir}`;
833
+ nodes.push({
834
+ id: moduleId,
835
+ type: 'module',
836
+ name: moduleDir,
837
+ summary: `Module: ${moduleDir} (${memberIds.length} files)`,
838
+ tags: [moduleDir],
839
+ });
840
+ for (const memberId of memberIds) {
841
+ edges.push({
842
+ source: moduleId,
843
+ target: memberId,
844
+ type: 'contains',
845
+ direction: 'forward',
846
+ weight: 1,
847
+ });
848
+ }
849
+ }
850
+ // 4. Build layers from directory patterns
851
+ const layerMap = new Map();
852
+ for (const [moduleDir] of moduleFiles) {
853
+ if (moduleDir === '_root')
854
+ continue;
855
+ const patternEntry = LAYER_PATTERNS[moduleDir];
856
+ if (!patternEntry)
857
+ continue;
858
+ const layerId = `layer:${patternEntry.name.toLowerCase().replace(/\s+/g, '-')}`;
859
+ if (!layerMap.has(layerId)) {
860
+ layerMap.set(layerId, {
861
+ id: layerId,
862
+ name: patternEntry.name,
863
+ description: patternEntry.description,
864
+ nodeIds: [],
865
+ });
866
+ }
867
+ const layer = layerMap.get(layerId);
868
+ // Add module node and its file nodes
869
+ layer.nodeIds.push(`module:${moduleDir}`);
870
+ const memberIds = moduleFiles.get(moduleDir);
871
+ if (memberIds) {
872
+ layer.nodeIds.push(...memberIds);
873
+ }
874
+ }
875
+ const layers = Array.from(layerMap.values());
876
+ // 5. Generate tour using topological sort
877
+ const tour = [];
878
+ let order = 1;
879
+ const moduleList = Array.from(moduleFiles.keys()).filter(m => m !== '_root');
880
+ const sortedModules = topologicalSortModules(moduleList, moduleImportEdges, entryModules);
881
+ for (const moduleDir of sortedModules) {
882
+ const memberIds = moduleFiles.get(moduleDir) ?? [];
883
+ const patternEntry = LAYER_PATTERNS[moduleDir];
884
+ const layerName = patternEntry?.name ?? moduleDir;
885
+ tour.push({
886
+ order: order++,
887
+ title: layerName,
888
+ description: patternEntry?.description ?? `Files in the ${moduleDir} directory`,
889
+ nodeIds: [`module:${moduleDir}`, ...memberIds.slice(0, 5)],
890
+ });
891
+ }
892
+ // 6. Assemble project metadata
893
+ const project = {
894
+ name: this.detectProjectName(root),
895
+ languages: Array.from(languagesFound).sort(),
896
+ frameworks: [],
897
+ description: `Code analysis of ${files.length} source files`,
898
+ analyzedAt: new Date().toISOString(),
899
+ };
900
+ // Try to detect frameworks from package.json
901
+ project.frameworks = this.detectFrameworks(root);
902
+ return {
903
+ version: '1.0.0',
904
+ valid: true,
905
+ project,
906
+ nodes,
907
+ edges,
908
+ layers,
909
+ tour,
910
+ };
911
+ }
912
+ /** Read project name from package.json or use directory name. */
913
+ detectProjectName(root) {
914
+ const pkgPath = join(root, 'package.json');
915
+ if (existsSync(pkgPath)) {
916
+ try {
917
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
918
+ if (pkg.name)
919
+ return pkg.name;
920
+ }
921
+ catch { /* fallback */ }
922
+ }
923
+ return basename(root);
924
+ }
925
+ /** Detect frameworks from package.json dependencies. */
926
+ detectFrameworks(root) {
927
+ const pkgPath = join(root, 'package.json');
928
+ if (!existsSync(pkgPath))
929
+ return [];
930
+ try {
931
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
932
+ const allDeps = {
933
+ ...pkg.dependencies,
934
+ ...pkg.devDependencies,
935
+ };
936
+ const frameworks = [];
937
+ const checks = [
938
+ ['react', 'React'],
939
+ ['vue', 'Vue'],
940
+ ['angular', 'Angular'],
941
+ ['express', 'Express'],
942
+ ['fastify', 'Fastify'],
943
+ ['next', 'Next.js'],
944
+ ['nuxt', 'Nuxt'],
945
+ ['commander', 'Commander'],
946
+ ['ink', 'Ink'],
947
+ ];
948
+ for (const [pkg, name] of checks) {
949
+ if (allDeps?.[pkg])
950
+ frameworks.push(name);
951
+ }
952
+ return frameworks;
953
+ }
954
+ catch {
955
+ return [];
956
+ }
957
+ }
958
+ }
959
+ //# sourceMappingURL=fs-analyzer.js.map