@laitszkin/apollo-toolkit 4.0.11 → 4.1.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 (134) hide show
  1. package/AGENTS.md +37 -27
  2. package/CHANGELOG.md +31 -0
  3. package/CLAUDE.md +37 -27
  4. package/README.md +15 -2
  5. package/assets/spec/rg13-1780435029246/test-questions.json +1 -0
  6. package/assets/spec/rg13-1780468345132/test-questions.json +1 -0
  7. package/package.json +3 -3
  8. package/packages/cli/dist/tool-registration.js +1 -0
  9. package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
  10. package/packages/cli/tool-registration.ts +1 -0
  11. package/packages/tools/architecture/dist/index.js +539 -2
  12. package/packages/tools/architecture/dist/index.test.d.ts +1 -0
  13. package/packages/tools/architecture/dist/index.test.js +229 -0
  14. package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
  15. package/packages/tools/architecture/index.test.ts +329 -0
  16. package/packages/tools/architecture/index.ts +607 -5
  17. package/packages/tools/codegraph/dist/index.d.ts +3 -0
  18. package/packages/tools/codegraph/dist/index.js +157 -0
  19. package/packages/tools/codegraph/dist/lib/cg-instance.d.ts +29 -0
  20. package/packages/tools/codegraph/dist/lib/cg-instance.js +59 -0
  21. package/packages/tools/codegraph/dist/lib/cg-instance.test.d.ts +1 -0
  22. package/packages/tools/codegraph/dist/lib/cg-instance.test.js +27 -0
  23. package/packages/tools/codegraph/dist/lib/cmd-explore.d.ts +5 -0
  24. package/packages/tools/codegraph/dist/lib/cmd-explore.js +95 -0
  25. package/packages/tools/codegraph/dist/lib/cmd-explore.test.d.ts +1 -0
  26. package/packages/tools/codegraph/dist/lib/cmd-explore.test.js +133 -0
  27. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.d.ts +1 -0
  28. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.js +83 -0
  29. package/packages/tools/codegraph/dist/lib/cmd-init.d.ts +5 -0
  30. package/packages/tools/codegraph/dist/lib/cmd-init.js +50 -0
  31. package/packages/tools/codegraph/dist/lib/cmd-init.test.d.ts +1 -0
  32. package/packages/tools/codegraph/dist/lib/cmd-init.test.js +51 -0
  33. package/packages/tools/codegraph/dist/lib/cmd-list-apis.d.ts +5 -0
  34. package/packages/tools/codegraph/dist/lib/cmd-list-apis.js +64 -0
  35. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.d.ts +1 -0
  36. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.js +69 -0
  37. package/packages/tools/codegraph/dist/lib/cmd-search.d.ts +5 -0
  38. package/packages/tools/codegraph/dist/lib/cmd-search.js +21 -0
  39. package/packages/tools/codegraph/dist/lib/cmd-search.test.d.ts +1 -0
  40. package/packages/tools/codegraph/dist/lib/cmd-search.test.js +30 -0
  41. package/packages/tools/codegraph/dist/lib/cmd-status.d.ts +4 -0
  42. package/packages/tools/codegraph/dist/lib/cmd-status.js +44 -0
  43. package/packages/tools/codegraph/dist/lib/cmd-status.test.d.ts +1 -0
  44. package/packages/tools/codegraph/dist/lib/cmd-status.test.js +72 -0
  45. package/packages/tools/codegraph/dist/lib/cmd-survey.d.ts +36 -0
  46. package/packages/tools/codegraph/dist/lib/cmd-survey.js +142 -0
  47. package/packages/tools/codegraph/dist/lib/cmd-survey.test.d.ts +1 -0
  48. package/packages/tools/codegraph/dist/lib/cmd-survey.test.js +136 -0
  49. package/packages/tools/codegraph/dist/lib/cmd-sync.d.ts +4 -0
  50. package/packages/tools/codegraph/dist/lib/cmd-sync.js +51 -0
  51. package/packages/tools/codegraph/dist/lib/cmd-sync.test.d.ts +1 -0
  52. package/packages/tools/codegraph/dist/lib/cmd-sync.test.js +30 -0
  53. package/packages/tools/codegraph/dist/lib/cmd-verify.d.ts +4 -0
  54. package/packages/tools/codegraph/dist/lib/cmd-verify.js +134 -0
  55. package/packages/tools/codegraph/dist/lib/cmd-verify.test.d.ts +1 -0
  56. package/packages/tools/codegraph/dist/lib/cmd-verify.test.js +139 -0
  57. package/packages/tools/codegraph/dist/lib/formatter.d.ts +67 -0
  58. package/packages/tools/codegraph/dist/lib/formatter.js +107 -0
  59. package/packages/tools/codegraph/dist/lib/formatter.test.d.ts +1 -0
  60. package/packages/tools/codegraph/dist/lib/formatter.test.js +41 -0
  61. package/packages/tools/codegraph/dist/lib/survey/grouper.d.ts +19 -0
  62. package/packages/tools/codegraph/dist/lib/survey/grouper.js +194 -0
  63. package/packages/tools/codegraph/dist/lib/survey/grouper.test.d.ts +1 -0
  64. package/packages/tools/codegraph/dist/lib/survey/grouper.test.js +62 -0
  65. package/packages/tools/codegraph/dist/lib/survey/scanner.d.ts +31 -0
  66. package/packages/tools/codegraph/dist/lib/survey/scanner.js +50 -0
  67. package/packages/tools/codegraph/dist/lib/verify/checker.d.ts +32 -0
  68. package/packages/tools/codegraph/dist/lib/verify/checker.js +146 -0
  69. package/packages/tools/codegraph/dist/lib/verify/checker.test.d.ts +1 -0
  70. package/packages/tools/codegraph/dist/lib/verify/checker.test.js +128 -0
  71. package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -0
  72. package/packages/tools/codegraph/env.d.ts +56 -0
  73. package/packages/tools/codegraph/index.ts +173 -0
  74. package/packages/tools/codegraph/lib/cg-instance.test.ts +36 -0
  75. package/packages/tools/codegraph/lib/cg-instance.ts +66 -0
  76. package/packages/tools/codegraph/lib/cmd-explore.test.ts +195 -0
  77. package/packages/tools/codegraph/lib/cmd-explore.ts +129 -0
  78. package/packages/tools/codegraph/lib/cmd-flag-splice.test.ts +94 -0
  79. package/packages/tools/codegraph/lib/cmd-init.test.ts +68 -0
  80. package/packages/tools/codegraph/lib/cmd-init.ts +60 -0
  81. package/packages/tools/codegraph/lib/cmd-list-apis.test.ts +80 -0
  82. package/packages/tools/codegraph/lib/cmd-list-apis.ts +90 -0
  83. package/packages/tools/codegraph/lib/cmd-search.test.ts +37 -0
  84. package/packages/tools/codegraph/lib/cmd-search.ts +32 -0
  85. package/packages/tools/codegraph/lib/cmd-status.test.ts +86 -0
  86. package/packages/tools/codegraph/lib/cmd-status.ts +53 -0
  87. package/packages/tools/codegraph/lib/cmd-survey.test.ts +161 -0
  88. package/packages/tools/codegraph/lib/cmd-survey.ts +199 -0
  89. package/packages/tools/codegraph/lib/cmd-sync.test.ts +41 -0
  90. package/packages/tools/codegraph/lib/cmd-sync.ts +62 -0
  91. package/packages/tools/codegraph/lib/cmd-verify.test.ts +162 -0
  92. package/packages/tools/codegraph/lib/cmd-verify.ts +145 -0
  93. package/packages/tools/codegraph/lib/formatter.test.ts +47 -0
  94. package/packages/tools/codegraph/lib/formatter.ts +130 -0
  95. package/packages/tools/codegraph/lib/survey/grouper.test.ts +72 -0
  96. package/packages/tools/codegraph/lib/survey/grouper.ts +226 -0
  97. package/packages/tools/codegraph/lib/survey/scanner.ts +89 -0
  98. package/packages/tools/codegraph/lib/verify/checker.test.ts +140 -0
  99. package/packages/tools/codegraph/lib/verify/checker.ts +172 -0
  100. package/packages/tools/codegraph/package.json +23 -0
  101. package/packages/tools/codegraph/tsconfig.json +22 -0
  102. package/resources/project-architecture/atlas/atlas.history.log +32 -0
  103. package/resources/project-architecture/atlas/atlas.history.undo.json +356 -28
  104. package/resources/project-architecture/atlas/atlas.history.undo.stack.json +14350 -0
  105. package/resources/project-architecture/atlas/atlas.index.yaml +76 -12
  106. package/resources/project-architecture/atlas/features/codegraph.yaml +95 -0
  107. package/resources/project-architecture/atlas/features/eval-ci-gate.yaml +6 -1
  108. package/resources/project-architecture/atlas/features/eval-cli.yaml +16 -1
  109. package/resources/project-architecture/atlas/features/eval-executor.yaml +12 -2
  110. package/resources/project-architecture/atlas/features/eval-isolation.yaml +6 -1
  111. package/resources/project-architecture/atlas/features/eval-optimizer.yaml +17 -2
  112. package/resources/project-architecture/atlas/features/eval-question.yaml +12 -2
  113. package/resources/project-architecture/atlas/features/eval-reporter.yaml +6 -1
  114. package/resources/project-architecture/atlas/features/eval-scorer.yaml +12 -2
  115. package/resources/project-architecture/features/codegraph/cg-discovery.html +47 -0
  116. package/resources/project-architecture/features/codegraph/cg-lifecycle.html +48 -0
  117. package/resources/project-architecture/features/codegraph/cg-validation.html +47 -0
  118. package/resources/project-architecture/features/codegraph/index.html +58 -0
  119. package/resources/project-architecture/features/eval-ci-gate/workflow-trigger.html +6 -1
  120. package/resources/project-architecture/features/eval-cli/cli-handler.html +8 -1
  121. package/resources/project-architecture/features/eval-executor/exec-api-client.html +6 -1
  122. package/resources/project-architecture/features/eval-executor/trace-recorder.html +6 -1
  123. package/resources/project-architecture/features/eval-isolation/tool-dispatcher.html +6 -1
  124. package/resources/project-architecture/features/eval-optimizer/dedup-engine.html +6 -1
  125. package/resources/project-architecture/features/eval-optimizer/issue-extractor.html +7 -1
  126. package/resources/project-architecture/features/eval-question/question-loader.html +6 -1
  127. package/resources/project-architecture/features/eval-question/variant-generator.html +6 -1
  128. package/resources/project-architecture/features/eval-reporter/report-composer.html +6 -1
  129. package/resources/project-architecture/features/eval-scorer/judge-api-client.html +6 -1
  130. package/resources/project-architecture/features/eval-scorer/judge-prompt-builder.html +6 -1
  131. package/resources/project-architecture/index.html +200 -94
  132. package/skills/design/SKILL.md +33 -0
  133. package/skills/init-project-html/SKILL.md +12 -11
  134. package/tsconfig.json +1 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Format structured data for CLI output.
