pan-wizard 2.8.1

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 (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -0
  3. package/agents/pan-debugger.md +1246 -0
  4. package/agents/pan-document_code.md +965 -0
  5. package/agents/pan-executor.md +469 -0
  6. package/agents/pan-integration-checker.md +443 -0
  7. package/agents/pan-phase-researcher.md +572 -0
  8. package/agents/pan-plan-checker.md +763 -0
  9. package/agents/pan-planner.md +1297 -0
  10. package/agents/pan-project-researcher.md +647 -0
  11. package/agents/pan-research-synthesizer.md +239 -0
  12. package/agents/pan-reviewer.md +112 -0
  13. package/agents/pan-roadmapper.md +642 -0
  14. package/agents/pan-verifier.md +672 -0
  15. package/assets/pan-logo-2000-transparent.svg +30 -0
  16. package/assets/pan-logo-2000.svg +43 -0
  17. package/assets/terminal.svg +119 -0
  18. package/bin/install-lib.cjs +616 -0
  19. package/bin/install.js +1936 -0
  20. package/commands/pan/add-phase.md +44 -0
  21. package/commands/pan/assumptions.md +47 -0
  22. package/commands/pan/audit-deployment.md +378 -0
  23. package/commands/pan/debug.md +168 -0
  24. package/commands/pan/discord.md +19 -0
  25. package/commands/pan/discuss-phase.md +84 -0
  26. package/commands/pan/exec-phase.md +45 -0
  27. package/commands/pan/focus-auto.md +323 -0
  28. package/commands/pan/focus-design.md +816 -0
  29. package/commands/pan/focus-exec.md +316 -0
  30. package/commands/pan/focus-plan.md +101 -0
  31. package/commands/pan/focus-scan.md +272 -0
  32. package/commands/pan/focus-sync.md +104 -0
  33. package/commands/pan/health.md +23 -0
  34. package/commands/pan/help.md +23 -0
  35. package/commands/pan/insert-phase.md +33 -0
  36. package/commands/pan/map-codebase.md +72 -0
  37. package/commands/pan/milestone-audit.md +37 -0
  38. package/commands/pan/milestone-cleanup.md +19 -0
  39. package/commands/pan/milestone-done.md +137 -0
  40. package/commands/pan/milestone-gaps.md +35 -0
  41. package/commands/pan/milestone-new.md +45 -0
  42. package/commands/pan/new-project.md +43 -0
  43. package/commands/pan/patches.md +110 -0
  44. package/commands/pan/pause.md +39 -0
  45. package/commands/pan/phase-budget.md +23 -0
  46. package/commands/pan/phase-tests.md +42 -0
  47. package/commands/pan/plan-phase.md +46 -0
  48. package/commands/pan/profile.md +36 -0
  49. package/commands/pan/progress.md +25 -0
  50. package/commands/pan/quick.md +42 -0
  51. package/commands/pan/remove-phase.md +32 -0
  52. package/commands/pan/research-phase.md +190 -0
  53. package/commands/pan/resume.md +41 -0
  54. package/commands/pan/retro.md +33 -0
  55. package/commands/pan/settings.md +37 -0
  56. package/commands/pan/todo-add.md +48 -0
  57. package/commands/pan/todo-check.md +46 -0
  58. package/commands/pan/update.md +38 -0
  59. package/commands/pan/verify-phase.md +39 -0
  60. package/hooks/dist/pan-check-update.js +62 -0
  61. package/hooks/dist/pan-context-monitor.js +122 -0
  62. package/hooks/dist/pan-statusline.js +108 -0
  63. package/package.json +66 -0
  64. package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
  65. package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
  66. package/pan-wizard-core/bin/lib/config.cjs +611 -0
  67. package/pan-wizard-core/bin/lib/constants.cjs +696 -0
  68. package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
  69. package/pan-wizard-core/bin/lib/core.cjs +650 -0
  70. package/pan-wizard-core/bin/lib/focus.cjs +900 -0
  71. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
  72. package/pan-wizard-core/bin/lib/init.cjs +881 -0
  73. package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
  74. package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
  75. package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
  76. package/pan-wizard-core/bin/lib/state.cjs +1029 -0
  77. package/pan-wizard-core/bin/lib/template.cjs +314 -0
  78. package/pan-wizard-core/bin/lib/utils.cjs +171 -0
  79. package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
  80. package/pan-wizard-core/bin/pan-tools.cjs +773 -0
  81. package/pan-wizard-core/references/checkpoints.md +776 -0
  82. package/pan-wizard-core/references/continuation-format.md +249 -0
  83. package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
  84. package/pan-wizard-core/references/git-integration.md +248 -0
  85. package/pan-wizard-core/references/git-planning-commit.md +38 -0
  86. package/pan-wizard-core/references/model-profile-resolution.md +34 -0
  87. package/pan-wizard-core/references/model-profiles.md +111 -0
  88. package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
  89. package/pan-wizard-core/references/planning-config.md +196 -0
  90. package/pan-wizard-core/references/questioning.md +145 -0
  91. package/pan-wizard-core/references/tdd.md +263 -0
  92. package/pan-wizard-core/references/ui-brand.md +160 -0
  93. package/pan-wizard-core/references/verification-patterns.md +612 -0
  94. package/pan-wizard-core/templates/codebase/architecture.md +283 -0
  95. package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
  96. package/pan-wizard-core/templates/codebase/concerns.md +325 -0
  97. package/pan-wizard-core/templates/codebase/conventions.md +307 -0
  98. package/pan-wizard-core/templates/codebase/integrations.md +305 -0
  99. package/pan-wizard-core/templates/codebase/relationships.md +124 -0
  100. package/pan-wizard-core/templates/codebase/stack.md +199 -0
  101. package/pan-wizard-core/templates/codebase/structure.md +298 -0
  102. package/pan-wizard-core/templates/codebase/testing.md +480 -0
  103. package/pan-wizard-core/templates/config.json +37 -0
  104. package/pan-wizard-core/templates/context.md +283 -0
  105. package/pan-wizard-core/templates/continue-here.md +78 -0
  106. package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
  107. package/pan-wizard-core/templates/debug.md +164 -0
  108. package/pan-wizard-core/templates/discovery.md +146 -0
  109. package/pan-wizard-core/templates/milestone-archive.md +123 -0
  110. package/pan-wizard-core/templates/milestone.md +115 -0
  111. package/pan-wizard-core/templates/phase-prompt.md +593 -0
  112. package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
  113. package/pan-wizard-core/templates/project.md +184 -0
  114. package/pan-wizard-core/templates/requirements.md +231 -0
  115. package/pan-wizard-core/templates/research-project/architecture.md +204 -0
  116. package/pan-wizard-core/templates/research-project/features.md +147 -0
  117. package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
  118. package/pan-wizard-core/templates/research-project/stack.md +120 -0
  119. package/pan-wizard-core/templates/research-project/summary.md +170 -0
  120. package/pan-wizard-core/templates/research.md +552 -0
  121. package/pan-wizard-core/templates/retrospective.md +54 -0
  122. package/pan-wizard-core/templates/roadmap.md +202 -0
  123. package/pan-wizard-core/templates/standards.md +24 -0
  124. package/pan-wizard-core/templates/state.md +176 -0
  125. package/pan-wizard-core/templates/summary-complex.md +59 -0
  126. package/pan-wizard-core/templates/summary-minimal.md +41 -0
  127. package/pan-wizard-core/templates/summary-standard.md +49 -0
  128. package/pan-wizard-core/templates/summary.md +249 -0
  129. package/pan-wizard-core/templates/uat.md +247 -0
  130. package/pan-wizard-core/templates/user-setup.md +311 -0
  131. package/pan-wizard-core/templates/validation.md +76 -0
  132. package/pan-wizard-core/templates/verification-report.md +322 -0
  133. package/pan-wizard-core/workflows/add-phase.md +111 -0
  134. package/pan-wizard-core/workflows/assumptions.md +178 -0
  135. package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
  136. package/pan-wizard-core/workflows/discuss-phase.md +542 -0
  137. package/pan-wizard-core/workflows/exec-phase.md +572 -0
  138. package/pan-wizard-core/workflows/execute-plan.md +448 -0
  139. package/pan-wizard-core/workflows/health.md +156 -0
  140. package/pan-wizard-core/workflows/help.md +431 -0
  141. package/pan-wizard-core/workflows/insert-phase.md +129 -0
  142. package/pan-wizard-core/workflows/map-codebase.md +401 -0
  143. package/pan-wizard-core/workflows/milestone-audit.md +297 -0
  144. package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
  145. package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
  146. package/pan-wizard-core/workflows/milestone-new.md +382 -0
  147. package/pan-wizard-core/workflows/new-project.md +1178 -0
  148. package/pan-wizard-core/workflows/pause.md +122 -0
  149. package/pan-wizard-core/workflows/phase-tests.md +388 -0
  150. package/pan-wizard-core/workflows/plan-phase.md +569 -0
  151. package/pan-wizard-core/workflows/profile.md +115 -0
  152. package/pan-wizard-core/workflows/progress.md +381 -0
  153. package/pan-wizard-core/workflows/quick.md +453 -0
  154. package/pan-wizard-core/workflows/remove-phase.md +154 -0
  155. package/pan-wizard-core/workflows/research-phase.md +73 -0
  156. package/pan-wizard-core/workflows/resume-project.md +306 -0
  157. package/pan-wizard-core/workflows/retro.md +121 -0
  158. package/pan-wizard-core/workflows/settings.md +213 -0
  159. package/pan-wizard-core/workflows/todo-add.md +157 -0
  160. package/pan-wizard-core/workflows/todo-check.md +176 -0
  161. package/pan-wizard-core/workflows/transition.md +544 -0
  162. package/pan-wizard-core/workflows/update.md +219 -0
  163. package/pan-wizard-core/workflows/verify-phase.md +301 -0
  164. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Codebase — Language-aware import analysis, dependency graphs, best-practices detection
3
+ *
4
+ * Zero runtime dependencies: uses regex-based parsing only (no AST, no tree-sitter).
5
+ * Supports JS/TS (v0), with extensible language registry for Python/Go/Rust/Java/C# (v1).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { CODEBASE_DIR, DRIFT_MAX_FILE_SIZE } = require('./constants.cjs');
11
+ const { output, error, safeReadFile, toPosix } = require('./core.cjs');
12
+ const { planningPath } = require('./utils.cjs');
13
+
14
+ // ─── Language Detection ─────────────────────────────────────────────────────
15
+
16
+ /** Extension → language mapping */
17
+ const EXTENSION_MAP = {
18
+ '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
19
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
20
+ '.py': 'python', '.pyw': 'python',
21
+ '.go': 'go',
22
+ '.rs': 'rust',
23
+ '.java': 'java',
24
+ '.cs': 'csharp',
25
+ };
26
+
27
+ /** Package manifest → language mapping */
28
+ const MANIFEST_MAP = {
29
+ 'package.json': 'javascript',
30
+ 'tsconfig.json': 'typescript',
31
+ 'requirements.txt': 'python',
32
+ 'pyproject.toml': 'python',
33
+ 'setup.py': 'python',
34
+ 'go.mod': 'go',
35
+ 'Cargo.toml': 'rust',
36
+ 'pom.xml': 'java',
37
+ 'build.gradle': 'java',
38
+ };
39
+
40
+ /** Directories to skip during scanning */
41
+ const SKIP_DIRS = new Set([
42
+ 'node_modules', '.git', 'dist', 'build', '.planning',
43
+ 'coverage', '.next', '.nuxt', '__pycache__', '.venv',
44
+ 'venv', 'target', 'vendor', 'bin', '.cache',
45
+ ]);
46
+
47
+ /**
48
+ * Walk a directory recursively, collecting source files grouped by language.
49
+ * @param {string} dir - Directory to walk
50
+ * @param {string} baseCwd - Project root for relative paths
51
+ * @returns {{ files_by_language: Object<string, string[]>, total: number }}
52
+ */
53
+ function walkSourceFiles(dir, baseCwd) {
54
+ const filesByLang = {};
55
+ let total = 0;
56
+
57
+ function walk(current) {
58
+ let entries;
59
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
63
+ walk(path.join(current, entry.name));
64
+ }
65
+ } else if (entry.isFile()) {
66
+ const ext = path.extname(entry.name).toLowerCase();
67
+ const lang = EXTENSION_MAP[ext];
68
+ if (lang) {
69
+ const rel = toPosix(path.relative(baseCwd, path.join(current, entry.name)));
70
+ if (!filesByLang[lang]) filesByLang[lang] = [];
71
+ filesByLang[lang].push(rel);
72
+ total++;
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ walk(dir);
79
+ return { files_by_language: filesByLang, total };
80
+ }
81
+
82
+ /**
83
+ * Detect languages used in a codebase.
84
+ * @param {string} cwd - Project root
85
+ * @returns {{ primary: string|null, secondary: string[], files_by_language: Object, file_count: number }}
86
+ */
87
+ function detectLanguages(cwd) {
88
+ const { files_by_language, total } = walkSourceFiles(cwd, cwd);
89
+
90
+ // Check manifests for additional signals
91
+ for (const [manifest, lang] of Object.entries(MANIFEST_MAP)) {
92
+ try {
93
+ fs.accessSync(path.join(cwd, manifest));
94
+ // Promote language if manifest exists but no source files found yet
95
+ if (!files_by_language[lang]) files_by_language[lang] = [];
96
+ } catch { /* manifest not found */ }
97
+ }
98
+
99
+ // TypeScript subsumes javascript if tsconfig.json exists
100
+ if (files_by_language.typescript && files_by_language.javascript) {
101
+ // Merge JS files under TypeScript project
102
+ files_by_language.typescript = files_by_language.typescript.concat(files_by_language.javascript);
103
+ delete files_by_language.javascript;
104
+ }
105
+
106
+ // Determine primary (most files) and secondary (>5% of total)
107
+ const sorted = Object.entries(files_by_language)
108
+ .filter(([, files]) => files.length > 0)
109
+ .sort((a, b) => b[1].length - a[1].length);
110
+
111
+ const primary = sorted.length > 0 ? sorted[0][0] : null;
112
+ const threshold = Math.max(1, total * 0.05);
113
+ const secondary = sorted.slice(1)
114
+ .filter(([, files]) => files.length >= threshold)
115
+ .map(([lang]) => lang);
116
+
117
+ return { primary, secondary, files_by_language, file_count: total };
118
+ }
119
+
120
+ // ─── Import/Export Parsing ──────────────────────────────────────────────────
121
+
122
+ /** Import regex pattern factories per language (fresh instances avoid g-flag state leaks) */
123
+ function getImportPatterns(lang) {
124
+ if (lang === 'javascript' || lang === 'typescript') {
125
+ return [
126
+ { re: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, type: 'require' },
127
+ { re: /import\s+.*?from\s+['"]([^'"]+)['"]/g, type: 'esm' },
128
+ { re: /(?:^|[\s;=])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, type: 'dynamic' },
129
+ ];
130
+ }
131
+ return [];
132
+ }
133
+
134
+ /** Export regex pattern factories per language */
135
+ function getExportPatterns(lang) {
136
+ if (lang === 'javascript' || lang === 'typescript') {
137
+ return [
138
+ { re: /module\.exports\s*=/g, type: 'cjs-default' },
139
+ { re: /exports\.(\w+)\s*=/g, type: 'cjs-named' },
140
+ { re: /export\s+default\s+(?:function|class|const|let|var)?\s*(\w*)/g, type: 'esm-default' },
141
+ { re: /export\s+(?:function|class|const|let|var)\s+(\w+)/g, type: 'esm-named' },
142
+ { re: /export\s*\{([^}]+)\}/g, type: 'esm-destructured' },
143
+ ];
144
+ }
145
+ return [];
146
+ }
147
+
148
+ /**
149
+ * Strip single-line and multi-line comments from source code.
150
+ * @param {string} content - Source code
151
+ * @returns {string} Content with comments replaced by whitespace (preserving line count)
152
+ */
153
+ function stripComments(content) {
154
+ // Replace multi-line comments with equivalent newlines
155
+ let result = content.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '));
156
+ // Replace single-line comments
157
+ result = result.replace(/\/\/.*$/gm, '');
158
+ return result;
159
+ }
160
+
161
+ /**
162
+ * Parse import statements from a file.
163
+ * @param {string} filePath - Absolute path to source file
164
+ * @param {string} lang - Language identifier
165
+ * @returns {Array<{ source: string, line: number, type: string }>}
166
+ */
167
+ function parseImports(filePath, lang) {
168
+ const patterns = getImportPatterns(lang);
169
+ if (patterns.length === 0) return [];
170
+
171
+ const content = safeReadFile(filePath);
172
+ if (!content) return [];
173
+
174
+ const stripped = stripComments(content);
175
+ const lines = stripped.split('\n');
176
+ const results = [];
177
+
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const line = lines[i];
180
+ for (const { re, type } of patterns) {
181
+ re.lastIndex = 0;
182
+ let match;
183
+ while ((match = re.exec(line)) !== null) {
184
+ results.push({ source: match[1], line: i + 1, type });
185
+ }
186
+ }
187
+ }
188
+ return results;
189
+ }
190
+
191
+ /**
192
+ * Parse export statements from a file.
193
+ * @param {string} filePath - Absolute path to source file
194
+ * @param {string} lang - Language identifier
195
+ * @returns {Array<{ name: string, type: string, line: number, default: boolean }>}
196
+ */
197
+ function parseExports(filePath, lang) {
198
+ const patterns = getExportPatterns(lang);
199
+ if (patterns.length === 0) return [];
200
+
201
+ const content = safeReadFile(filePath);
202
+ if (!content) return [];
203
+
204
+ const stripped = stripComments(content);
205
+ const lines = stripped.split('\n');
206
+ const results = [];
207
+
208
+ for (let i = 0; i < lines.length; i++) {
209
+ const line = lines[i];
210
+ for (const { re, type } of patterns) {
211
+ re.lastIndex = 0;
212
+ let match;
213
+ while ((match = re.exec(line)) !== null) {
214
+ if (type === 'cjs-default') {
215
+ results.push({ name: 'default', type, line: i + 1, default: true });
216
+ } else if (type === 'esm-destructured') {
217
+ const names = match[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
218
+ for (const name of names) {
219
+ results.push({ name, type, line: i + 1, default: false });
220
+ }
221
+ } else if (type === 'esm-default') {
222
+ results.push({ name: match[1] || 'default', type, line: i + 1, default: true });
223
+ } else {
224
+ results.push({ name: match[1], type, line: i + 1, default: false });
225
+ }
226
+ }
227
+ }
228
+ }
229
+ return results;
230
+ }
231
+
232
+ // ─── Dependency Graph ───────────────────────────────────────────────────────
233
+
234
+ /** Common file extensions to try when resolving imports */
235
+ const RESOLVE_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'];
236
+
237
+ /**
238
+ * Resolve an import source to an actual file path.
239
+ * @param {string} source - Import source (e.g., './module', '../utils')
240
+ * @param {string} importerDir - Directory of the importing file
241
+ * @param {string} cwd - Project root
242
+ * @returns {string|null} Resolved relative path or null
243
+ */
244
+ function resolveImport(source, importerDir, cwd) {
245
+ // Skip non-relative imports (node_modules, builtins)
246
+ if (!source.startsWith('.')) return null;
247
+
248
+ const resolved = path.resolve(importerDir, source);
249
+
250
+ // Try exact match
251
+ try { if (fs.statSync(resolved).isFile()) return toPosix(path.relative(cwd, resolved)); } catch { /* */ }
252
+
253
+ // Try with extensions
254
+ for (const ext of RESOLVE_EXTENSIONS) {
255
+ try { if (fs.statSync(resolved + ext).isFile()) return toPosix(path.relative(cwd, resolved + ext)); } catch { /* */ }
256
+ }
257
+
258
+ // Try index files
259
+ for (const ext of RESOLVE_EXTENSIONS) {
260
+ const indexPath = path.join(resolved, 'index' + ext);
261
+ try { if (fs.statSync(indexPath).isFile()) return toPosix(path.relative(cwd, indexPath)); } catch { /* */ }
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * Build a dependency graph from import analysis.
269
+ * @param {string} cwd - Project root
270
+ * @returns {{ nodes: string[], edges: Array<{from: string, to: string}>, adjacency: Object<string, string[]> }}
271
+ */
272
+ function buildDependencyGraph(cwd) {
273
+ const { files_by_language } = detectLanguages(cwd);
274
+ const nodeSet = new Set();
275
+ const edges = [];
276
+ const adjacency = {};
277
+
278
+ for (const [lang, files] of Object.entries(files_by_language)) {
279
+ for (const relPath of files) {
280
+ const absPath = path.join(cwd, relPath);
281
+
282
+ // Skip large files
283
+ try {
284
+ const stat = fs.statSync(absPath);
285
+ if (stat.size > DRIFT_MAX_FILE_SIZE) continue;
286
+ } catch { continue; }
287
+
288
+ nodeSet.add(relPath);
289
+ if (!adjacency[relPath]) adjacency[relPath] = [];
290
+
291
+ const imports = parseImports(absPath, lang);
292
+ const importerDir = path.dirname(absPath);
293
+
294
+ for (const imp of imports) {
295
+ const resolved = resolveImport(imp.source, importerDir, cwd);
296
+ if (resolved && resolved !== relPath) {
297
+ nodeSet.add(resolved);
298
+ if (!adjacency[relPath].includes(resolved)) {
299
+ adjacency[relPath].push(resolved);
300
+ edges.push({ from: relPath, to: resolved });
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ return { nodes: Array.from(nodeSet), edges, adjacency };
308
+ }
309
+
310
+ // ─── Circular Dependency Detection ──────────────────────────────────────────
311
+
312
+ /**
313
+ * Find circular dependencies using DFS cycle detection.
314
+ * @param {{ adjacency: Object<string, string[]> }} graph - Dependency graph
315
+ * @returns {Array<string[]>} Array of cycles (each cycle is an array of file paths)
316
+ */
317
+ function findCircularDeps(graph) {
318
+ const { adjacency } = graph;
319
+ const WHITE = 0, GRAY = 1, BLACK = 2;
320
+ const color = {};
321
+ const cycles = [];
322
+
323
+ for (const node of Object.keys(adjacency)) {
324
+ color[node] = WHITE;
325
+ }
326
+
327
+ function dfs(node, pathStack) {
328
+ color[node] = GRAY;
329
+ pathStack.push(node);
330
+
331
+ const neighbors = adjacency[node] || [];
332
+ for (const neighbor of neighbors) {
333
+ if (color[neighbor] === GRAY) {
334
+ // Found cycle — extract from pathStack
335
+ const cycleStart = pathStack.indexOf(neighbor);
336
+ const cycle = pathStack.slice(cycleStart).concat(neighbor);
337
+ cycles.push(cycle);
338
+ } else if (color[neighbor] === WHITE || color[neighbor] === undefined) {
339
+ dfs(neighbor, pathStack);
340
+ }
341
+ }
342
+
343
+ pathStack.pop();
344
+ color[node] = BLACK;
345
+ }
346
+
347
+ for (const node of Object.keys(adjacency)) {
348
+ if (color[node] === WHITE) {
349
+ dfs(node, []);
350
+ }
351
+ }
352
+
353
+ return cycles;
354
+ }
355
+
356
+ // ─── Entry Points & Orphans ─────────────────────────────────────────────────
357
+
358
+ /**
359
+ * Find entry points (files with no incoming edges).
360
+ * @param {{ nodes: string[], adjacency: Object }} graph
361
+ * @returns {string[]}
362
+ */
363
+ function findEntryPoints(graph) {
364
+ const { nodes, adjacency } = graph;
365
+ const hasIncoming = new Set();
366
+
367
+ for (const deps of Object.values(adjacency)) {
368
+ for (const dep of deps) {
369
+ hasIncoming.add(dep);
370
+ }
371
+ }
372
+
373
+ return nodes.filter(n => !hasIncoming.has(n));
374
+ }
375
+
376
+ /**
377
+ * Find orphan modules (files that export but are never imported).
378
+ * @param {string} cwd - Project root
379
+ * @param {{ nodes: string[], adjacency: Object }} graph
380
+ * @returns {string[]}
381
+ */
382
+ function findOrphanExports(cwd, graph) {
383
+ const hasIncoming = new Set();
384
+
385
+ for (const deps of Object.values(graph.adjacency)) {
386
+ for (const dep of deps) {
387
+ hasIncoming.add(dep);
388
+ }
389
+ }
390
+
391
+ const orphans = [];
392
+ for (const node of graph.nodes) {
393
+ if (hasIncoming.has(node)) continue;
394
+
395
+ // Check if file has exports but no outgoing edges (completely disconnected exporter)
396
+ const outgoing = graph.adjacency[node] || [];
397
+ if (outgoing.length > 0) continue; // Has imports — likely an entry point, not orphan
398
+
399
+ const absPath = path.join(cwd, node);
400
+ const lang = EXTENSION_MAP[path.extname(node).toLowerCase()] || 'javascript';
401
+ const exports = parseExports(absPath, lang);
402
+ if (exports.length > 0) {
403
+ orphans.push(node);
404
+ }
405
+ }
406
+
407
+ return orphans;
408
+ }
409
+
410
+ // ─── Mermaid Graph Generation ───────────────────────────────────────────────
411
+
412
+ /**
413
+ * Generate a Mermaid dependency graph.
414
+ * @param {{ nodes: string[], edges: Array<{from: string, to: string}> }} graph
415
+ * @param {number} [maxNodes=15] - Maximum nodes to show
416
+ * @returns {string} Mermaid graph source
417
+ */
418
+ function generateMermaidGraph(graph, maxNodes = 15) {
419
+ const { nodes, edges } = graph;
420
+ if (nodes.length === 0) return 'graph LR\n empty[No modules found]';
421
+
422
+ // Score nodes by total edge count (incoming + outgoing)
423
+ const edgeCount = {};
424
+ for (const node of nodes) edgeCount[node] = 0;
425
+ for (const { from, to } of edges) {
426
+ edgeCount[from] = (edgeCount[from] || 0) + 1;
427
+ edgeCount[to] = (edgeCount[to] || 0) + 1;
428
+ }
429
+
430
+ // Pick top N nodes
431
+ const topNodes = Object.entries(edgeCount)
432
+ .sort((a, b) => b[1] - a[1])
433
+ .slice(0, maxNodes)
434
+ .map(([node]) => node);
435
+
436
+ const topSet = new Set(topNodes);
437
+
438
+ // Build Mermaid
439
+ const lines = ['graph LR'];
440
+ const nodeIds = {};
441
+ let idCounter = 0;
442
+
443
+ function nodeId(name) {
444
+ if (!nodeIds[name]) {
445
+ nodeIds[name] = `N${idCounter++}`;
446
+ }
447
+ return nodeIds[name];
448
+ }
449
+
450
+ function shortName(filepath) {
451
+ const parts = filepath.split('/');
452
+ return parts[parts.length - 1].replace(/\.\w+$/, '');
453
+ }
454
+
455
+ for (const { from, to } of edges) {
456
+ if (topSet.has(from) && topSet.has(to)) {
457
+ lines.push(` ${nodeId(from)}[${shortName(from)}] --> ${nodeId(to)}[${shortName(to)}]`);
458
+ }
459
+ }
460
+
461
+ if (nodes.length > maxNodes) {
462
+ lines.push(` note[... and ${nodes.length - maxNodes} more modules]`);
463
+ }
464
+
465
+ return lines.join('\n');
466
+ }
467
+
468
+ // ─── Best Practices Detection ───────────────────────────────────────────────
469
+
470
+ /**
471
+ * Detect best practices in a codebase across 5 categories.
472
+ * @param {string} cwd - Project root
473
+ * @returns {{ categories: Array, score: number, recommendations: string[] }}
474
+ */
475
+ function detectBestPractices(cwd) {
476
+ const { files_by_language, file_count } = detectLanguages(cwd);
477
+ const categories = [];
478
+ const recommendations = [];
479
+
480
+ // Flatten all source files
481
+ const allFiles = [];
482
+ for (const files of Object.values(files_by_language)) {
483
+ allFiles.push(...files);
484
+ }
485
+
486
+ // Sample up to 30 files for detailed analysis
487
+ const sampleFiles = allFiles.slice(0, 30);
488
+ const sampleContents = [];
489
+ for (const rel of sampleFiles) {
490
+ const content = safeReadFile(path.join(cwd, rel));
491
+ if (content) sampleContents.push({ path: rel, content });
492
+ }
493
+
494
+ // Category 1: Error Handling
495
+ const errorCat = detectErrorHandling(sampleContents);
496
+ categories.push(errorCat);
497
+ if (errorCat.score < 7) recommendations.push(...errorCat.recommendations);
498
+
499
+ // Category 2: Testing
500
+ const testCat = detectTestingPractices(cwd, file_count);
501
+ categories.push(testCat);
502
+ if (testCat.score < 7) recommendations.push(...testCat.recommendations);
503
+
504
+ // Category 3: Naming Conventions
505
+ const namingCat = detectNamingConventions(sampleContents);
506
+ categories.push(namingCat);
507
+ if (namingCat.score < 7) recommendations.push(...namingCat.recommendations);
508
+
509
+ // Category 4: Security
510
+ const secCat = detectSecurityPractices(cwd, sampleContents);
511
+ categories.push(secCat);
512
+ if (secCat.score < 7) recommendations.push(...secCat.recommendations);
513
+
514
+ // Category 5: Performance
515
+ const perfCat = detectPerformancePractices(sampleContents);
516
+ categories.push(perfCat);
517
+ if (perfCat.score < 7) recommendations.push(...perfCat.recommendations);
518
+
519
+ const score = categories.length > 0
520
+ ? Math.round(categories.reduce((sum, c) => sum + c.score, 0) / categories.length * 10) / 10
521
+ : 0;
522
+
523
+ return { categories, score, recommendations };
524
+ }
525
+
526
+ function detectErrorHandling(samples) {
527
+ let tryCatchCount = 0;
528
+ let functionCount = 0;
529
+ const findings = [];
530
+
531
+ for (const { path: fp, content } of samples) {
532
+ const tryCatches = (content.match(/\btry\s*\{/g) || []).length;
533
+ const functions = (content.match(/\bfunction\s+\w+|=>\s*\{|\bconst\s+\w+\s*=\s*(?:async\s+)?\(/g) || []).length;
534
+ tryCatchCount += tryCatches;
535
+ functionCount += functions;
536
+ }
537
+
538
+ const ratio = functionCount > 0 ? tryCatchCount / functionCount : 0;
539
+ const score = Math.min(10, Math.round(ratio * 20));
540
+ const recs = [];
541
+ if (ratio < 0.3) recs.push('Add try-catch to more functions, especially async operations');
542
+
543
+ return { name: 'Error Handling', score, detected_patterns: [`try-catch ratio: ${Math.round(ratio * 100)}%`], recommendations: recs };
544
+ }
545
+
546
+ function detectTestingPractices(cwd, sourceFileCount) {
547
+ const patterns = ['*.test.*', '*.spec.*', '__tests__/**'];
548
+ let testFileCount = 0;
549
+
550
+ try {
551
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
552
+ for (const entry of entries) {
553
+ if (entry.name === 'tests' || entry.name === '__tests__' || entry.name === 'test') {
554
+ try {
555
+ const testDir = path.join(cwd, entry.name);
556
+ const testFiles = fs.readdirSync(testDir).filter(f => f.endsWith('.test.cjs') || f.endsWith('.test.js') || f.endsWith('.test.ts') || f.endsWith('.spec.js') || f.endsWith('.spec.ts'));
557
+ testFileCount += testFiles.length;
558
+ } catch { /* */ }
559
+ }
560
+ }
561
+ } catch { /* */ }
562
+
563
+ // Check for test framework config
564
+ let hasConfig = false;
565
+ for (const cfg of ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', '.mocharc.yml', '.mocharc.json']) {
566
+ try { fs.accessSync(path.join(cwd, cfg)); hasConfig = true; break; } catch { /* */ }
567
+ }
568
+ // node:test doesn't need config — check package.json for test script
569
+ try {
570
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
571
+ if (pkg.scripts && pkg.scripts.test) hasConfig = true;
572
+ } catch { /* */ }
573
+
574
+ const ratio = sourceFileCount > 0 ? testFileCount / sourceFileCount : 0;
575
+ const score = Math.min(10, Math.round((ratio * 10) + (hasConfig ? 3 : 0) + (testFileCount > 0 ? 2 : 0)));
576
+ const recs = [];
577
+ if (!hasConfig) recs.push('Add test framework configuration');
578
+ if (testFileCount === 0) recs.push('Create test files for source modules');
579
+
580
+ return { name: 'Testing', score, detected_patterns: [`${testFileCount} test files found`, hasConfig ? 'Test config present' : 'No test config'], recommendations: recs };
581
+ }
582
+
583
+ function detectNamingConventions(samples) {
584
+ let consistent = 0;
585
+ let total = 0;
586
+ const filePatterns = new Set();
587
+
588
+ for (const { path: fp } of samples) {
589
+ const filename = fp.split('/').pop().replace(/\.\w+$/, '');
590
+ total++;
591
+ if (/^[a-z][a-z0-9-]*$/.test(filename)) { consistent++; filePatterns.add('kebab-case'); }
592
+ else if (/^[a-z][a-zA-Z0-9]*$/.test(filename)) { consistent++; filePatterns.add('camelCase'); }
593
+ else if (/^[A-Z][a-zA-Z0-9]*$/.test(filename)) { consistent++; filePatterns.add('PascalCase'); }
594
+ else if (/^[a-z][a-z0-9_]*$/.test(filename)) { consistent++; filePatterns.add('snake_case'); }
595
+ }
596
+
597
+ const ratio = total > 0 ? consistent / total : 0;
598
+ const singlePattern = filePatterns.size <= 2;
599
+ const score = Math.min(10, Math.round(ratio * 7 + (singlePattern ? 3 : 0)));
600
+ const recs = [];
601
+ if (filePatterns.size > 2) recs.push('Standardize file naming convention — multiple patterns detected');
602
+
603
+ return { name: 'Naming Conventions', score, detected_patterns: [`File patterns: ${Array.from(filePatterns).join(', ')}`], recommendations: recs };
604
+ }
605
+
606
+ function detectSecurityPractices(cwd, samples) {
607
+ let score = 5; // Start at baseline
608
+ const patterns = [];
609
+ const recs = [];
610
+
611
+ // Check for .gitignore with .env
612
+ const gitignore = safeReadFile(path.join(cwd, '.gitignore'));
613
+ if (gitignore && /\.env/.test(gitignore)) { score += 2; patterns.push('.env in .gitignore'); }
614
+ else { recs.push('Add .env to .gitignore'); }
615
+
616
+ // Check for .env.example
617
+ try { fs.accessSync(path.join(cwd, '.env.example')); score += 1; patterns.push('.env.example exists'); } catch { /* */ }
618
+
619
+ // Check for hardcoded secrets patterns in code
620
+ let secretsFound = 0;
621
+ for (const { content } of samples) {
622
+ if (/(?:password|secret|api_key|token)\s*=\s*['"][^'"]{8,}/i.test(content)) {
623
+ secretsFound++;
624
+ }
625
+ }
626
+ if (secretsFound === 0) { score += 2; patterns.push('No hardcoded secrets detected'); }
627
+ else { score -= 2; recs.push(`Found ${secretsFound} files with potential hardcoded secrets`); }
628
+
629
+ return { name: 'Security', score: Math.max(0, Math.min(10, score)), detected_patterns: patterns, recommendations: recs };
630
+ }
631
+
632
+ function detectPerformancePractices(samples) {
633
+ let score = 5;
634
+ const patterns = [];
635
+ const recs = [];
636
+
637
+ let hasMemoization = false;
638
+ let hasLazyLoading = false;
639
+
640
+ for (const { content } of samples) {
641
+ if (/\buseMemo\b|\buseCallback\b|\bmemoize\b/.test(content)) hasMemoization = true;
642
+ if (/\blazy\s*\(|\bimport\s*\(/.test(content)) hasLazyLoading = true;
643
+ }
644
+
645
+ if (hasMemoization) { score += 2; patterns.push('Memoization patterns detected'); }
646
+ if (hasLazyLoading) { score += 2; patterns.push('Lazy loading detected'); }
647
+ if (!hasMemoization && !hasLazyLoading) { recs.push('Consider memoization or lazy loading for performance'); }
648
+
649
+ return { name: 'Performance', score: Math.min(10, score), detected_patterns: patterns.length > 0 ? patterns : ['No specific performance patterns detected'], recommendations: recs };
650
+ }
651
+
652
+ // ─── Lowercase Codebase Doc Helper ──────────────────────────────────────────
653
+
654
+ /**
655
+ * Find a codebase document, checking lowercase first then UPPERCASE.
656
+ * @param {string} cwd - Project root
657
+ * @param {string} docName - Document name without path (e.g., 'conventions.md')
658
+ * @returns {string|null} Full path to found document, or null
659
+ */
660
+ function findCodebaseDoc(cwd, docName) {
661
+ const codebaseDir = path.join(planningPath(cwd), CODEBASE_DIR);
662
+ // Try lowercase first
663
+ const lowercase = path.join(codebaseDir, docName.toLowerCase());
664
+ const content = safeReadFile(lowercase);
665
+ if (content !== null) return lowercase;
666
+
667
+ // Try UPPERCASE fallback
668
+ const uppercase = path.join(codebaseDir, docName.toUpperCase());
669
+ const upperContent = safeReadFile(uppercase);
670
+ if (upperContent !== null) return uppercase;
671
+
672
+ return null;
673
+ }
674
+
675
+ // ─── CLI Commands ───────────────────────────────────────────────────────────
676
+
677
+ /**
678
+ * CLI: Detect languages in codebase.
679
+ * @param {string} cwd - Project root
680
+ * @param {boolean} raw - Raw output flag
681
+ */
682
+ function cmdDetectLanguages(cwd, raw) {
683
+ const result = detectLanguages(cwd);
684
+ output(result, raw);
685
+ }
686
+
687
+ /**
688
+ * CLI: Analyze imports and build dependency graph.
689
+ * @param {string} cwd - Project root
690
+ * @param {boolean} raw - Raw output flag
691
+ * @param {string[]} args - Additional arguments (--files f1,f2)
692
+ */
693
+ function cmdAnalyzeImports(cwd, raw, args) {
694
+ const graph = buildDependencyGraph(cwd);
695
+ const circularDeps = findCircularDeps(graph);
696
+ const entryPoints = findEntryPoints(graph);
697
+ const orphans = findOrphanExports(cwd, graph);
698
+ const { primary } = detectLanguages(cwd);
699
+ const mermaid = generateMermaidGraph(graph);
700
+
701
+ const result = {
702
+ language: primary,
703
+ modules: graph.nodes.length,
704
+ imports: graph.edges.length,
705
+ circular_deps: circularDeps,
706
+ entry_points: entryPoints,
707
+ orphan_modules: orphans,
708
+ dependency_graph: mermaid,
709
+ };
710
+
711
+ output(result, raw);
712
+ }
713
+
714
+ /**
715
+ * CLI: Detect best practices.
716
+ * @param {string} cwd - Project root
717
+ * @param {boolean} raw - Raw output flag
718
+ */
719
+ function cmdBestPractices(cwd, raw) {
720
+ const result = detectBestPractices(cwd);
721
+ output(result, raw);
722
+ }
723
+
724
+ // ─── Exports ────────────────────────────────────────────────────────────────
725
+
726
+ module.exports = {
727
+ // Core analysis
728
+ detectLanguages,
729
+ parseImports,
730
+ parseExports,
731
+ buildDependencyGraph,
732
+ findCircularDeps,
733
+ findEntryPoints,
734
+ findOrphanExports,
735
+ generateMermaidGraph,
736
+ detectBestPractices,
737
+ // Helper
738
+ findCodebaseDoc,
739
+ stripComments,
740
+ walkSourceFiles,
741
+ resolveImport,
742
+ // CLI commands
743
+ cmdDetectLanguages,
744
+ cmdAnalyzeImports,
745
+ cmdBestPractices,
746
+ };