@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.
- package/AGENTS.md +37 -27
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +37 -27
- package/README.md +15 -2
- package/assets/spec/rg13-1780435029246/test-questions.json +1 -0
- package/assets/spec/rg13-1780468345132/test-questions.json +1 -0
- package/package.json +3 -3
- package/packages/cli/dist/tool-registration.js +1 -0
- package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/cli/tool-registration.ts +1 -0
- package/packages/tools/architecture/dist/index.js +539 -2
- package/packages/tools/architecture/dist/index.test.d.ts +1 -0
- package/packages/tools/architecture/dist/index.test.js +229 -0
- package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/architecture/index.test.ts +329 -0
- package/packages/tools/architecture/index.ts +607 -5
- package/packages/tools/codegraph/dist/index.d.ts +3 -0
- package/packages/tools/codegraph/dist/index.js +157 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.d.ts +29 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.js +59 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.test.js +27 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.js +95 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.test.js +133 -0
- package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.js +83 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.js +50 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.test.js +51 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.js +64 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.js +69 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.js +21 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.test.js +30 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.js +44 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.test.js +72 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.d.ts +36 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.js +142 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.test.js +136 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.js +51 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.test.js +30 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.js +134 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.test.js +139 -0
- package/packages/tools/codegraph/dist/lib/formatter.d.ts +67 -0
- package/packages/tools/codegraph/dist/lib/formatter.js +107 -0
- package/packages/tools/codegraph/dist/lib/formatter.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/formatter.test.js +41 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.d.ts +19 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.js +194 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.test.js +62 -0
- package/packages/tools/codegraph/dist/lib/survey/scanner.d.ts +31 -0
- package/packages/tools/codegraph/dist/lib/survey/scanner.js +50 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.d.ts +32 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.js +146 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.test.js +128 -0
- package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/tools/codegraph/env.d.ts +56 -0
- package/packages/tools/codegraph/index.ts +173 -0
- package/packages/tools/codegraph/lib/cg-instance.test.ts +36 -0
- package/packages/tools/codegraph/lib/cg-instance.ts +66 -0
- package/packages/tools/codegraph/lib/cmd-explore.test.ts +195 -0
- package/packages/tools/codegraph/lib/cmd-explore.ts +129 -0
- package/packages/tools/codegraph/lib/cmd-flag-splice.test.ts +94 -0
- package/packages/tools/codegraph/lib/cmd-init.test.ts +68 -0
- package/packages/tools/codegraph/lib/cmd-init.ts +60 -0
- package/packages/tools/codegraph/lib/cmd-list-apis.test.ts +80 -0
- package/packages/tools/codegraph/lib/cmd-list-apis.ts +90 -0
- package/packages/tools/codegraph/lib/cmd-search.test.ts +37 -0
- package/packages/tools/codegraph/lib/cmd-search.ts +32 -0
- package/packages/tools/codegraph/lib/cmd-status.test.ts +86 -0
- package/packages/tools/codegraph/lib/cmd-status.ts +53 -0
- package/packages/tools/codegraph/lib/cmd-survey.test.ts +161 -0
- package/packages/tools/codegraph/lib/cmd-survey.ts +199 -0
- package/packages/tools/codegraph/lib/cmd-sync.test.ts +41 -0
- package/packages/tools/codegraph/lib/cmd-sync.ts +62 -0
- package/packages/tools/codegraph/lib/cmd-verify.test.ts +162 -0
- package/packages/tools/codegraph/lib/cmd-verify.ts +145 -0
- package/packages/tools/codegraph/lib/formatter.test.ts +47 -0
- package/packages/tools/codegraph/lib/formatter.ts +130 -0
- package/packages/tools/codegraph/lib/survey/grouper.test.ts +72 -0
- package/packages/tools/codegraph/lib/survey/grouper.ts +226 -0
- package/packages/tools/codegraph/lib/survey/scanner.ts +89 -0
- package/packages/tools/codegraph/lib/verify/checker.test.ts +140 -0
- package/packages/tools/codegraph/lib/verify/checker.ts +172 -0
- package/packages/tools/codegraph/package.json +23 -0
- package/packages/tools/codegraph/tsconfig.json +22 -0
- package/resources/project-architecture/atlas/atlas.history.log +32 -0
- package/resources/project-architecture/atlas/atlas.history.undo.json +356 -28
- package/resources/project-architecture/atlas/atlas.history.undo.stack.json +14350 -0
- package/resources/project-architecture/atlas/atlas.index.yaml +76 -12
- package/resources/project-architecture/atlas/features/codegraph.yaml +95 -0
- package/resources/project-architecture/atlas/features/eval-ci-gate.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-cli.yaml +16 -1
- package/resources/project-architecture/atlas/features/eval-executor.yaml +12 -2
- package/resources/project-architecture/atlas/features/eval-isolation.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-optimizer.yaml +17 -2
- package/resources/project-architecture/atlas/features/eval-question.yaml +12 -2
- package/resources/project-architecture/atlas/features/eval-reporter.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-scorer.yaml +12 -2
- package/resources/project-architecture/features/codegraph/cg-discovery.html +47 -0
- package/resources/project-architecture/features/codegraph/cg-lifecycle.html +48 -0
- package/resources/project-architecture/features/codegraph/cg-validation.html +47 -0
- package/resources/project-architecture/features/codegraph/index.html +58 -0
- package/resources/project-architecture/features/eval-ci-gate/workflow-trigger.html +6 -1
- package/resources/project-architecture/features/eval-cli/cli-handler.html +8 -1
- package/resources/project-architecture/features/eval-executor/exec-api-client.html +6 -1
- package/resources/project-architecture/features/eval-executor/trace-recorder.html +6 -1
- package/resources/project-architecture/features/eval-isolation/tool-dispatcher.html +6 -1
- package/resources/project-architecture/features/eval-optimizer/dedup-engine.html +6 -1
- package/resources/project-architecture/features/eval-optimizer/issue-extractor.html +7 -1
- package/resources/project-architecture/features/eval-question/question-loader.html +6 -1
- package/resources/project-architecture/features/eval-question/variant-generator.html +6 -1
- package/resources/project-architecture/features/eval-reporter/report-composer.html +6 -1
- package/resources/project-architecture/features/eval-scorer/judge-api-client.html +6 -1
- package/resources/project-architecture/features/eval-scorer/judge-prompt-builder.html +6 -1
- package/resources/project-architecture/index.html +200 -94
- package/skills/design/SKILL.md +33 -0
- package/skills/init-project-html/SKILL.md +12 -11
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { closeIndex } from './cg-instance.js';
|
|
6
|
+
import { formatOutput } from './formatter.js';
|
|
7
|
+
import { scanDirectory } from './survey/scanner.js';
|
|
8
|
+
import { groupIntoSubmodules } from './survey/grouper.js';
|
|
9
|
+
|
|
10
|
+
export interface SurveyOptions {
|
|
11
|
+
feature?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SurveyReport {
|
|
16
|
+
directory: string;
|
|
17
|
+
feature?: string;
|
|
18
|
+
totalFiles: number;
|
|
19
|
+
totalSymbols: number;
|
|
20
|
+
files: Array<{
|
|
21
|
+
filePath: string;
|
|
22
|
+
language: string;
|
|
23
|
+
symbolCount: number;
|
|
24
|
+
}>;
|
|
25
|
+
entryPoints: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
filePath: string;
|
|
29
|
+
startLine: number;
|
|
30
|
+
isExported: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
suggestedSubmodules: Array<{
|
|
33
|
+
slug: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
role: string;
|
|
36
|
+
memberFunctions: string[];
|
|
37
|
+
memberFiles: string[];
|
|
38
|
+
}>;
|
|
39
|
+
suggestedEdges: Array<{
|
|
40
|
+
source: string;
|
|
41
|
+
target: string;
|
|
42
|
+
kind: string;
|
|
43
|
+
label: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function handleSurvey(
|
|
48
|
+
projectRoot: string,
|
|
49
|
+
dirPath: string,
|
|
50
|
+
options: SurveyOptions = {},
|
|
51
|
+
): Promise<number> {
|
|
52
|
+
// Check that the target directory exists
|
|
53
|
+
const targetPath = path.resolve(projectRoot, dirPath);
|
|
54
|
+
if (!existsSync(targetPath)) {
|
|
55
|
+
process.stderr.write(`Error: Directory not found: ${dirPath}\n`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
60
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
|
|
61
|
+
|
|
62
|
+
// Scan the directory
|
|
63
|
+
const scan = await scanDirectory(cg, dirPath);
|
|
64
|
+
|
|
65
|
+
const fileSet = new Set(scan.files.map((f) => f.filePath));
|
|
66
|
+
|
|
67
|
+
// Group into submodule suggestions
|
|
68
|
+
const suggestions = groupIntoSubmodules(scan, cg);
|
|
69
|
+
|
|
70
|
+
// Build edge suggestions from cross-file call relationships
|
|
71
|
+
const edgeSuggestions = buildEdgeSuggestions(scan, cg, fileSet);
|
|
72
|
+
|
|
73
|
+
// Determine entry points: exported symbols called from outside the scanned directory
|
|
74
|
+
const entryPoints = scan.allSymbols.filter(s => {
|
|
75
|
+
if (!s.isExported) return false;
|
|
76
|
+
const nodes = cg.getNodesByName(s.name);
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (node.filePath !== s.filePath) continue;
|
|
79
|
+
const callers = cg.getCallers(node.id);
|
|
80
|
+
for (const caller of callers) {
|
|
81
|
+
if (!fileSet.has(caller.node.filePath)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
closeIndex(cg);
|
|
90
|
+
|
|
91
|
+
const report: SurveyReport = {
|
|
92
|
+
directory: dirPath,
|
|
93
|
+
feature: options.feature,
|
|
94
|
+
totalFiles: scan.totalFiles,
|
|
95
|
+
totalSymbols: scan.totalSymbols,
|
|
96
|
+
files: scan.files.map((f) => ({
|
|
97
|
+
filePath: f.filePath,
|
|
98
|
+
language: f.language,
|
|
99
|
+
symbolCount: f.symbols.length,
|
|
100
|
+
})),
|
|
101
|
+
entryPoints,
|
|
102
|
+
suggestedSubmodules: suggestions.map((s) => ({
|
|
103
|
+
slug: s.slug,
|
|
104
|
+
kind: s.kind,
|
|
105
|
+
role: s.role,
|
|
106
|
+
memberFunctions: s.memberFunctions,
|
|
107
|
+
memberFiles: s.memberFiles,
|
|
108
|
+
})),
|
|
109
|
+
suggestedEdges: edgeSuggestions,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (options.json) {
|
|
113
|
+
process.stdout.write(formatOutput(report, { json: true }) + '\n');
|
|
114
|
+
} else {
|
|
115
|
+
// Human-readable output
|
|
116
|
+
process.stdout.write(`\n=== Survey: ${dirPath} ===\n`);
|
|
117
|
+
if (report.feature) {
|
|
118
|
+
process.stdout.write(`Feature: ${report.feature}\n`);
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write('\n');
|
|
121
|
+
|
|
122
|
+
process.stdout.write(`Files: ${report.totalFiles} Symbols: ${report.totalSymbols}\n\n`);
|
|
123
|
+
|
|
124
|
+
process.stdout.write('Files:\n');
|
|
125
|
+
for (const f of report.files) {
|
|
126
|
+
process.stdout.write(` ${f.filePath} [${f.language}] (${f.symbolCount} symbols)\n`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.stdout.write('\nEntry Points:\n');
|
|
130
|
+
if (report.entryPoints.length === 0) {
|
|
131
|
+
process.stdout.write(' (none)\n');
|
|
132
|
+
} else {
|
|
133
|
+
for (const ep of report.entryPoints) {
|
|
134
|
+
process.stdout.write(` ${ep.name} [${ep.kind}] ${ep.filePath}:${ep.startLine}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
process.stdout.write('\nSuggested Submodules:\n');
|
|
139
|
+
if (report.suggestedSubmodules.length === 0) {
|
|
140
|
+
process.stdout.write(' (none)\n');
|
|
141
|
+
} else {
|
|
142
|
+
for (const sub of report.suggestedSubmodules) {
|
|
143
|
+
process.stdout.write(` ${sub.slug} [${sub.kind}] ${sub.role}\n`);
|
|
144
|
+
process.stdout.write(` Functions: ${sub.memberFunctions.join(', ')}\n`);
|
|
145
|
+
process.stdout.write(` Files: ${sub.memberFiles.join(', ')}\n\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.stdout.write('Suggested Edges:\n');
|
|
150
|
+
if (report.suggestedEdges.length === 0) {
|
|
151
|
+
process.stdout.write(' (none)\n');
|
|
152
|
+
} else {
|
|
153
|
+
for (const edge of report.suggestedEdges) {
|
|
154
|
+
process.stdout.write(` ${edge.source} --[${edge.kind}]--> ${edge.target} (${edge.label})\n`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.stdout.write('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build suggested edges from cross-file call relationships in the scanned directory.
|
|
166
|
+
*/
|
|
167
|
+
function buildEdgeSuggestions(
|
|
168
|
+
scan: Awaited<ReturnType<typeof scanDirectory>>,
|
|
169
|
+
cg: any,
|
|
170
|
+
fileSet: Set<string>,
|
|
171
|
+
): SurveyReport['suggestedEdges'] {
|
|
172
|
+
const edges: SurveyReport['suggestedEdges'] = [];
|
|
173
|
+
const dedup = new Set<string>();
|
|
174
|
+
|
|
175
|
+
// For each symbol in the scan, check if it calls symbols outside the scanned directory
|
|
176
|
+
for (const sym of scan.allSymbols) {
|
|
177
|
+
const nodes = cg.getNodesByName(sym.name);
|
|
178
|
+
for (const node of nodes) {
|
|
179
|
+
if (node.filePath !== sym.filePath) continue;
|
|
180
|
+
const callees = cg.getCallees(node.id);
|
|
181
|
+
for (const callee of callees) {
|
|
182
|
+
// Only consider callees OUTSIDE the scanned directory (cross-boundary edges)
|
|
183
|
+
if (!fileSet.has(callee.node.filePath)) {
|
|
184
|
+
const edgeKey = `${sym.name}::${callee.node.name}`;
|
|
185
|
+
if (dedup.has(edgeKey)) continue;
|
|
186
|
+
dedup.add(edgeKey);
|
|
187
|
+
edges.push({
|
|
188
|
+
source: sym.name,
|
|
189
|
+
target: callee.node.name,
|
|
190
|
+
kind: 'call',
|
|
191
|
+
label: `${sym.filePath} -> ${callee.node.filePath}`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return edges;
|
|
199
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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('cmd-sync handleSync', () => {
|
|
8
|
+
it('should return exit code 1 when CodeGraph is not initialized', async () => {
|
|
9
|
+
// Pre-load CodeGraph via CJS require (same mechanism used by cmd-sync.ts)
|
|
10
|
+
// and mock isInitialized BEFORE the module under test is evaluated, so
|
|
11
|
+
// the mock is in place when cmd-sync.ts runs its own require().
|
|
12
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
13
|
+
const isInitializedMock = mock.method(CodeGraph, 'isInitialized', () => false);
|
|
14
|
+
|
|
15
|
+
// Capture stderr output
|
|
16
|
+
const stderrWriteMock = mock.method(process.stderr, 'write', () => true);
|
|
17
|
+
|
|
18
|
+
// Import the module under test after mocks are set up
|
|
19
|
+
const { handleSync } = await import('./cmd-sync.js');
|
|
20
|
+
|
|
21
|
+
// Call the handler with an uninitialized project
|
|
22
|
+
const exitCode = await handleSync('/tmp/fake-project', { json: false });
|
|
23
|
+
|
|
24
|
+
// Verify exit code is 1 (error)
|
|
25
|
+
assert.strictEqual(exitCode, 1);
|
|
26
|
+
|
|
27
|
+
// Verify stderr mentions "init"
|
|
28
|
+
assert.strictEqual(stderrWriteMock.mock.calls.length, 1);
|
|
29
|
+
const callArg = stderrWriteMock.mock.calls[0].arguments[0];
|
|
30
|
+
assert.ok(callArg !== undefined, 'Expected stderr write argument to be defined');
|
|
31
|
+
const stderrOutput = typeof callArg === 'string' ? callArg : Buffer.from(callArg).toString('utf8');
|
|
32
|
+
assert.ok(
|
|
33
|
+
stderrOutput.toLowerCase().includes('init'),
|
|
34
|
+
`Expected stderr to mention "init", got: ${JSON.stringify(stderrOutput)}`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Cleanup mocks
|
|
38
|
+
isInitializedMock.mock.restore();
|
|
39
|
+
stderrWriteMock.mock.restore();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
import { closeIndex } from './cg-instance.js';
|
|
4
|
+
import { formatSummary, formatOutput } from './formatter.js';
|
|
5
|
+
|
|
6
|
+
export interface SyncOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function handleSync(projectRoot: string, options: SyncOptions = {}): Promise<number> {
|
|
11
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
12
|
+
if (!CodeGraph.isInitialized(projectRoot)) {
|
|
13
|
+
process.stderr.write('CodeGraph is not initialized. Run `apltk codegraph init` first.\n');
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: false });
|
|
18
|
+
|
|
19
|
+
let progressEvents: Array<{ phase: string; current: number; total: number }> = [];
|
|
20
|
+
const result = await cg.sync({
|
|
21
|
+
onProgress: (p: any) => {
|
|
22
|
+
progressEvents.push({ phase: p.phase, current: p.current, total: p.total });
|
|
23
|
+
if (process.stdout.isTTY) {
|
|
24
|
+
process.stdout.write(`\r Indexing: ${p.phase} ${p.current}/${p.total}`);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (process.stdout.isTTY) {
|
|
30
|
+
process.stdout.write('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
closeIndex(cg);
|
|
34
|
+
|
|
35
|
+
const output = {
|
|
36
|
+
projectRoot,
|
|
37
|
+
filesChecked: result.filesChecked,
|
|
38
|
+
filesAdded: result.filesAdded,
|
|
39
|
+
filesModified: result.filesModified,
|
|
40
|
+
filesRemoved: result.filesRemoved,
|
|
41
|
+
nodesUpdated: result.nodesUpdated,
|
|
42
|
+
durationMs: result.durationMs,
|
|
43
|
+
progress: progressEvents.length > 0 ? progressEvents : undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (options.json) {
|
|
47
|
+
process.stdout.write(formatOutput(output, { json: true }) + '\n');
|
|
48
|
+
} else {
|
|
49
|
+
const summary: [string, string | number][] = [
|
|
50
|
+
['Project:', projectRoot],
|
|
51
|
+
['Checked:', result.filesChecked],
|
|
52
|
+
['Added:', result.filesAdded],
|
|
53
|
+
['Modified:', result.filesModified],
|
|
54
|
+
['Removed:', result.filesRemoved],
|
|
55
|
+
['Nodes updated:', result.nodesUpdated],
|
|
56
|
+
['Duration:', `${result.durationMs}ms`],
|
|
57
|
+
];
|
|
58
|
+
process.stdout.write(formatSummary(summary) + '\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* REGTEST-1: Verify that the js-yaml based parser (used in `loadOverlay`)
|
|
10
|
+
* correctly parses object-format YAML function declarations.
|
|
11
|
+
*
|
|
12
|
+
* The fix replaced a custom YAML parser with `js-yaml`. The old parser
|
|
13
|
+
* incorrectly produced raw string fragments like `"name: init"` when
|
|
14
|
+
* encountering object-format functions:
|
|
15
|
+
*
|
|
16
|
+
* functions:
|
|
17
|
+
* - name: init
|
|
18
|
+
* in: string
|
|
19
|
+
* out: void
|
|
20
|
+
*
|
|
21
|
+
* With `js-yaml`, the parser produces actual objects:
|
|
22
|
+
* `{ name: "init", in: "string", out: "void" }`
|
|
23
|
+
*
|
|
24
|
+
* This test exercises the same parsing path as `loadOverlay`: reading a YAML
|
|
25
|
+
* file from disk with `fs.readFileSync`, then parsing with `yaml.load`.
|
|
26
|
+
*/
|
|
27
|
+
describe('REGTEST-1: loadOverlay YAML parser', () => {
|
|
28
|
+
it('should parse object-format functions and correctly extract .name', () => {
|
|
29
|
+
const yamlStr = [
|
|
30
|
+
'slug: test-feature',
|
|
31
|
+
'submodules:',
|
|
32
|
+
' - slug: module-a',
|
|
33
|
+
' functions:',
|
|
34
|
+
' - name: init',
|
|
35
|
+
' in: string',
|
|
36
|
+
' out: void',
|
|
37
|
+
'edges:',
|
|
38
|
+
' - from: funcA',
|
|
39
|
+
' to: funcB',
|
|
40
|
+
' kind: call',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
// Parse with js-yaml (same library used by loadOverlay)
|
|
44
|
+
const data = yaml.load(yamlStr) as any;
|
|
45
|
+
|
|
46
|
+
// Verify object-format function parsing
|
|
47
|
+
const fn = data.submodules[0].functions[0];
|
|
48
|
+
assert.strictEqual(typeof fn, 'object', 'object-format function should be parsed as object, not string');
|
|
49
|
+
assert.strictEqual(fn.name, 'init', 'parsed function .name should be "init"');
|
|
50
|
+
|
|
51
|
+
// Verify the old bug is fixed: no raw "name: init" string
|
|
52
|
+
assert.notStrictEqual(fn, 'name: init', 'function should NOT be a raw string fragment');
|
|
53
|
+
assert.notStrictEqual(typeof fn, 'string', 'object-format function should not be a string');
|
|
54
|
+
|
|
55
|
+
// Verify additional fields are preserved
|
|
56
|
+
assert.strictEqual(fn.in, 'string', 'function "in" field should be preserved');
|
|
57
|
+
assert.strictEqual(fn.out, 'void', 'function "out" field should be preserved');
|
|
58
|
+
|
|
59
|
+
// Verify string-format functions still work
|
|
60
|
+
// Note: the YAML above only has object-format functions; string format is tested separately
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should parse string-format functions correctly', () => {
|
|
64
|
+
const yamlStr = [
|
|
65
|
+
'slug: test-feature',
|
|
66
|
+
'submodules:',
|
|
67
|
+
' - slug: module-a',
|
|
68
|
+
' functions:',
|
|
69
|
+
' - init',
|
|
70
|
+
' - process',
|
|
71
|
+
].join('\n');
|
|
72
|
+
|
|
73
|
+
const data = yaml.load(yamlStr) as any;
|
|
74
|
+
|
|
75
|
+
const functions = data.submodules[0].functions;
|
|
76
|
+
assert.strictEqual(functions.length, 2, 'should have 2 string-format functions');
|
|
77
|
+
assert.strictEqual(typeof functions[0], 'string', 'string-format function should be a string');
|
|
78
|
+
assert.strictEqual(functions[0], 'init', 'string function name should be "init"');
|
|
79
|
+
assert.strictEqual(functions[1], 'process', 'string function name should be "process"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should parse mixed object and string format functions', () => {
|
|
83
|
+
const yamlStr = [
|
|
84
|
+
'slug: test-feature',
|
|
85
|
+
'submodules:',
|
|
86
|
+
' - slug: module-a',
|
|
87
|
+
' functions:',
|
|
88
|
+
' - name: init',
|
|
89
|
+
' in: string',
|
|
90
|
+
' out: void',
|
|
91
|
+
' - process',
|
|
92
|
+
' - name: render',
|
|
93
|
+
' in: string',
|
|
94
|
+
' out: void',
|
|
95
|
+
' - cleanup',
|
|
96
|
+
].join('\n');
|
|
97
|
+
|
|
98
|
+
const data = yaml.load(yamlStr) as any;
|
|
99
|
+
const functions = data.submodules[0].functions;
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(functions.length, 4, 'should have 4 functions total');
|
|
102
|
+
|
|
103
|
+
// Index 0: object-format
|
|
104
|
+
assert.strictEqual(typeof functions[0], 'object', 'functions[0] should be object');
|
|
105
|
+
assert.strictEqual(functions[0].name, 'init');
|
|
106
|
+
|
|
107
|
+
// Index 1: string-format
|
|
108
|
+
assert.strictEqual(typeof functions[1], 'string', 'functions[1] should be string');
|
|
109
|
+
assert.strictEqual(functions[1], 'process');
|
|
110
|
+
|
|
111
|
+
// Index 2: object-format
|
|
112
|
+
assert.strictEqual(typeof functions[2], 'object', 'functions[2] should be object');
|
|
113
|
+
assert.strictEqual(functions[2].name, 'render');
|
|
114
|
+
|
|
115
|
+
// Index 3: string-format
|
|
116
|
+
assert.strictEqual(typeof functions[3], 'string', 'functions[3] should be string');
|
|
117
|
+
assert.strictEqual(functions[3], 'cleanup');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should parse feature YAML from disk (same path as loadOverlay)', async () => {
|
|
121
|
+
// Create a temp directory matching the loadOverlay structure
|
|
122
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-verify-test-'));
|
|
123
|
+
try {
|
|
124
|
+
const featuresDir = path.join(tmpDir, 'architecture_diff', 'atlas', 'features');
|
|
125
|
+
fs.mkdirSync(featuresDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
const yamlPath = path.join(featuresDir, 'test-feature.yaml');
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
yamlPath,
|
|
130
|
+
[
|
|
131
|
+
'slug: test-feature',
|
|
132
|
+
'submodules:',
|
|
133
|
+
' - slug: module-a',
|
|
134
|
+
' functions:',
|
|
135
|
+
' - name: init',
|
|
136
|
+
' in: string',
|
|
137
|
+
' out: void',
|
|
138
|
+
' edges:',
|
|
139
|
+
' - from: funcA',
|
|
140
|
+
' to: funcB',
|
|
141
|
+
' kind: call',
|
|
142
|
+
].join('\n') + '\n',
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Read file and parse (same as loadOverlay)
|
|
146
|
+
const raw = fs.readFileSync(yamlPath, 'utf8');
|
|
147
|
+
const data = yaml.load(raw) as any;
|
|
148
|
+
|
|
149
|
+
assert.ok(data, 'parsed data should be truthy');
|
|
150
|
+
assert.strictEqual(typeof data, 'object', 'parsed data should be an object');
|
|
151
|
+
assert.strictEqual(data.slug, 'test-feature', 'feature slug should be preserved');
|
|
152
|
+
|
|
153
|
+
const fn = data.submodules[0].functions[0];
|
|
154
|
+
assert.strictEqual(typeof fn, 'object', 'object-format function parsed from file should be object');
|
|
155
|
+
assert.strictEqual(fn.name, 'init', 'function .name should be "init"');
|
|
156
|
+
assert.strictEqual(fn.in, 'string', 'function .in should be "string"');
|
|
157
|
+
assert.strictEqual(fn.out, 'void', 'function .out should be "void"');
|
|
158
|
+
} finally {
|
|
159
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
import { closeIndex } from './cg-instance.js';
|
|
6
|
+
import { formatOutput } from './formatter.js';
|
|
7
|
+
import { verifyOverlay } from './verify/checker.js';
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
|
|
10
|
+
export interface VerifyOptions {
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read a spec overlay from the standard atlas directory layout.
|
|
16
|
+
* Loads the overlay in the same way as skills/init-project-html/lib/atlas/state.js::loadOverlay.
|
|
17
|
+
*/
|
|
18
|
+
function loadOverlay(specDir: string): any {
|
|
19
|
+
const overlayDir = path.join(specDir, 'architecture_diff', 'atlas');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(overlayDir)) {
|
|
22
|
+
throw new Error(`No architecture diff atlas found at: ${overlayDir}. Run "apltk architecture diff" first to generate the overlay.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const overlay: any = {
|
|
26
|
+
meta: null,
|
|
27
|
+
actors: null,
|
|
28
|
+
edges: null,
|
|
29
|
+
featureOrder: null,
|
|
30
|
+
features: {},
|
|
31
|
+
removed: { features: [], submodules: [] },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Parse atlas.index.yaml via js-yaml
|
|
35
|
+
const indexFile = path.join(overlayDir, 'atlas.index.yaml');
|
|
36
|
+
if (fs.existsSync(indexFile)) {
|
|
37
|
+
const raw = fs.readFileSync(indexFile, 'utf8');
|
|
38
|
+
if (raw.trim()) {
|
|
39
|
+
const index = yaml.load(raw) as any;
|
|
40
|
+
if (index && typeof index === 'object' && !Array.isArray(index)) {
|
|
41
|
+
if (index.meta !== undefined) overlay.meta = index.meta;
|
|
42
|
+
if (index.actors !== undefined) overlay.actors = index.actors;
|
|
43
|
+
if (index.edges !== undefined) overlay.edges = index.edges;
|
|
44
|
+
if (Array.isArray(index.features)) {
|
|
45
|
+
overlay.featureOrder = index.features
|
|
46
|
+
.map((entry: any) => (typeof entry === 'string' ? entry : entry?.slug))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load feature files via js-yaml
|
|
54
|
+
const featuresDir = path.join(overlayDir, 'features');
|
|
55
|
+
if (fs.existsSync(featuresDir)) {
|
|
56
|
+
for (const entry of fs.readdirSync(featuresDir)) {
|
|
57
|
+
if (!entry.endsWith('.yaml')) continue;
|
|
58
|
+
const featureFile = path.join(featuresDir, entry);
|
|
59
|
+
const raw = fs.readFileSync(featureFile, 'utf8');
|
|
60
|
+
if (raw.trim()) {
|
|
61
|
+
const data = yaml.load(raw) as any;
|
|
62
|
+
if (data && typeof data === 'object' && data.slug) {
|
|
63
|
+
const feature: any = {
|
|
64
|
+
slug: data.slug,
|
|
65
|
+
submodules: Array.isArray(data.submodules)
|
|
66
|
+
? data.submodules.map(normalizeSubmodule)
|
|
67
|
+
: [],
|
|
68
|
+
edges: Array.isArray(data.edges) ? data.edges : [],
|
|
69
|
+
};
|
|
70
|
+
if (data.action !== undefined) feature.action = data.action;
|
|
71
|
+
overlay.features[data.slug] = feature;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Load _removed.yaml via js-yaml
|
|
78
|
+
const removedFile = path.join(overlayDir, '_removed.yaml');
|
|
79
|
+
if (fs.existsSync(removedFile)) {
|
|
80
|
+
const raw = fs.readFileSync(removedFile, 'utf8');
|
|
81
|
+
if (raw.trim()) {
|
|
82
|
+
const removed = yaml.load(raw) as any;
|
|
83
|
+
if (removed && typeof removed === 'object' && !Array.isArray(removed)) {
|
|
84
|
+
if (Array.isArray(removed.features)) overlay.removed.features = removed.features;
|
|
85
|
+
if (Array.isArray(removed.submodules)) overlay.removed.submodules = removed.submodules;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return overlay;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeSubmodule(sub: any): any {
|
|
94
|
+
if (!sub || typeof sub !== 'object') return sub;
|
|
95
|
+
return {
|
|
96
|
+
slug: sub.slug,
|
|
97
|
+
kind: sub.kind || 'service',
|
|
98
|
+
role: sub.role || '',
|
|
99
|
+
functions: Array.isArray(sub.functions) ? sub.functions : [],
|
|
100
|
+
variables: Array.isArray(sub.variables) ? sub.variables : [],
|
|
101
|
+
...(sub.action !== undefined ? { action: sub.action } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function handleVerify(
|
|
106
|
+
projectRoot: string,
|
|
107
|
+
specDir: string,
|
|
108
|
+
options: VerifyOptions = {},
|
|
109
|
+
): Promise<number> {
|
|
110
|
+
const resolvedSpecDir = path.resolve(specDir);
|
|
111
|
+
|
|
112
|
+
let overlay: any;
|
|
113
|
+
try {
|
|
114
|
+
overlay = loadOverlay(resolvedSpecDir);
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
process.stderr.write(`Error loading overlay: ${err.message}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
121
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
|
|
122
|
+
const report = await verifyOverlay(cg, overlay);
|
|
123
|
+
closeIndex(cg);
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
process.stdout.write(formatOutput(report, { json: true }) + '\n');
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(`\n=== Verify Report ===\n\n`);
|
|
129
|
+
process.stdout.write(`Total: ${report.total}\n`);
|
|
130
|
+
process.stdout.write(`Passed: ${report.passed}\n`);
|
|
131
|
+
process.stdout.write(`Failed: ${report.failed.length}\n`);
|
|
132
|
+
process.stdout.write(`Skipped: ${report.skipped}\n`);
|
|
133
|
+
|
|
134
|
+
if (report.failed.length > 0) {
|
|
135
|
+
process.stdout.write('\nFailures:\n');
|
|
136
|
+
for (const f of report.failed) {
|
|
137
|
+
process.stdout.write(` [${f.type}] ${f.location}\n`);
|
|
138
|
+
if (f.suggestion) process.stdout.write(` Suggestion: ${f.suggestion}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
process.stdout.write('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return report.failed.length > 0 ? 1 : 0;
|
|
145
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { formatApiList } from './formatter.js';
|
|
4
|
+
|
|
5
|
+
describe('formatApiList', () => {
|
|
6
|
+
it('should show caller names in output when callers exist', () => {
|
|
7
|
+
const apis = [
|
|
8
|
+
{
|
|
9
|
+
name: 'myFunc',
|
|
10
|
+
kind: 'function',
|
|
11
|
+
filePath: 'src/a.ts',
|
|
12
|
+
startLine: 10,
|
|
13
|
+
callerCount: 3,
|
|
14
|
+
callers: [
|
|
15
|
+
{ name: 'callerOne', filePath: 'src/b.ts', startLine: 5 },
|
|
16
|
+
{ name: 'callerTwo', filePath: 'src/c.ts', startLine: 15 },
|
|
17
|
+
{ name: 'callerThree', filePath: 'src/d.ts', startLine: 25 },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const output = formatApiList(apis);
|
|
23
|
+
|
|
24
|
+
assert.ok(output.includes('callerOne'), 'output should contain callerOne');
|
|
25
|
+
assert.ok(output.includes('callerTwo'), 'output should contain callerTwo');
|
|
26
|
+
assert.ok(output.includes('callerThree'), 'output should contain callerThree');
|
|
27
|
+
assert.ok(output.includes('(3 callers)'), 'output should show caller count');
|
|
28
|
+
assert.ok(output.includes('myFunc'), 'output should contain function name');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should show "(0 callers)" when there are no callers', () => {
|
|
32
|
+
const apis = [
|
|
33
|
+
{
|
|
34
|
+
name: 'noCallers',
|
|
35
|
+
kind: 'function',
|
|
36
|
+
filePath: 'src/a.ts',
|
|
37
|
+
startLine: 1,
|
|
38
|
+
callerCount: 0,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const output = formatApiList(apis);
|
|
43
|
+
|
|
44
|
+
assert.ok(output.includes('(0 callers)'), 'output should show zero callers');
|
|
45
|
+
assert.ok(!output.includes('Called by:'), 'output should not contain caller lines');
|
|
46
|
+
});
|
|
47
|
+
});
|