@laitszkin/apollo-toolkit 4.0.11 → 4.1.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 (145) hide show
  1. package/AGENTS.md +37 -27
  2. package/CHANGELOG.md +47 -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 +549 -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 +613 -5
  17. package/packages/tools/codegraph/dist/index.d.ts +3 -0
  18. package/packages/tools/codegraph/dist/index.js +343 -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 +362 -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/scripts/test.sh +39 -0
  133. package/skills/design/SKILL.md +33 -0
  134. package/skills/init-project-html/SKILL.md +66 -56
  135. package/skills/init-project-html/lib/atlas/assets/architecture.css +2 -1
  136. package/skills/init-project-html/lib/atlas/render.js +11 -1
  137. package/skills/init-project-html/lib/atlas/schema.js +44 -7
  138. package/skills/init-project-html/references/TEMPLATE_SPEC.md +20 -0
  139. package/skills/init-project-html/references/architecture.md +35 -35
  140. package/skills/init-project-html/references/definition.md +12 -17
  141. package/skills/update-project-html/README.md +16 -27
  142. package/skills/update-project-html/SKILL.md +54 -41
  143. package/skills/update-project-html/references/architecture.md +35 -35
  144. package/skills/update-project-html/references/definition.md +12 -17
  145. package/tsconfig.json +1 -0