3
+ *
4
+ * - TTY mode produces human-readable tables and headings.
5
+ * - Non-TTY or `--json` produces JSON.stringify with 2-space indent.
6
+ * - Auto-detects TTY via `process.stdout.isTTY`.
7
+ */
8
+ function isTTY(options) {
9
+ if (options.json)
10
+ return false;
11
+ if (options.tty !== undefined)
12
+ return options.tty;
13
+ return !!process.stdout.isTTY;
14
+ }
15
+ /**
16
+ * Format generic data for output.
17
+ */
18
+ export function formatOutput(data, options = {}) {
19
+ if (!isTTY(options)) {
20
+ return JSON.stringify(data, null, 2);
21
+ }
22
+ // Fallback: JSON for complex objects in TTY too
23
+ if (typeof data === 'string')
24
+ return data;
25
+ if (data === null || data === undefined)
26
+ return '';
27
+ return JSON.stringify(data, null, 2);
28
+ }
29
+ /**
30
+ * Format a key-value summary block for human-readable output.
31
+ * Example:
32
+ * Files: 42
33
+ * Nodes: 1280
34
+ * Edges: 5600
35
+ */
36
+ export function formatSummary(rows) {
37
+ const width = rows.reduce((max, [key]) => Math.max(max, key.length + 1), 0);
38
+ return rows
39
+ .map(([key, val]) => `${key.padEnd(width, ' ')} ${val}`)
40
+ .join('\n');
41
+ }
42
+ /**
43
+ * Format a list of search results as a human-readable table.
44
+ */
45
+ export function formatSearchResults(results) {
46
+ if (results.length === 0)
47
+ return 'No results found.';
48
+ const lines = results.map((r, i) => {
49
+ const score = (r.score * 100).toFixed(0);
50
+ return ` ${i + 1}. ${r.node.name} [${r.node.kind}] ${r.node.filePath}:${r.node.startLine} (${score}%)`;
51
+ });
52
+ return `Results (${results.length}):\n${lines.join('\n')}`;
53
+ }
54
+ /**
55
+ * Format an API directory listing for human-readable output.
56
+ */
57
+ export function formatApiList(apis) {
58
+ if (apis.length === 0)
59
+ return 'No public APIs found.';
60
+ const lines = [];
61
+ for (const a of apis) {
62
+ const sig = a.signature ? ` ${a.signature}` : '';
63
+ lines.push(` ${a.name} [${a.kind}] ${a.filePath}:${a.startLine} (${a.callerCount} callers)${sig}`);
64
+ if (a.callerCount > 0 && a.callers) {
65
+ const callerLines = a.callers.slice(0, 5).map(c => ` Called by: ${c.name} ${c.filePath}:${c.startLine}`);
66
+ lines.push(...callerLines);
67
+ if (a.callerCount > 5)
68
+ lines.push(` ... and ${a.callerCount - 5} more`);
69
+ }
70
+ }
71
+ return `APIs (${apis.length}):\n${lines.join('\n')}`;
72
+ }
73
+ /**
74
+ * Format an API listing grouped by directory.
75
+ */
76
+ export function formatApiListGrouped(apis) {
77
+ if (apis.length === 0)
78
+ return 'No public APIs found.';
79
+ // Group by directory
80
+ const groups = new Map();
81
+ for (const a of apis) {
82
+ const dir = a.filePath.substring(0, a.filePath.lastIndexOf('/'));
83
+ if (!groups.has(dir))
84
+ groups.set(dir, []);
85
+ groups.get(dir).push(a);
86
+ }
87
+ // Render with directory headers
88
+ const lines = [];
89
+ const sortedDirs = [...groups.keys()].sort();
90
+ for (const dir of sortedDirs) {
91
+ const entries = groups.get(dir);
92
+ lines.push('');
93
+ lines.push(`=== ${dir}/ ===`);
94
+ for (const a of entries) {
95
+ const sig = a.signature ? ` ${a.signature}` : '';
96
+ lines.push(` ${a.name} [${a.kind}] ${a.filePath}:${a.startLine} (${a.callerCount} callers)${sig}`);
97
+ if (a.callerCount > 0 && a.callers) {
98
+ const callerLines = a.callers.slice(0, 5).map(c => ` Called by: ${c.name} ${c.filePath}:${c.startLine}`);
99
+ lines.push(...callerLines);
100
+ if (a.callerCount > 5)
101
+ lines.push(` ... and ${a.callerCount - 5} more`);
102
+ }
103
+ }
104
+ }
105
+ lines.push('');
106
+ return lines.join('\n').trimStart();
107
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { formatApiList } from './formatter.js';
4
+ describe('formatApiList', () => {
5
+ it('should show caller names in output when callers exist', () => {
6
+ const apis = [
7
+ {
8
+ name: 'myFunc',
9
+ kind: 'function',
10
+ filePath: 'src/a.ts',
11
+ startLine: 10,
12
+ callerCount: 3,
13
+ callers: [
14
+ { name: 'callerOne', filePath: 'src/b.ts', startLine: 5 },
15
+ { name: 'callerTwo', filePath: 'src/c.ts', startLine: 15 },
16
+ { name: 'callerThree', filePath: 'src/d.ts', startLine: 25 },
17
+ ],
18
+ },
19
+ ];
20
+ const output = formatApiList(apis);
21
+ assert.ok(output.includes('callerOne'), 'output should contain callerOne');
22
+ assert.ok(output.includes('callerTwo'), 'output should contain callerTwo');
23
+ assert.ok(output.includes('callerThree'), 'output should contain callerThree');
24
+ assert.ok(output.includes('(3 callers)'), 'output should show caller count');
25
+ assert.ok(output.includes('myFunc'), 'output should contain function name');
26
+ });
27
+ it('should show "(0 callers)" when there are no callers', () => {
28
+ const apis = [
29
+ {
30
+ name: 'noCallers',
31
+ kind: 'function',
32
+ filePath: 'src/a.ts',
33
+ startLine: 1,
34
+ callerCount: 0,
35
+ },
36
+ ];
37
+ const output = formatApiList(apis);
38
+ assert.ok(output.includes('(0 callers)'), 'output should show zero callers');
39
+ assert.ok(!output.includes('Called by:'), 'output should not contain caller lines');
40
+ });
41
+ });
@@ -0,0 +1,19 @@
1
+ import type { ScanResult } from './scanner.js';
2
+ export interface SubmoduleSuggestion {
3
+ slug: string;
4
+ kind: 'api' | 'service' | 'db' | 'ui' | 'pure-fn' | 'queue';
5
+ role: string;
6
+ memberFunctions: string[];
7
+ memberFiles: string[];
8
+ }
9
+ /**
10
+ * Group scanned symbols into suggested submodule groupings.
11
+ *
12
+ * Algorithm (hybrid):
13
+ * 1. Build adjacency map from call graph: for each symbol, find callees within the scan
14
+ * 2. BFS on undirected graph to find connected components
15
+ * 3. Components with >=2 symbols create connectivity-based submodule suggestions
16
+ * 4. Remaining (isolated) symbols fall back to per-file grouping
17
+ * 5. Apply mergeByDirectoryPrefix as final step
18
+ */
19
+ export declare function groupIntoSubmodules(scan: ScanResult, cg: any): SubmoduleSuggestion[];
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Group scanned symbols into suggested submodule groupings.
3
+ *
4
+ * Algorithm (hybrid):
5
+ * 1. Build adjacency map from call graph: for each symbol, find callees within the scan
6
+ * 2. BFS on undirected graph to find connected components
7
+ * 3. Components with >=2 symbols create connectivity-based submodule suggestions
8
+ * 4. Remaining (isolated) symbols fall back to per-file grouping
9
+ * 5. Apply mergeByDirectoryPrefix as final step
10
+ */
11
+ export function groupIntoSubmodules(scan, cg) {
12
+ if (scan.allSymbols.length === 0)
13
+ return [];
14
+ // --- Phase 1: Build adjacency map from call graph connectivity ---
15
+ const allNameSet = new Set(scan.allSymbols.map(s => s.name));
16
+ const adj = new Map();
17
+ const nodeKeyMap = new Map();
18
+ for (const sym of scan.allSymbols) {
19
+ const key = `${sym.filePath}::${sym.name}`;
20
+ const calleeSet = new Set();
21
+ adj.set(key, calleeSet);
22
+ nodeKeyMap.set(key, sym);
23
+ const nodes = cg.getNodesByName(sym.name);
24
+ for (const node of nodes) {
25
+ if (node.filePath !== sym.filePath)
26
+ continue;
27
+ const callees = cg.getCallees(node.id);
28
+ for (const callee of callees) {
29
+ if (allNameSet.has(callee.node.name)) {
30
+ calleeSet.add(callee.node.name);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ // Build reverse index: name -> list of symbol keys (for efficient BFS lookup)
36
+ const nameToKeys = new Map();
37
+ for (const [key, sym] of nodeKeyMap.entries()) {
38
+ const arr = nameToKeys.get(sym.name) || [];
39
+ arr.push(key);
40
+ nameToKeys.set(sym.name, arr);
41
+ }
42
+ // --- Phase 2: BFS to find connected components (undirected graph) ---
43
+ const visited = new Set();
44
+ const components = [];
45
+ for (const key of adj.keys()) {
46
+ if (visited.has(key))
47
+ continue;
48
+ const component = [];
49
+ const queue = [key];
50
+ visited.add(key);
51
+ while (queue.length > 0) {
52
+ const currentKey = queue.shift();
53
+ const sym = nodeKeyMap.get(currentKey);
54
+ component.push(sym);
55
+ const calleeNames = adj.get(currentKey) || new Set();
56
+ for (const calleeName of calleeNames) {
57
+ const targetKeys = nameToKeys.get(calleeName) || [];
58
+ for (const tk of targetKeys) {
59
+ if (!visited.has(tk)) {
60
+ visited.add(tk);
61
+ queue.push(tk);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ if (component.length > 0) {
67
+ components.push(component);
68
+ }
69
+ }
70
+ // --- Phase 3: Build suggestions from connected components ---
71
+ const suggestions = [];
72
+ const processed = new Set();
73
+ // Components with >=2 symbols become connectivity-based submodules
74
+ for (const component of components) {
75
+ if (component.length < 2)
76
+ continue;
77
+ const files = [...new Set(component.map(s => s.filePath))];
78
+ const representativePath = files[0];
79
+ const fileName = representativePath.split('/').pop() || '';
80
+ const slug = fileName.replace(/\.\w+$/, '').replace(/[_ ]/g, '-').toLowerCase();
81
+ const kind = inferKind(representativePath, component);
82
+ const role = inferRole(kind, slug, component.filter(s => s.isExported));
83
+ for (const s of component) {
84
+ processed.add(`${s.filePath}::${s.name}`);
85
+ }
86
+ suggestions.push({
87
+ slug,
88
+ kind,
89
+ role,
90
+ memberFunctions: [...new Set(component.map(s => s.name))],
91
+ memberFiles: files,
92
+ });
93
+ }
94
+ // --- Phase 4: Remaining (isolated) symbols fall back to per-file grouping ---
95
+ for (const file of scan.files) {
96
+ const unprocessedSymbols = file.symbols.filter(s => !processed.has(`${file.filePath}::${s.name}`));
97
+ if (unprocessedSymbols.length === 0 && file.symbols.length === 0)
98
+ continue;
99
+ const fileName = file.filePath.split('/').pop() || '';
100
+ const slug = fileName.replace(/\.\w+$/, '').replace(/[_ ]/g, '-').toLowerCase();
101
+ const kind = inferKind(file.filePath, file.symbols);
102
+ const symbols = unprocessedSymbols.map(s => s.name);
103
+ if (symbols.length === 0)
104
+ continue;
105
+ for (const s of unprocessedSymbols)
106
+ processed.add(`${file.filePath}::${s.name}`);
107
+ const role = inferRole(kind, slug, file.symbols.filter(s => s.isExported));
108
+ suggestions.push({
109
+ slug,
110
+ kind,
111
+ role,
112
+ memberFunctions: symbols,
113
+ memberFiles: [file.filePath],
114
+ });
115
+ }
116
+ // --- Phase 5: Merge small files sharing a directory prefix ---
117
+ const merged = mergeByDirectoryPrefix(suggestions, scan.directory);
118
+ return merged;
119
+ }
120
+ function inferKind(filePath, symbols) {
121
+ const lower = filePath.toLowerCase();
122
+ // Detect by path patterns
123
+ if (lower.includes('/api/') || lower.includes('/routes/') || lower.includes('/controller'))
124
+ return 'api';
125
+ if (lower.includes('/db/') || lower.includes('/model/') || lower.includes('/repository') || lower.includes('/schema'))
126
+ return 'db';
127
+ if (lower.includes('/ui/') || lower.includes('/component/') || lower.includes('/page/') || lower.includes('/view'))
128
+ return 'ui';
129
+ if (lower.includes('/queue/') || lower.includes('/job/') || lower.includes('/worker'))
130
+ return 'queue';
131
+ // Detect by symbol kinds
132
+ const hasHandler = symbols.some((s) => s.kind === 'route' || s.kind === 'component');
133
+ if (hasHandler)
134
+ return 'api';
135
+ const hasModel = symbols.some((s) => s.kind === 'interface' || s.kind === 'struct');
136
+ if (hasModel)
137
+ return 'db';
138
+ return 'service';
139
+ }
140
+ function inferRole(kind, slug, exportedSymbols) {
141
+ const name = slug.replace(/-/g, ' ');
142
+ switch (kind) {
143
+ case 'api':
144
+ return `Handles API requests for ${name}`;
145
+ case 'db':
146
+ return `Manages data access and persistence for ${name}`;
147
+ case 'service':
148
+ return `Contains business logic for ${name}`;
149
+ case 'ui':
150
+ return `Renders UI components for ${name}`;
151
+ case 'queue':
152
+ return `Processes background jobs for ${name}`;
153
+ case 'pure-fn':
154
+ return `Provides pure utility functions for ${name}`;
155
+ default:
156
+ return `Supports ${name} functionality`;
157
+ }
158
+ }
159
+ function mergeByDirectoryPrefix(suggestions, _directory) {
160
+ // When there are many single-file suggestions, merge those
161
+ // that share a common 2-segment directory prefix
162
+ const prefixMap = new Map();
163
+ for (const s of suggestions) {
164
+ if (s.memberFiles.length !== 1) {
165
+ // Already contains multiple files, keep as-is
166
+ const key = s.slug;
167
+ prefixMap.set(key, s);
168
+ continue;
169
+ }
170
+ const filePath = s.memberFiles[0];
171
+ const parts = filePath.split('/');
172
+ // Use the parent directory as merge key for files in subdirs
173
+ const mergeKey = parts.length >= 3 ? parts.slice(0, -1).join('/') : s.slug;
174
+ if (prefixMap.has(mergeKey)) {
175
+ const existing = prefixMap.get(mergeKey);
176
+ existing.memberFunctions.push(...s.memberFunctions);
177
+ existing.memberFiles.push(...s.memberFiles);
178
+ // Keep more specific kind
179
+ if (existing.kind === 'service' && s.kind !== 'service') {
180
+ existing.kind = s.kind;
181
+ }
182
+ }
183
+ else {
184
+ prefixMap.set(mergeKey, {
185
+ slug: mergeKey.replace(/\//g, '-'),
186
+ kind: s.kind,
187
+ role: s.role,
188
+ memberFunctions: [...s.memberFunctions],
189
+ memberFiles: [...s.memberFiles],
190
+ });
191
+ }
192
+ }
193
+ return Array.from(prefixMap.values());
194
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { groupIntoSubmodules } from './grouper.js';
4
+ describe('groupIntoSubmodules', () => {
5
+ it('groups connected call graph symbols into one submodule (hybrid connectivity grouping)', () => {
6
+ // -----------------------------------------------------------------------
7
+ // GIVEN a scan result with functions A, B, C where A calls B and B calls C
8
+ // (all in the same file, forming a connected chain)
9
+ // -----------------------------------------------------------------------
10
+ const filePath = 'src/module.ts';
11
+ const scan = {
12
+ directory: 'src',
13
+ files: [
14
+ {
15
+ filePath,
16
+ language: 'typescript',
17
+ symbols: [
18
+ { name: 'A', kind: 'function', qualifiedName: 'A', startLine: 1, endLine: 5, isExported: true },
19
+ { name: 'B', kind: 'function', qualifiedName: 'B', startLine: 6, endLine: 10, isExported: true },
20
+ { name: 'C', kind: 'function', qualifiedName: 'C', startLine: 11, endLine: 15, isExported: false },
21
+ ],
22
+ },
23
+ ],
24
+ allSymbols: [
25
+ { name: 'A', kind: 'function', filePath, qualifiedName: 'A', startLine: 1, isExported: true },
26
+ { name: 'B', kind: 'function', filePath, qualifiedName: 'B', startLine: 6, isExported: true },
27
+ { name: 'C', kind: 'function', filePath, qualifiedName: 'C', startLine: 11, isExported: false },
28
+ ],
29
+ totalFiles: 1,
30
+ totalSymbols: 3,
31
+ };
32
+ // Mock call graph: A -> B -> C
33
+ const mockCg = {
34
+ getNodesByName(name) {
35
+ const nodeId = `node-${name}`;
36
+ return [{ id: nodeId, filePath }];
37
+ },
38
+ getCallees(nodeId) {
39
+ const calleeMap = {
40
+ 'node-A': ['B'],
41
+ 'node-B': ['C'],
42
+ 'node-C': [],
43
+ };
44
+ return (calleeMap[nodeId] || []).map((calleeName) => ({
45
+ node: { name: calleeName },
46
+ }));
47
+ },
48
+ };
49
+ // -----------------------------------------------------------------------
50
+ // WHEN groupIntoSubmodules(scan, mockCg) runs
51
+ // -----------------------------------------------------------------------
52
+ const result = groupIntoSubmodules(scan, mockCg);
53
+ // -----------------------------------------------------------------------
54
+ // THEN A, B, C are grouped into one submodule
55
+ // (connectivity-based grouping takes priority over per-file isolation)
56
+ // -----------------------------------------------------------------------
57
+ assert.equal(result.length, 1, 'should produce exactly one submodule');
58
+ const sortedMembers = [...result[0].memberFunctions].sort();
59
+ assert.deepEqual(sortedMembers, ['A', 'B', 'C'], 'should contain A, B, C');
60
+ assert.ok(result[0].memberFiles.includes(filePath), 'should include the source file in memberFiles');
61
+ });
62
+ });
@@ -0,0 +1,31 @@
1
+ export interface FileScan {
2
+ filePath: string;
3
+ language: string;
4
+ symbols: Array<{
5
+ name: string;
6
+ kind: string;
7
+ qualifiedName: string;
8
+ startLine: number;
9
+ endLine: number;
10
+ isExported: boolean;
11
+ signature?: string;
12
+ }>;
13
+ }
14
+ export interface ScanResult {
15
+ directory: string;
16
+ files: FileScan[];
17
+ allSymbols: Array<{
18
+ name: string;
19
+ kind: string;
20
+ filePath: string;
21
+ qualifiedName: string;
22
+ startLine: number;
23
+ isExported: boolean;
24
+ }>;
25
+ totalFiles: number;
26
+ totalSymbols: number;
27
+ }
28
+ /**
29
+ * Scan a directory for all files and symbols tracked by CodeGraph.
30
+ */
31
+ export declare function scanDirectory(cg: any, dirPath: string): Promise<ScanResult>;
@@ -0,0 +1,50 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ /**
4
+ * Scan a directory for all files and symbols tracked by CodeGraph.
5
+ */
6
+ export async function scanDirectory(cg, dirPath) {
7
+ const { CodeGraph } = require('@colbymchenry/codegraph');
8
+ const files = cg.getFiles();
9
+ const dirPrefix = dirPath.replace(/^\/?/, '').replace(/\/?$/, '') + '/';
10
+ // Filter files within the directory
11
+ const dirFiles = files.filter((f) => f.path.startsWith(dirPrefix));
12
+ const fileScans = [];
13
+ const allSymbols = [];
14
+ for (const file of dirFiles) {
15
+ const nodes = cg.getNodesInFile(file.path);
16
+ const symbols = nodes
17
+ .filter((n) => !['file', 'import', 'parameter'].includes(n.kind))
18
+ .map((n) => ({
19
+ name: n.name,
20
+ kind: n.kind,
21
+ qualifiedName: n.qualifiedName,
22
+ startLine: n.startLine,
23
+ endLine: n.endLine,
24
+ isExported: !!n.isExported,
25
+ signature: n.signature,
26
+ }));
27
+ fileScans.push({
28
+ filePath: file.path,
29
+ language: file.language,
30
+ symbols,
31
+ });
32
+ for (const sym of symbols) {
33
+ allSymbols.push({
34
+ name: sym.name,
35
+ kind: sym.kind,
36
+ filePath: file.path,
37
+ qualifiedName: sym.qualifiedName,
38
+ startLine: sym.startLine,
39
+ isExported: sym.isExported,
40
+ });
41
+ }
42
+ }
43
+ return {
44
+ directory: dirPath,
45
+ files: fileScans,
46
+ allSymbols,
47
+ totalFiles: dirFiles.length,
48
+ totalSymbols: allSymbols.length,
49
+ };
50
+ }
@@ -0,0 +1,32 @@
1
+ import type { CodeGraph } from '@colbymchenry/codegraph';
2
+ export interface VerifyItem {
3
+ type: 'feature' | 'submodule' | 'function' | 'edge' | 'variable';
4
+ location: string;
5
+ action?: string;
6
+ suggestion?: string;
7
+ }
8
+ export interface VerifyReport {
9
+ passed: number;
10
+ failed: VerifyItem[];
11
+ skipped: number;
12
+ total: number;
13
+ }
14
+ /**
15
+ * Verify an architecture overlay against the actual CodeGraph to confirm
16
+ * that every referenced symbol, submodule, and edge actually exists in the
17
+ * indexed codebase.
18
+ *
19
+ * Overlay format follows the atlas convention:
20
+ * ```json
21
+ * {
22
+ * "features": {
23
+ * "<slug>": {
24
+ * "submodules": [{ "slug": "...", "kind": "...", "role": "...", "functions": [...] }],
25
+ * "edges": [{ "from": "...", "to": "...", "kind": "..." }]
26
+ * }
27
+ * },
28
+ * "removed": { "features": [...], "submodules": [...] }
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function verifyOverlay(cg: CodeGraph, overlay: any): Promise<VerifyReport>;