@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,173 @@
1
+ import type { ToolDefinition, ToolContext } from '@laitszkin/tool-registry';
2
+ import { findProjectRoot } from './lib/cg-instance.js';
3
+ import { handleInit } from './lib/cmd-init.js';
4
+ import { handleSync } from './lib/cmd-sync.js';
5
+ import { handleStatus } from './lib/cmd-status.js';
6
+ import { handleSearch } from './lib/cmd-search.js';
7
+ import { handleExplore } from './lib/cmd-explore.js';
8
+ import { handleSurvey } from './lib/cmd-survey.js';
9
+ import { handleListApis } from './lib/cmd-list-apis.js';
10
+ import { handleVerify } from './lib/cmd-verify.js';
11
+
12
+ export async function codegraphHandler(args: string[], context: ToolContext): Promise<number> {
13
+ const stdout = context.stdout || process.stdout;
14
+ const stderr = context.stderr || process.stderr;
15
+ const projectRoot = findProjectRoot(context.cwd || process.cwd());
16
+
17
+ // Parse --json flag early (can appear anywhere)
18
+ const jsonIndex = args.indexOf('--json');
19
+ const isJson = jsonIndex >= 0;
20
+ if (jsonIndex >= 0) args.splice(jsonIndex, 1);
21
+
22
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
23
+ printHelp(stdout);
24
+ return 0;
25
+ }
26
+
27
+ const subcommand = args[0];
28
+ const rest = args.slice(1);
29
+
30
+ // Parse --spec <dir> for verify
31
+ const specIndex = rest.indexOf('--spec');
32
+ let specDir: string | undefined;
33
+ if (specIndex >= 0 && specIndex + 1 < rest.length) {
34
+ specDir = rest[specIndex + 1];
35
+ rest.splice(specIndex, 2);
36
+ }
37
+
38
+ // Parse --all flag for list-apis
39
+ const allIndex = rest.indexOf('--all');
40
+ const isAll = allIndex >= 0;
41
+ if (allIndex >= 0) rest.splice(allIndex, 1);
42
+
43
+ // Parse --index flag for init
44
+ const shouldIndex = rest.includes('--index');
45
+ const indexIdx = rest.indexOf('--index');
46
+ if (indexIdx >= 0) rest.splice(indexIdx, 1);
47
+
48
+ // Parse --feature <name> for survey
49
+ const featureIndex = rest.indexOf('--feature');
50
+ let featureName: string | undefined;
51
+ if (featureIndex >= 0 && featureIndex + 1 < rest.length) {
52
+ featureName = rest[featureIndex + 1];
53
+ rest.splice(featureIndex, 2);
54
+ }
55
+
56
+ // Parse limit for search
57
+ const limitIndex = rest.indexOf('--limit');
58
+ let limit: number | undefined;
59
+ if (limitIndex >= 0 && limitIndex + 1 < rest.length) {
60
+ limit = parseInt(rest[limitIndex + 1], 10);
61
+ rest.splice(limitIndex, 2);
62
+ }
63
+
64
+ try {
65
+ switch (subcommand) {
66
+ case 'init':
67
+ return await handleInit(projectRoot, { index: shouldIndex, json: isJson });
68
+
69
+ case 'sync':
70
+ return await handleSync(projectRoot, { json: isJson });
71
+
72
+ case 'status':
73
+ return await handleStatus(projectRoot, { json: isJson });
74
+
75
+ case 'search': {
76
+ const query = rest.join(' ');
77
+ if (!query) {
78
+ stderr.write('Usage: apltk codegraph search <query> [--limit N] [--json]\n');
79
+ return 1;
80
+ }
81
+ return await handleSearch(projectRoot, query, { limit, json: isJson });
82
+ }
83
+
84
+ case 'explore': {
85
+ const query = rest.join(' ');
86
+ if (!query) {
87
+ stderr.write('Usage: apltk codegraph explore <query> [--json]\n');
88
+ return 1;
89
+ }
90
+ return await handleExplore(projectRoot, query, { json: isJson, feature: featureName });
91
+ }
92
+
93
+ case 'survey': {
94
+ const dirPath = rest[0] || '.';
95
+ return await handleSurvey(projectRoot, dirPath, { feature: featureName, json: isJson });
96
+ }
97
+
98
+ case 'list-apis': {
99
+ const pathArg = rest[0];
100
+ const combinedPath = featureName
101
+ ? (pathArg ? `${featureName}/${pathArg.replace(/^\//, '')}` : featureName)
102
+ : pathArg;
103
+ return await handleListApis(projectRoot, combinedPath, { all: isAll, json: isJson });
104
+ }
105
+
106
+ case 'verify': {
107
+ if (!specDir) {
108
+ stderr.write('Usage: apltk codegraph verify --spec <spec-dir> [--json]\n');
109
+ return 1;
110
+ }
111
+ return await handleVerify(projectRoot, specDir, { json: isJson });
112
+ }
113
+
114
+ default:
115
+ stderr.write(`Unknown subcommand: ${subcommand}\n\n`);
116
+ printHelp(stderr);
117
+ return 1;
118
+ }
119
+ } catch (error: any) {
120
+ if (error.code === 'MODULE_NOT_FOUND' || (error.message && error.message.includes('Cannot find module'))) {
121
+ stderr.write('`@colbymchenry/codegraph` is not installed. Run `npm install @colbymchenry/codegraph` in your project directory.\n');
122
+ } else {
123
+ stderr.write(`Error running codegraph ${subcommand}: ${error.message}\n`);
124
+ }
125
+ return 1;
126
+ }
127
+ }
128
+
129
+ function printHelp(stream: NodeJS.WriteStream): void {
130
+ stream.write(`Usage: apltk codegraph <subcommand> [options]
131
+
132
+ Subcommands:
133
+
134
+ lifecycle:
135
+ init Initialize CodeGraph for the project
136
+ --index Run initial indexing after init
137
+
138
+ sync Sync the index with current file state
139
+
140
+ status Show index statistics (files, nodes, edges)
141
+
142
+ discovery:
143
+ search <query> Search the code graph for symbols
144
+ --limit N Max results (default: 20)
145
+
146
+ explore <query> Deep-dive on a symbol (callers, callees, source)
147
+ --json JSON output
148
+
149
+ survey [dir] Scan a directory and suggest submodule groupings
150
+ --feature <name> Feature context
151
+ --json JSON output
152
+
153
+ list-apis [path] List public APIs in the project or a sub-path
154
+ --all Include non-exported symbols
155
+ --json JSON output
156
+
157
+ validation:
158
+ verify Verify a spec overlay against the actual code
159
+ --spec <dir> Spec directory (required)
160
+ --json JSON output
161
+
162
+ Global options:
163
+ --json Output as JSON instead of human-readable format
164
+ --help Show this help message
165
+ `);
166
+ }
167
+
168
+ export const tool: ToolDefinition = {
169
+ name: 'codegraph',
170
+ category: 'Code analysis',
171
+ description: 'CodeGraph code intelligence — init, sync, status, search, explore, survey, list-apis, verify',
172
+ handler: codegraphHandler,
173
+ };
@@ -0,0 +1,36 @@
1
+ import { describe, it, before, after } 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
+
7
+ import { createOrOpenIndex } from './cg-instance.js';
8
+
9
+ describe('createOrOpenIndex', () => {
10
+ let tmpDir: string;
11
+
12
+ before(() => {
13
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-test-'));
14
+ });
15
+
16
+ after(() => {
17
+ fs.rmSync(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it('should throw when project is already initialized', async () => {
21
+ // Arrange: create .codegraph/ and codegraph.db to simulate an initialized project
22
+ const codegraphDir = path.join(tmpDir, '.codegraph');
23
+ fs.mkdirSync(codegraphDir, { recursive: true });
24
+ fs.writeFileSync(path.join(codegraphDir, 'codegraph.db'), '');
25
+
26
+ // Act & Assert
27
+ await assert.rejects(
28
+ () => createOrOpenIndex(tmpDir),
29
+ (err: unknown) => {
30
+ assert.ok(err instanceof Error);
31
+ assert.match(err.message, /sync/);
32
+ return true;
33
+ },
34
+ );
35
+ });
36
+ });
@@ -0,0 +1,66 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let _codeGraphModule: any = null;
7
+ export function getCodeGraphModule(): { CodeGraph: any; findNearestCodeGraphRoot: any } {
8
+ if (!_codeGraphModule) {
9
+ _codeGraphModule = require('@colbymchenry/codegraph');
10
+ }
11
+ return _codeGraphModule;
12
+ }
13
+
14
+ /**
15
+ * Locate the project root by walking up from the given directory.
16
+ * Returns the nearest parent containing `.codegraph/`, or falls back
17
+ * to the nearest parent containing `package.json`.
18
+ */
19
+ export function findProjectRoot(startPath?: string): string {
20
+ const cwd = startPath || process.cwd();
21
+ const codegraphRoot = getCodeGraphModule().findNearestCodeGraphRoot(cwd);
22
+ if (codegraphRoot) return codegraphRoot;
23
+
24
+ // Fallback: walk up looking for package.json
25
+ let dir = path.resolve(cwd);
26
+ while (true) {
27
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) return cwd; // hit filesystem root
30
+ dir = parent;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Initialize a CodeGraph index for the given project root.
36
+ *
37
+ * If the project is already initialized, throws an error suggesting
38
+ * `apltk codegraph sync` instead. Otherwise, initializes a new CodeGraph
39
+ * project. When `options.index` is true, runs initial indexing after init.
40
+ *
41
+ * Note: `CodeGraph.init()` supports an `{ index: true }` shorthand that
42
+ * runs initial indexing inline -- this deviates from a two-step init-then-index
43
+ * pattern but is the supported API through the npm package.
44
+ */
45
+ export async function createOrOpenIndex(
46
+ projectRoot: string,
47
+ options?: { index?: boolean; onProgress?: (progress: any) => void },
48
+ ): Promise<any> {
49
+ const isInit = getCodeGraphModule().CodeGraph.isInitialized(projectRoot);
50
+ if (isInit) {
51
+ throw new Error(
52
+ `Project is already initialized at ${projectRoot}. Use \`apltk codegraph sync\` to update the index.`,
53
+ );
54
+ }
55
+ return getCodeGraphModule().CodeGraph.init(projectRoot, {
56
+ index: options?.index ?? false,
57
+ onProgress: options?.onProgress,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Close a CodeGraph instance and release resources.
63
+ */
64
+ export function closeIndex(cg: any): void {
65
+ cg.close();
66
+ }
@@ -0,0 +1,195 @@
1
+ import { describe, it, mock, before } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Shared mutable mock state — each test sets its own data before calling the
7
+ // handler. The mock instance's searchNodes captures this array by reference,
8
+ // so mutations are reflected in every invocation.
9
+ // ---------------------------------------------------------------------------
10
+ interface MockSymbolNode {
11
+ id: string;
12
+ name: string;
13
+ kind: string;
14
+ filePath: string;
15
+ startLine: number;
16
+ endLine: number;
17
+ qualifiedName: string;
18
+ signature?: string;
19
+ }
20
+
21
+ interface MockSearchResult {
22
+ node: MockSymbolNode;
23
+ score: number;
24
+ }
25
+
26
+ const mockSearchResults: MockSearchResult[] = [];
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Pre-load @colbymchenry/codegraph so we can mock its methods before
30
+ // cmd-explore.js performs its own require().
31
+ // ---------------------------------------------------------------------------
32
+ const require = createRequire(import.meta.url);
33
+ const { CodeGraph } = require('@colbymchenry/codegraph');
34
+
35
+ let mockInstance: {
36
+ searchNodes: () => { node: MockSymbolNode; score: number }[];
37
+ getCallers: () => never[];
38
+ getCallees: () => never[];
39
+ getCode: () => Promise<null>;
40
+ close: () => void;
41
+ } | undefined;
42
+
43
+ before(() => {
44
+ mockInstance = {
45
+ searchNodes: () => mockSearchResults.map(r => ({ node: r.node, score: r.score })),
46
+ getCallers: () => [],
47
+ getCallees: () => [],
48
+ getCode: async () => null,
49
+ close: () => {},
50
+ };
51
+ mock.method(CodeGraph, 'open', async () => mockInstance);
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Import the module under test (same cached CodeGraph is used internally)
56
+ // ---------------------------------------------------------------------------
57
+ let handleExplore: (
58
+ projectRoot: string,
59
+ query: string,
60
+ options?: Record<string, unknown>,
61
+ ) => Promise<number>;
62
+
63
+ before(async () => {
64
+ const mod = await import('./cmd-explore.js');
65
+ handleExplore = mod.handleExplore;
66
+ });
67
+
68
+ // =========================================================================
69
+ // REGTEST-7: Explore output should group symbols by file
70
+ // =========================================================================
71
+ describe('REGTEST-7: Explore grouping by file', () => {
72
+ it('should group symbols under a single file header', async () => {
73
+ // Arrange: two symbols in the same file
74
+ mockSearchResults.length = 0;
75
+ mockSearchResults.push(
76
+ {
77
+ node: {
78
+ id: '1',
79
+ name: 'addUser',
80
+ kind: 'function',
81
+ filePath: 'src/utils.ts',
82
+ startLine: 10,
83
+ endLine: 25,
84
+ qualifiedName: 'utils.addUser',
85
+ signature: '(name: string, age: number): User',
86
+ },
87
+ score: 0.95,
88
+ },
89
+ {
90
+ node: {
91
+ id: '2',
92
+ name: 'deleteUser',
93
+ kind: 'function',
94
+ filePath: 'src/utils.ts',
95
+ startLine: 30,
96
+ endLine: 40,
97
+ qualifiedName: 'utils.deleteUser',
98
+ signature: '(id: number): void',
99
+ },
100
+ score: 0.85,
101
+ },
102
+ );
103
+
104
+ // Capture stdout
105
+ const stdoutChunks: string[] = [];
106
+ const origWrite = process.stdout.write;
107
+ process.stdout.write = ((chunk: unknown) => {
108
+ stdoutChunks.push(String(chunk));
109
+ return true;
110
+ }) as typeof process.stdout.write;
111
+
112
+ try {
113
+ // Act
114
+ const exitCode = await handleExplore('/fake/project', 'utils', {});
115
+
116
+ // Assert
117
+ assert.strictEqual(exitCode, 0);
118
+ const output = stdoutChunks.join('');
119
+
120
+ // Exactly one file header for the shared filePath
121
+ const headerMatches = output.match(/=== src\/utils\.ts ===/g);
122
+ assert.strictEqual(
123
+ headerMatches?.length,
124
+ 1,
125
+ 'Expected exactly one file header for src/utils.ts, ' +
126
+ `got ${headerMatches?.length ?? 0}`,
127
+ );
128
+
129
+ // Both symbols appear in the output
130
+ assert.ok(output.includes('addUser'), 'Output should contain addUser');
131
+ assert.ok(output.includes('deleteUser'), 'Output should contain deleteUser');
132
+
133
+ // File header precedes both symbols (not duplicated)
134
+ const headerIdx = output.indexOf('=== src/utils.ts ===');
135
+ assert.ok(
136
+ headerIdx < output.indexOf('addUser'),
137
+ 'File header should appear before addUser',
138
+ );
139
+ assert.ok(
140
+ headerIdx < output.indexOf('deleteUser'),
141
+ 'File header should appear before deleteUser',
142
+ );
143
+ } finally {
144
+ process.stdout.write = origWrite;
145
+ }
146
+ });
147
+ });
148
+
149
+ // =========================================================================
150
+ // REGTEST-8: Explore --feature acceptance
151
+ // =========================================================================
152
+ describe('REGTEST-8: Explore --feature acceptance', () => {
153
+ it('should accept feature parameter without error', async () => {
154
+ // Arrange: at least one result so the feature line is emitted
155
+ mockSearchResults.length = 0;
156
+ mockSearchResults.push({
157
+ node: {
158
+ id: '3',
159
+ name: 'authLogin',
160
+ kind: 'function',
161
+ filePath: 'src/auth.ts',
162
+ startLine: 5,
163
+ endLine: 20,
164
+ qualifiedName: 'auth.login',
165
+ signature: '(credentials: Record<string, unknown>): Session',
166
+ },
167
+ score: 0.9,
168
+ });
169
+
170
+ const stdoutChunks: string[] = [];
171
+ const origWrite = process.stdout.write;
172
+ process.stdout.write = ((chunk: unknown) => {
173
+ stdoutChunks.push(String(chunk));
174
+ return true;
175
+ }) as typeof process.stdout.write;
176
+
177
+ try {
178
+ // Act — pass feature without json, expect no error
179
+ const exitCode = await handleExplore('/fake/project', 'authLogin', {
180
+ feature: 'auth',
181
+ json: false,
182
+ });
183
+
184
+ // Assert
185
+ assert.strictEqual(exitCode, 0, 'Should return exit code 0');
186
+ const output = stdoutChunks.join('');
187
+ assert.ok(
188
+ output.includes('Feature: auth'),
189
+ 'Output should include "Feature: auth" header',
190
+ );
191
+ } finally {
192
+ process.stdout.write = origWrite;
193
+ }
194
+ });
195
+ });
@@ -0,0 +1,129 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ import { closeIndex } from './cg-instance.js';
4
+ import { formatOutput } from './formatter.js';
5
+
6
+ export interface ExploreOptions {
7
+ json?: boolean;
8
+ feature?: string;
9
+ }
10
+
11
+ export async function handleExplore(
12
+ projectRoot: string,
13
+ query: string,
14
+ options: ExploreOptions = {},
15
+ ): Promise<number> {
16
+ const { CodeGraph } = require('@colbymchenry/codegraph');
17
+ const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
18
+
19
+ // Step 1: Search for the query
20
+ const searchResults = cg.searchNodes(query, { limit: 10 });
21
+
22
+ if (searchResults.length === 0) {
23
+ process.stdout.write('No symbols found matching the query.\n');
24
+ closeIndex(cg);
25
+ return 0;
26
+ }
27
+
28
+ // Step 2: For each result, get callers, callees, and source code
29
+ type SymbolDetail = {
30
+ name: string;
31
+ kind: string;
32
+ filePath: string;
33
+ startLine: number;
34
+ endLine: number;
35
+ qualifiedName: string;
36
+ signature?: string;
37
+ callers: Array<{ name: string; filePath: string; startLine: number }>;
38
+ callees: Array<{ name: string; filePath: string; startLine: number }>;
39
+ code: string | null;
40
+ };
41
+
42
+ const details: SymbolDetail[] = [];
43
+ for (const result of searchResults) {
44
+ const node = result.node;
45
+ const callers = cg.getCallers(node.id).map((c: any) => ({
46
+ name: c.node.name,
47
+ filePath: c.node.filePath,
48
+ startLine: c.node.startLine,
49
+ }));
50
+ const callees = cg.getCallees(node.id).map((c: any) => ({
51
+ name: c.node.name,
52
+ filePath: c.node.filePath,
53
+ startLine: c.node.startLine,
54
+ }));
55
+ const code = await cg.getCode(node.id);
56
+
57
+ details.push({
58
+ name: node.name,
59
+ kind: node.kind,
60
+ filePath: node.filePath,
61
+ startLine: node.startLine,
62
+ endLine: node.endLine,
63
+ qualifiedName: node.qualifiedName,
64
+ signature: node.signature,
65
+ callers,
66
+ callees,
67
+ code,
68
+ });
69
+ }
70
+
71
+ closeIndex(cg);
72
+
73
+ if (options.json) {
74
+ process.stdout.write(formatOutput(details, { json: true }) + '\n');
75
+ return 0;
76
+ }
77
+
78
+ // Human-readable output — group by filePath
79
+ const grouped = new Map<string, SymbolDetail[]>();
80
+ for (const d of details) {
81
+ const group = grouped.get(d.filePath) ?? [];
82
+ group.push(d);
83
+ grouped.set(d.filePath, group);
84
+ }
85
+
86
+ if (options.feature) {
87
+ process.stdout.write(`Feature: ${options.feature}\n`);
88
+ }
89
+
90
+ for (const [filePath, symbols] of grouped) {
91
+ process.stdout.write(`\n=== ${filePath} ===\n\n`);
92
+ for (const d of symbols) {
93
+ process.stdout.write(` ${d.name} [${d.kind}] line ${d.startLine}-${d.endLine}\n`);
94
+ process.stdout.write(` QName: ${d.qualifiedName}\n`);
95
+ if (d.signature) process.stdout.write(` Signature: ${d.signature}\n`);
96
+
97
+ process.stdout.write(` Callers (${d.callers.length}):\n`);
98
+ if (d.callers.length === 0) {
99
+ process.stdout.write(' (none)\n');
100
+ } else {
101
+ for (const c of d.callers.slice(0, 20)) {
102
+ process.stdout.write(` ${c.name} ${c.filePath}:${c.startLine}\n`);
103
+ }
104
+ }
105
+
106
+ process.stdout.write(` Callees (${d.callees.length}):\n`);
107
+ if (d.callees.length === 0) {
108
+ process.stdout.write(' (none)\n');
109
+ } else {
110
+ for (const c of d.callees.slice(0, 20)) {
111
+ process.stdout.write(` ${c.name} ${c.filePath}:${c.startLine}\n`);
112
+ }
113
+ }
114
+
115
+ if (d.code) {
116
+ process.stdout.write(` Source (${d.filePath}):\n`);
117
+ const lines = d.code.split('\n');
118
+ for (let i = 0; i < Math.min(lines.length, 30); i++) {
119
+ process.stdout.write(` ${lines[i]}\n`);
120
+ }
121
+ if (lines.length > 30) {
122
+ process.stdout.write(` ... (${lines.length - 30} more lines)\n`);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ return 0;
129
+ }
@@ -0,0 +1,94 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ describe('REGTEST-R2-01: handleListApis --all flag splice', () => {
8
+ it('should include non-exported symbols with all=true and filter them with all=false', async (ctx) => {
9
+ // Arrange: three nodes — two exported, one non-exported
10
+ const nodes = [
11
+ {
12
+ id: 'n1',
13
+ name: 'funcA',
14
+ kind: 'function',
15
+ filePath: 'src/feature/a.ts',
16
+ startLine: 10,
17
+ endLine: 30,
18
+ qualifiedName: 'funcA',
19
+ signature: '(x: string): void',
20
+ isExported: true,
21
+ },
22
+ {
23
+ id: 'n2',
24
+ name: 'funcB',
25
+ kind: 'function',
26
+ filePath: 'src/lib/b.ts',
27
+ startLine: 5,
28
+ endLine: 25,
29
+ qualifiedName: 'funcB',
30
+ signature: '(y: number): string',
31
+ isExported: false,
32
+ },
33
+ {
34
+ id: 'n3',
35
+ name: 'funcC',
36
+ kind: 'function',
37
+ filePath: 'src/lib/c.ts',
38
+ startLine: 1,
39
+ endLine: 20,
40
+ qualifiedName: 'funcC',
41
+ signature: '(z: boolean): void',
42
+ isExported: true,
43
+ },
44
+ ];
45
+
46
+ // Mock CodeGraph.open before importing the module under test
47
+ const { CodeGraph } = require('@colbymchenry/codegraph');
48
+ const openMock = mock.method(CodeGraph, 'open', async () => ({
49
+ getNodesByKind: (_kind: string) => nodes,
50
+ getCallers: (_id: string) => [],
51
+ close: () => {},
52
+ }));
53
+
54
+ const { handleListApis } = await import('./cmd-list-apis.js');
55
+
56
+ // Test 1: all=true — all symbols appear, grouped by directory
57
+ {
58
+ const chunks: string[] = [];
59
+ ctx.mock.method(process.stdout, 'write', (chunk: string | Uint8Array) => {
60
+ chunks.push(String(chunk));
61
+ });
62
+
63
+ await handleListApis('/fake/root', undefined, { all: true });
64
+
65
+ const output = chunks.join('');
66
+ assert.ok(output.includes('funcA'), 'all=true: should include exported funcA');
67
+ assert.ok(output.includes('funcB'), 'all=true: should include non-exported funcB');
68
+ assert.ok(output.includes('funcC'), 'all=true: should include exported funcC');
69
+ assert.ok(output.includes('=== src/feature/ ==='), 'all=true: should group src/feature/');
70
+ assert.ok(output.includes('=== src/lib/ ==='), 'all=true: should group src/lib/');
71
+ ctx.mock.reset();
72
+ }
73
+
74
+ // Test 2: all=false — only exported symbols, ungrouped
75
+ {
76
+ const chunks: string[] = [];
77
+ ctx.mock.method(process.stdout, 'write', (chunk: string | Uint8Array) => {
78
+ chunks.push(String(chunk));
79
+ });
80
+
81
+ await handleListApis('/fake/root', undefined, { all: false });
82
+
83
+ const output = chunks.join('');
84
+ assert.ok(output.includes('funcA'), 'all=false: should include exported funcA');
85
+ assert.ok(!output.includes('funcB'), 'all=false: should NOT include non-exported funcB');
86
+ assert.ok(output.includes('funcC'), 'all=false: should include exported funcC');
87
+ assert.ok(!output.includes('=== src/feature/ ==='), 'all=false: should not group');
88
+ assert.ok(!output.includes('=== src/lib/ ==='), 'all=false: should not group');
89
+ }
90
+
91
+ // Clean up global mocks (CodeGraph.open)
92
+ mock.reset();
93
+ });
94
+ });