@@ -0,0 +1,139 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import yaml from 'js-yaml';
7
+ /**
8
+ * REGTEST-1: Verify that the js-yaml based parser (used in `loadOverlay`)
9
+ * correctly parses object-format YAML function declarations.
10
+ *
11
+ * The fix replaced a custom YAML parser with `js-yaml`. The old parser
12
+ * incorrectly produced raw string fragments like `"name: init"` when
13
+ * encountering object-format functions:
14
+ *
15
+ * functions:
16
+ * - name: init
17
+ * in: string
18
+ * out: void
19
+ *
20
+ * With `js-yaml`, the parser produces actual objects:
21
+ * `{ name: "init", in: "string", out: "void" }`
22
+ *
23
+ * This test exercises the same parsing path as `loadOverlay`: reading a YAML
24
+ * file from disk with `fs.readFileSync`, then parsing with `yaml.load`.
25
+ */
26
+ describe('REGTEST-1: loadOverlay YAML parser', () => {
27
+ it('should parse object-format functions and correctly extract .name', () => {
28
+ const yamlStr = [
29
+ 'slug: test-feature',
30
+ 'submodules:',
31
+ ' - slug: module-a',
32
+ ' functions:',
33
+ ' - name: init',
34
+ ' in: string',
35
+ ' out: void',
36
+ 'edges:',
37
+ ' - from: funcA',
38
+ ' to: funcB',
39
+ ' kind: call',
40
+ ].join('\n');
41
+ // Parse with js-yaml (same library used by loadOverlay)
42
+ const data = yaml.load(yamlStr);
43
+ // Verify object-format function parsing
44
+ const fn = data.submodules[0].functions[0];
45
+ assert.strictEqual(typeof fn, 'object', 'object-format function should be parsed as object, not string');
46
+ assert.strictEqual(fn.name, 'init', 'parsed function .name should be "init"');
47
+ // Verify the old bug is fixed: no raw "name: init" string
48
+ assert.notStrictEqual(fn, 'name: init', 'function should NOT be a raw string fragment');
49
+ assert.notStrictEqual(typeof fn, 'string', 'object-format function should not be a string');
50
+ // Verify additional fields are preserved
51
+ assert.strictEqual(fn.in, 'string', 'function "in" field should be preserved');
52
+ assert.strictEqual(fn.out, 'void', 'function "out" field should be preserved');
53
+ // Verify string-format functions still work
54
+ // Note: the YAML above only has object-format functions; string format is tested separately
55
+ });
56
+ it('should parse string-format functions correctly', () => {
57
+ const yamlStr = [
58
+ 'slug: test-feature',
59
+ 'submodules:',
60
+ ' - slug: module-a',
61
+ ' functions:',
62
+ ' - init',
63
+ ' - process',
64
+ ].join('\n');
65
+ const data = yaml.load(yamlStr);
66
+ const functions = data.submodules[0].functions;
67
+ assert.strictEqual(functions.length, 2, 'should have 2 string-format functions');
68
+ assert.strictEqual(typeof functions[0], 'string', 'string-format function should be a string');
69
+ assert.strictEqual(functions[0], 'init', 'string function name should be "init"');
70
+ assert.strictEqual(functions[1], 'process', 'string function name should be "process"');
71
+ });
72
+ it('should parse mixed object and string format functions', () => {
73
+ const yamlStr = [
74
+ 'slug: test-feature',
75
+ 'submodules:',
76
+ ' - slug: module-a',
77
+ ' functions:',
78
+ ' - name: init',
79
+ ' in: string',
80
+ ' out: void',
81
+ ' - process',
82
+ ' - name: render',
83
+ ' in: string',
84
+ ' out: void',
85
+ ' - cleanup',
86
+ ].join('\n');
87
+ const data = yaml.load(yamlStr);
88
+ const functions = data.submodules[0].functions;
89
+ assert.strictEqual(functions.length, 4, 'should have 4 functions total');
90
+ // Index 0: object-format
91
+ assert.strictEqual(typeof functions[0], 'object', 'functions[0] should be object');
92
+ assert.strictEqual(functions[0].name, 'init');
93
+ // Index 1: string-format
94
+ assert.strictEqual(typeof functions[1], 'string', 'functions[1] should be string');
95
+ assert.strictEqual(functions[1], 'process');
96
+ // Index 2: object-format
97
+ assert.strictEqual(typeof functions[2], 'object', 'functions[2] should be object');
98
+ assert.strictEqual(functions[2].name, 'render');
99
+ // Index 3: string-format
100
+ assert.strictEqual(typeof functions[3], 'string', 'functions[3] should be string');
101
+ assert.strictEqual(functions[3], 'cleanup');
102
+ });
103
+ it('should parse feature YAML from disk (same path as loadOverlay)', async () => {
104
+ // Create a temp directory matching the loadOverlay structure
105
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-verify-test-'));
106
+ try {
107
+ const featuresDir = path.join(tmpDir, 'architecture_diff', 'atlas', 'features');
108
+ fs.mkdirSync(featuresDir, { recursive: true });
109
+ const yamlPath = path.join(featuresDir, 'test-feature.yaml');
110
+ fs.writeFileSync(yamlPath, [
111
+ 'slug: test-feature',
112
+ 'submodules:',
113
+ ' - slug: module-a',
114
+ ' functions:',
115
+ ' - name: init',
116
+ ' in: string',
117
+ ' out: void',
118
+ ' edges:',
119
+ ' - from: funcA',
120
+ ' to: funcB',
121
+ ' kind: call',
122
+ ].join('\n') + '\n');
123
+ // Read file and parse (same as loadOverlay)
124
+ const raw = fs.readFileSync(yamlPath, 'utf8');
125
+ const data = yaml.load(raw);
126
+ assert.ok(data, 'parsed data should be truthy');
127
+ assert.strictEqual(typeof data, 'object', 'parsed data should be an object');
128
+ assert.strictEqual(data.slug, 'test-feature', 'feature slug should be preserved');
129
+ const fn = data.submodules[0].functions[0];
130
+ assert.strictEqual(typeof fn, 'object', 'object-format function parsed from file should be object');
131
+ assert.strictEqual(fn.name, 'init', 'function .name should be "init"');
132
+ assert.strictEqual(fn.in, 'string', 'function .in should be "string"');
133
+ assert.strictEqual(fn.out, 'void', 'function .out should be "void"');
134
+ }
135
+ finally {
136
+ fs.rmSync(tmpDir, { recursive: true, force: true });
137
+ }
138
+ });
139
+ });
@@ -0,0 +1,67 @@
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
+ export interface FormatOptions {
9
+ json?: boolean;
10
+ tty?: boolean;
11
+ }
12
+ /**
13
+ * Format generic data for output.
14
+ */
15
+ export declare function formatOutput(data: unknown, options?: FormatOptions): string;
16
+ /**
17
+ * Format a key-value summary block for human-readable output.
18
+ * Example:
19
+ * Files: 42
20
+ * Nodes: 1280
21
+ * Edges: 5600
22
+ */
23
+ export declare function formatSummary(rows: [string, string | number][]): string;
24
+ /**
25
+ * Format a list of search results as a human-readable table.
26
+ */
27
+ export declare function formatSearchResults(results: Array<{
28
+ node: {
29
+ name: string;
30
+ kind: string;
31
+ filePath: string;
32
+ startLine: number;
33
+ };
34
+ score: number;
35
+ }>): string;
36
+ /**
37
+ * Format an API directory listing for human-readable output.
38
+ */
39
+ export declare function formatApiList(apis: Array<{
40
+ name: string;
41
+ kind: string;
42
+ filePath: string;
43
+ startLine: number;
44
+ signature?: string;
45
+ callerCount: number;
46
+ callers?: Array<{
47
+ name: string;
48
+ filePath: string;
49
+ startLine: number;
50
+ }>;
51
+ }>): string;
52
+ /**
53
+ * Format an API listing grouped by directory.
54
+ */
55
+ export declare function formatApiListGrouped(apis: Array<{
56
+ name: string;
57
+ kind: string;
58
+ filePath: string;
59
+ startLine: number;
60
+ signature?: string;
61
+ callerCount: number;
62
+ callers?: Array<{
63
+ name: string;
64
+ filePath: string;
65
+ startLine: number;
66
+ }>;
67
+ }>): string;
@@ -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>;