@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.
- package/AGENTS.md +37 -27
- package/CHANGELOG.md +47 -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 +549 -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 +613 -5
- package/packages/tools/codegraph/dist/index.d.ts +3 -0
- package/packages/tools/codegraph/dist/index.js +343 -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 +362 -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/scripts/test.sh +39 -0
- package/skills/design/SKILL.md +33 -0
- package/skills/init-project-html/SKILL.md +66 -56
- package/skills/init-project-html/lib/atlas/assets/architecture.css +2 -1
- package/skills/init-project-html/lib/atlas/render.js +11 -1
- package/skills/init-project-html/lib/atlas/schema.js +44 -7
- package/skills/init-project-html/references/TEMPLATE_SPEC.md +20 -0
- package/skills/init-project-html/references/architecture.md +35 -35
- package/skills/init-project-html/references/definition.md +12 -17
- package/skills/update-project-html/README.md +16 -27
- package/skills/update-project-html/SKILL.md +54 -41
- package/skills/update-project-html/references/architecture.md +35 -35
- package/skills/update-project-html/references/definition.md +12 -17
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
describe('REGTEST-6: handleStatus — Languages section', () => {
|
|
6
|
+
it('should display filesByLanguage entries in human-readable output', async () => {
|
|
7
|
+
const testStats = {
|
|
8
|
+
fileCount: 100,
|
|
9
|
+
nodeCount: 500,
|
|
10
|
+
edgeCount: 200,
|
|
11
|
+
dbSizeBytes: 1_024_000,
|
|
12
|
+
lastUpdated: new Date('2026-06-01T12:00:00Z').toISOString(),
|
|
13
|
+
nodesByKind: { function: 50, class: 10 },
|
|
14
|
+
edgesByKind: { calls: 30, extends: 5 },
|
|
15
|
+
filesByLanguage: { typescript: 10, javascript: 5 },
|
|
16
|
+
};
|
|
17
|
+
const mockCg = {
|
|
18
|
+
getStats: () => testStats,
|
|
19
|
+
close: () => { },
|
|
20
|
+
};
|
|
21
|
+
// Load the same CodeGraph module that cmd-status.js uses (shared CJS cache),
|
|
22
|
+
// then mock CodeGraph.open + isInitialized so handleStatus uses our fake instance.
|
|
23
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
24
|
+
const openMock = mock.method(CodeGraph, 'open', async () => mockCg);
|
|
25
|
+
const initMock = mock.method(CodeGraph, 'isInitialized', () => true);
|
|
26
|
+
// Import the module under test AFTER the mock is in place
|
|
27
|
+
const { handleStatus } = await import('./cmd-status.js');
|
|
28
|
+
// Capture stdout writes
|
|
29
|
+
const chunks = [];
|
|
30
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
31
|
+
process.stdout.write = (chunk) => {
|
|
32
|
+
chunks.push(String(chunk));
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const exitCode = await handleStatus('/test/project', {});
|
|
37
|
+
assert.strictEqual(exitCode, 0);
|
|
38
|
+
const output = chunks.join('');
|
|
39
|
+
assert.ok(output.includes('Languages:'), 'Output should contain "Languages:" section header');
|
|
40
|
+
assert.ok(output.includes('typescript'), 'Output should contain "typescript" language name');
|
|
41
|
+
assert.ok(output.includes('javascript'), 'Output should contain "javascript" language name');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
process.stdout.write = originalWrite;
|
|
45
|
+
openMock.mock.restore();
|
|
46
|
+
initMock.mock.restore();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('REGTEST-R2-02: should return exit code 1 when CodeGraph is not initialized', async () => {
|
|
50
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
51
|
+
const initMock = mock.method(CodeGraph, 'isInitialized', () => false);
|
|
52
|
+
// Import fresh (module cache returns same CodeGraph reference with mocked isInitialized)
|
|
53
|
+
const { handleStatus } = await import('./cmd-status.js');
|
|
54
|
+
// Capture stderr writes
|
|
55
|
+
const stderrChunks = [];
|
|
56
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
57
|
+
process.stderr.write = (chunk) => {
|
|
58
|
+
stderrChunks.push(String(chunk));
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
const exitCode = await handleStatus('/test/project', {});
|
|
63
|
+
assert.strictEqual(exitCode, 1);
|
|
64
|
+
const stderrOutput = stderrChunks.join('');
|
|
65
|
+
assert.ok(stderrOutput.includes('CodeGraph is not initialized'), 'stderr should contain "CodeGraph is not initialized" message');
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
process.stderr.write = originalStderrWrite;
|
|
69
|
+
initMock.mock.restore();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface SurveyOptions {
|
|
2
|
+
feature?: string;
|
|
3
|
+
json?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface SurveyReport {
|
|
6
|
+
directory: string;
|
|
7
|
+
feature?: string;
|
|
8
|
+
totalFiles: number;
|
|
9
|
+
totalSymbols: number;
|
|
10
|
+
files: Array<{
|
|
11
|
+
filePath: string;
|
|
12
|
+
language: string;
|
|
13
|
+
symbolCount: number;
|
|
14
|
+
}>;
|
|
15
|
+
entryPoints: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
startLine: number;
|
|
20
|
+
isExported: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
suggestedSubmodules: Array<{
|
|
23
|
+
slug: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
role: string;
|
|
26
|
+
memberFunctions: string[];
|
|
27
|
+
memberFiles: string[];
|
|
28
|
+
}>;
|
|
29
|
+
suggestedEdges: Array<{
|
|
30
|
+
source: string;
|
|
31
|
+
target: string;
|
|
32
|
+
kind: string;
|
|
33
|
+
label: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function handleSurvey(projectRoot: string, dirPath: string, options?: SurveyOptions): Promise<number>;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
export async function handleSurvey(projectRoot, dirPath, options = {}) {
|
|
10
|
+
// Check that the target directory exists
|
|
11
|
+
const targetPath = path.resolve(projectRoot, dirPath);
|
|
12
|
+
if (!existsSync(targetPath)) {
|
|
13
|
+
process.stderr.write(`Error: Directory not found: ${dirPath}\n`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
17
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
|
|
18
|
+
// Scan the directory
|
|
19
|
+
const scan = await scanDirectory(cg, dirPath);
|
|
20
|
+
const fileSet = new Set(scan.files.map((f) => f.filePath));
|
|
21
|
+
// Group into submodule suggestions
|
|
22
|
+
const suggestions = groupIntoSubmodules(scan, cg);
|
|
23
|
+
// Build edge suggestions from cross-file call relationships
|
|
24
|
+
const edgeSuggestions = buildEdgeSuggestions(scan, cg, fileSet);
|
|
25
|
+
// Determine entry points: exported symbols called from outside the scanned directory
|
|
26
|
+
const entryPoints = scan.allSymbols.filter(s => {
|
|
27
|
+
if (!s.isExported)
|
|
28
|
+
return false;
|
|
29
|
+
const nodes = cg.getNodesByName(s.name);
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
if (node.filePath !== s.filePath)
|
|
32
|
+
continue;
|
|
33
|
+
const callers = cg.getCallers(node.id);
|
|
34
|
+
for (const caller of callers) {
|
|
35
|
+
if (!fileSet.has(caller.node.filePath)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
closeIndex(cg);
|
|
43
|
+
const report = {
|
|
44
|
+
directory: dirPath,
|
|
45
|
+
feature: options.feature,
|
|
46
|
+
totalFiles: scan.totalFiles,
|
|
47
|
+
totalSymbols: scan.totalSymbols,
|
|
48
|
+
files: scan.files.map((f) => ({
|
|
49
|
+
filePath: f.filePath,
|
|
50
|
+
language: f.language,
|
|
51
|
+
symbolCount: f.symbols.length,
|
|
52
|
+
})),
|
|
53
|
+
entryPoints,
|
|
54
|
+
suggestedSubmodules: suggestions.map((s) => ({
|
|
55
|
+
slug: s.slug,
|
|
56
|
+
kind: s.kind,
|
|
57
|
+
role: s.role,
|
|
58
|
+
memberFunctions: s.memberFunctions,
|
|
59
|
+
memberFiles: s.memberFiles,
|
|
60
|
+
})),
|
|
61
|
+
suggestedEdges: edgeSuggestions,
|
|
62
|
+
};
|
|
63
|
+
if (options.json) {
|
|
64
|
+
process.stdout.write(formatOutput(report, { json: true }) + '\n');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Human-readable output
|
|
68
|
+
process.stdout.write(`\n=== Survey: ${dirPath} ===\n`);
|
|
69
|
+
if (report.feature) {
|
|
70
|
+
process.stdout.write(`Feature: ${report.feature}\n`);
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write('\n');
|
|
73
|
+
process.stdout.write(`Files: ${report.totalFiles} Symbols: ${report.totalSymbols}\n\n`);
|
|
74
|
+
process.stdout.write('Files:\n');
|
|
75
|
+
for (const f of report.files) {
|
|
76
|
+
process.stdout.write(` ${f.filePath} [${f.language}] (${f.symbolCount} symbols)\n`);
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write('\nEntry Points:\n');
|
|
79
|
+
if (report.entryPoints.length === 0) {
|
|
80
|
+
process.stdout.write(' (none)\n');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
for (const ep of report.entryPoints) {
|
|
84
|
+
process.stdout.write(` ${ep.name} [${ep.kind}] ${ep.filePath}:${ep.startLine}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write('\nSuggested Submodules:\n');
|
|
88
|
+
if (report.suggestedSubmodules.length === 0) {
|
|
89
|
+
process.stdout.write(' (none)\n');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const sub of report.suggestedSubmodules) {
|
|
93
|
+
process.stdout.write(` ${sub.slug} [${sub.kind}] ${sub.role}\n`);
|
|
94
|
+
process.stdout.write(` Functions: ${sub.memberFunctions.join(', ')}\n`);
|
|
95
|
+
process.stdout.write(` Files: ${sub.memberFiles.join(', ')}\n\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write('Suggested Edges:\n');
|
|
99
|
+
if (report.suggestedEdges.length === 0) {
|
|
100
|
+
process.stdout.write(' (none)\n');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
for (const edge of report.suggestedEdges) {
|
|
104
|
+
process.stdout.write(` ${edge.source} --[${edge.kind}]--> ${edge.target} (${edge.label})\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write('\n');
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build suggested edges from cross-file call relationships in the scanned directory.
|
|
113
|
+
*/
|
|
114
|
+
function buildEdgeSuggestions(scan, cg, fileSet) {
|
|
115
|
+
const edges = [];
|
|
116
|
+
const dedup = new Set();
|
|
117
|
+
// For each symbol in the scan, check if it calls symbols outside the scanned directory
|
|
118
|
+
for (const sym of scan.allSymbols) {
|
|
119
|
+
const nodes = cg.getNodesByName(sym.name);
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
if (node.filePath !== sym.filePath)
|
|
122
|
+
continue;
|
|
123
|
+
const callees = cg.getCallees(node.id);
|
|
124
|
+
for (const callee of callees) {
|
|
125
|
+
// Only consider callees OUTSIDE the scanned directory (cross-boundary edges)
|
|
126
|
+
if (!fileSet.has(callee.node.filePath)) {
|
|
127
|
+
const edgeKey = `${sym.name}::${callee.node.name}`;
|
|
128
|
+
if (dedup.has(edgeKey))
|
|
129
|
+
continue;
|
|
130
|
+
dedup.add(edgeKey);
|
|
131
|
+
edges.push({
|
|
132
|
+
source: sym.name,
|
|
133
|
+
target: callee.node.name,
|
|
134
|
+
kind: 'call',
|
|
135
|
+
label: `${sym.filePath} -> ${callee.node.filePath}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return edges;
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, before, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
describe('cmd-survey', () => {
|
|
6
|
+
let handleSurvey;
|
|
7
|
+
// Mock CodeGraph instance shared across tests:
|
|
8
|
+
// - Symbol A in src/feature/a.ts calls symbol B in src/lib/b.ts (cross-boundary)
|
|
9
|
+
// - Symbol X in src/feature/a.ts is exported but only called internally
|
|
10
|
+
const mockCg = {
|
|
11
|
+
getFiles: () => [{ path: 'src/feature/a.ts', language: 'typescript' }],
|
|
12
|
+
getNodesInFile: () => [
|
|
13
|
+
{
|
|
14
|
+
id: 'node-a',
|
|
15
|
+
name: 'A',
|
|
16
|
+
kind: 'function',
|
|
17
|
+
qualifiedName: 'A',
|
|
18
|
+
startLine: 1,
|
|
19
|
+
endLine: 10,
|
|
20
|
+
isExported: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'node-x',
|
|
24
|
+
name: 'X',
|
|
25
|
+
kind: 'function',
|
|
26
|
+
qualifiedName: 'X',
|
|
27
|
+
startLine: 20,
|
|
28
|
+
endLine: 30,
|
|
29
|
+
isExported: true,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
getNodesByName: (name) => {
|
|
33
|
+
if (name === 'A') {
|
|
34
|
+
return [{ id: 'node-a', filePath: 'src/feature/a.ts', name: 'A' }];
|
|
35
|
+
}
|
|
36
|
+
if (name === 'X') {
|
|
37
|
+
return [{ id: 'node-x', filePath: 'src/feature/a.ts', name: 'X' }];
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
getCallees: (_id) => {
|
|
42
|
+
if (_id === 'node-a') {
|
|
43
|
+
return [{ node: { name: 'B', filePath: 'src/lib/b.ts' } }];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
},
|
|
47
|
+
getCallers: (_id) => {
|
|
48
|
+
if (_id === 'node-x') {
|
|
49
|
+
return [{ node: { name: 'internal', filePath: 'src/feature/a.ts' } }];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
},
|
|
53
|
+
close: () => { },
|
|
54
|
+
};
|
|
55
|
+
before(async () => {
|
|
56
|
+
// Module-level mocks (only need to be set up once):
|
|
57
|
+
mock.module('./survey/scanner.js', {
|
|
58
|
+
namedExports: {
|
|
59
|
+
scanDirectory: async () => ({
|
|
60
|
+
directory: 'src/feature',
|
|
61
|
+
files: [
|
|
62
|
+
{
|
|
63
|
+
filePath: 'src/feature/a.ts',
|
|
64
|
+
language: 'typescript',
|
|
65
|
+
symbols: [
|
|
66
|
+
{ name: 'A', kind: 'function', qualifiedName: 'A', startLine: 1, endLine: 10, isExported: true, signature: undefined },
|
|
67
|
+
{ name: 'X', kind: 'function', qualifiedName: 'X', startLine: 20, endLine: 30, isExported: true, signature: undefined },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
allSymbols: [
|
|
72
|
+
{ name: 'A', kind: 'function', filePath: 'src/feature/a.ts', qualifiedName: 'A', startLine: 1, isExported: true },
|
|
73
|
+
{ name: 'X', kind: 'function', filePath: 'src/feature/a.ts', qualifiedName: 'X', startLine: 20, isExported: true },
|
|
74
|
+
],
|
|
75
|
+
totalFiles: 1,
|
|
76
|
+
totalSymbols: 2,
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
mock.module('./survey/grouper.js', {
|
|
81
|
+
namedExports: { groupIntoSubmodules: () => [] },
|
|
82
|
+
});
|
|
83
|
+
mock.module('./cg-instance.js', {
|
|
84
|
+
namedExports: { closeIndex: () => { } },
|
|
85
|
+
});
|
|
86
|
+
// Import the compiled module (dist/lib/cmd-survey.js has the fixed code)
|
|
87
|
+
const mod = await import('./cmd-survey.js');
|
|
88
|
+
handleSurvey = mod.handleSurvey;
|
|
89
|
+
});
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
// Re-apply CodeGraph.open mock before each test (mock.reset() in afterEach clears it).
|
|
92
|
+
// Must use manual mock.method instead of mock.module because lazy CJS require()
|
|
93
|
+
// inside handlers bypasses mock.module interception.
|
|
94
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
95
|
+
mock.method(CodeGraph, 'open', async () => mockCg);
|
|
96
|
+
});
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
mock.reset();
|
|
99
|
+
});
|
|
100
|
+
it('REGTEST-10: Cross-boundary edges point outside the scanned directory', async () => {
|
|
101
|
+
const stdoutChunks = [];
|
|
102
|
+
mock.method(process.stdout, 'write', (chunk) => {
|
|
103
|
+
stdoutChunks.push(String(chunk));
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
// Use a real directory for projectRoot so existsSync returns true naturally
|
|
107
|
+
const exitCode = await handleSurvey(process.cwd(), 'packages');
|
|
108
|
+
assert.equal(exitCode, 0);
|
|
109
|
+
const output = stdoutChunks.join('');
|
|
110
|
+
assert.ok(output.includes('src/feature/a.ts -> src/lib/b.ts'), `Expected cross-boundary edge label in output, got:\n${output}`);
|
|
111
|
+
});
|
|
112
|
+
it('REGTEST-11: Exported symbol called only from within the directory is not an entry point', async () => {
|
|
113
|
+
const stdoutChunks = [];
|
|
114
|
+
mock.method(process.stdout, 'write', (chunk) => {
|
|
115
|
+
stdoutChunks.push(String(chunk));
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
// Use a real directory for projectRoot so existsSync returns true naturally
|
|
119
|
+
const exitCode = await handleSurvey(process.cwd(), 'packages');
|
|
120
|
+
assert.equal(exitCode, 0);
|
|
121
|
+
const output = stdoutChunks.join('');
|
|
122
|
+
// X has only internal callers, so "(none)" should appear for entry points
|
|
123
|
+
assert.ok(output.includes('(none)'), `Expected "(none)" for entry points, got:\n${output}`);
|
|
124
|
+
});
|
|
125
|
+
it('REGTEST-12: Non-existent directory returns exit code 1 with error message', async () => {
|
|
126
|
+
const stderrChunks = [];
|
|
127
|
+
mock.method(process.stderr, 'write', (chunk) => {
|
|
128
|
+
stderrChunks.push(String(chunk));
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
// Use a path that does not exist on disk
|
|
132
|
+
const exitCode = await handleSurvey('/tmp', 'nonexistent-' + process.hrtime.bigint().toString());
|
|
133
|
+
assert.equal(exitCode, 1);
|
|
134
|
+
assert.ok(stderrChunks.join('').includes('Directory not found'), `Expected "Directory not found" error, got: ${stderrChunks.join('')}`);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
export async function handleSync(projectRoot, options = {}) {
|
|
6
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
7
|
+
if (!CodeGraph.isInitialized(projectRoot)) {
|
|
8
|
+
process.stderr.write('CodeGraph is not initialized. Run `apltk codegraph init` first.\n');
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: false });
|
|
12
|
+
let progressEvents = [];
|
|
13
|
+
const result = await cg.sync({
|
|
14
|
+
onProgress: (p) => {
|
|
15
|
+
progressEvents.push({ phase: p.phase, current: p.current, total: p.total });
|
|
16
|
+
if (process.stdout.isTTY) {
|
|
17
|
+
process.stdout.write(`\r Indexing: ${p.phase} ${p.current}/${p.total}`);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (process.stdout.isTTY) {
|
|
22
|
+
process.stdout.write('\n');
|
|
23
|
+
}
|
|
24
|
+
closeIndex(cg);
|
|
25
|
+
const output = {
|
|
26
|
+
projectRoot,
|
|
27
|
+
filesChecked: result.filesChecked,
|
|
28
|
+
filesAdded: result.filesAdded,
|
|
29
|
+
filesModified: result.filesModified,
|
|
30
|
+
filesRemoved: result.filesRemoved,
|
|
31
|
+
nodesUpdated: result.nodesUpdated,
|
|
32
|
+
durationMs: result.durationMs,
|
|
33
|
+
progress: progressEvents.length > 0 ? progressEvents : undefined,
|
|
34
|
+
};
|
|
35
|
+
if (options.json) {
|
|
36
|
+
process.stdout.write(formatOutput(output, { json: true }) + '\n');
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const summary = [
|
|
40
|
+
['Project:', projectRoot],
|
|
41
|
+
['Checked:', result.filesChecked],
|
|
42
|
+
['Added:', result.filesAdded],
|
|
43
|
+
['Modified:', result.filesModified],
|
|
44
|
+
['Removed:', result.filesRemoved],
|
|
45
|
+
['Nodes updated:', result.nodesUpdated],
|
|
46
|
+
['Duration:', `${result.durationMs}ms`],
|
|
47
|
+
];
|
|
48
|
+
process.stdout.write(formatSummary(summary) + '\n');
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
describe('cmd-sync handleSync', () => {
|
|
6
|
+
it('should return exit code 1 when CodeGraph is not initialized', async () => {
|
|
7
|
+
// Pre-load CodeGraph via CJS require (same mechanism used by cmd-sync.ts)
|
|
8
|
+
// and mock isInitialized BEFORE the module under test is evaluated, so
|
|
9
|
+
// the mock is in place when cmd-sync.ts runs its own require().
|
|
10
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
11
|
+
const isInitializedMock = mock.method(CodeGraph, 'isInitialized', () => false);
|
|
12
|
+
// Capture stderr output
|
|
13
|
+
const stderrWriteMock = mock.method(process.stderr, 'write', () => true);
|
|
14
|
+
// Import the module under test after mocks are set up
|
|
15
|
+
const { handleSync } = await import('./cmd-sync.js');
|
|
16
|
+
// Call the handler with an uninitialized project
|
|
17
|
+
const exitCode = await handleSync('/tmp/fake-project', { json: false });
|
|
18
|
+
// Verify exit code is 1 (error)
|
|
19
|
+
assert.strictEqual(exitCode, 1);
|
|
20
|
+
// Verify stderr mentions "init"
|
|
21
|
+
assert.strictEqual(stderrWriteMock.mock.calls.length, 1);
|
|
22
|
+
const callArg = stderrWriteMock.mock.calls[0].arguments[0];
|
|
23
|
+
assert.ok(callArg !== undefined, 'Expected stderr write argument to be defined');
|
|
24
|
+
const stderrOutput = typeof callArg === 'string' ? callArg : Buffer.from(callArg).toString('utf8');
|
|
25
|
+
assert.ok(stderrOutput.toLowerCase().includes('init'), `Expected stderr to mention "init", got: ${JSON.stringify(stderrOutput)}`);
|
|
26
|
+
// Cleanup mocks
|
|
27
|
+
isInitializedMock.mock.restore();
|
|
28
|
+
stderrWriteMock.mock.restore();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
* Read a spec overlay from the standard atlas directory layout.
|
|
11
|
+
* Loads the overlay in the same way as skills/init-project-html/lib/atlas/state.js::loadOverlay.
|
|
12
|
+
*/
|
|
13
|
+
function loadOverlay(specDir) {
|
|
14
|
+
const overlayDir = path.join(specDir, 'architecture_diff', 'atlas');
|
|
15
|
+
if (!fs.existsSync(overlayDir)) {
|
|
16
|
+
throw new Error(`No architecture diff atlas found at: ${overlayDir}. Run "apltk architecture diff" first to generate the overlay.`);
|
|
17
|
+
}
|
|
18
|
+
const overlay = {
|
|
19
|
+
meta: null,
|
|
20
|
+
actors: null,
|
|
21
|
+
edges: null,
|
|
22
|
+
featureOrder: null,
|
|
23
|
+
features: {},
|
|
24
|
+
removed: { features: [], submodules: [] },
|
|
25
|
+
};
|
|
26
|
+
// Parse atlas.index.yaml via js-yaml
|
|
27
|
+
const indexFile = path.join(overlayDir, 'atlas.index.yaml');
|
|
28
|
+
if (fs.existsSync(indexFile)) {
|
|
29
|
+
const raw = fs.readFileSync(indexFile, 'utf8');
|
|
30
|
+
if (raw.trim()) {
|
|
31
|
+
const index = yaml.load(raw);
|
|
32
|
+
if (index && typeof index === 'object' && !Array.isArray(index)) {
|
|
33
|
+
if (index.meta !== undefined)
|
|
34
|
+
overlay.meta = index.meta;
|
|
35
|
+
if (index.actors !== undefined)
|
|
36
|
+
overlay.actors = index.actors;
|
|
37
|
+
if (index.edges !== undefined)
|
|
38
|
+
overlay.edges = index.edges;
|
|
39
|
+
if (Array.isArray(index.features)) {
|
|
40
|
+
overlay.featureOrder = index.features
|
|
41
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry?.slug))
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Load feature files via js-yaml
|
|
48
|
+
const featuresDir = path.join(overlayDir, 'features');
|
|
49
|
+
if (fs.existsSync(featuresDir)) {
|
|
50
|
+
for (const entry of fs.readdirSync(featuresDir)) {
|
|
51
|
+
if (!entry.endsWith('.yaml'))
|
|
52
|
+
continue;
|
|
53
|
+
const featureFile = path.join(featuresDir, entry);
|
|
54
|
+
const raw = fs.readFileSync(featureFile, 'utf8');
|
|
55
|
+
if (raw.trim()) {
|
|
56
|
+
const data = yaml.load(raw);
|
|
57
|
+
if (data && typeof data === 'object' && data.slug) {
|
|
58
|
+
const feature = {
|
|
59
|
+
slug: data.slug,
|
|
60
|
+
submodules: Array.isArray(data.submodules)
|
|
61
|
+
? data.submodules.map(normalizeSubmodule)
|
|
62
|
+
: [],
|
|
63
|
+
edges: Array.isArray(data.edges) ? data.edges : [],
|
|
64
|
+
};
|
|
65
|
+
if (data.action !== undefined)
|
|
66
|
+
feature.action = data.action;
|
|
67
|
+
overlay.features[data.slug] = feature;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Load _removed.yaml via js-yaml
|
|
73
|
+
const removedFile = path.join(overlayDir, '_removed.yaml');
|
|
74
|
+
if (fs.existsSync(removedFile)) {
|
|
75
|
+
const raw = fs.readFileSync(removedFile, 'utf8');
|
|
76
|
+
if (raw.trim()) {
|
|
77
|
+
const removed = yaml.load(raw);
|
|
78
|
+
if (removed && typeof removed === 'object' && !Array.isArray(removed)) {
|
|
79
|
+
if (Array.isArray(removed.features))
|
|
80
|
+
overlay.removed.features = removed.features;
|
|
81
|
+
if (Array.isArray(removed.submodules))
|
|
82
|
+
overlay.removed.submodules = removed.submodules;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return overlay;
|
|
87
|
+
}
|
|
88
|
+
function normalizeSubmodule(sub) {
|
|
89
|
+
if (!sub || typeof sub !== 'object')
|
|
90
|
+
return sub;
|
|
91
|
+
return {
|
|
92
|
+
slug: sub.slug,
|
|
93
|
+
kind: sub.kind || 'service',
|
|
94
|
+
role: sub.role || '',
|
|
95
|
+
functions: Array.isArray(sub.functions) ? sub.functions : [],
|
|
96
|
+
variables: Array.isArray(sub.variables) ? sub.variables : [],
|
|
97
|
+
...(sub.action !== undefined ? { action: sub.action } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function handleVerify(projectRoot, specDir, options = {}) {
|
|
101
|
+
const resolvedSpecDir = path.resolve(specDir);
|
|
102
|
+
let overlay;
|
|
103
|
+
try {
|
|
104
|
+
overlay = loadOverlay(resolvedSpecDir);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
process.stderr.write(`Error loading overlay: ${err.message}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const { CodeGraph } = require('@colbymchenry/codegraph');
|
|
111
|
+
const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
|
|
112
|
+
const report = await verifyOverlay(cg, overlay);
|
|
113
|
+
closeIndex(cg);
|
|
114
|
+
if (options.json) {
|
|
115
|
+
process.stdout.write(formatOutput(report, { json: true }) + '\n');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
process.stdout.write(`\n=== Verify Report ===\n\n`);
|
|
119
|
+
process.stdout.write(`Total: ${report.total}\n`);
|
|
120
|
+
process.stdout.write(`Passed: ${report.passed}\n`);
|
|
121
|
+
process.stdout.write(`Failed: ${report.failed.length}\n`);
|
|
122
|
+
process.stdout.write(`Skipped: ${report.skipped}\n`);
|
|
123
|
+
if (report.failed.length > 0) {
|
|
124
|
+
process.stdout.write('\nFailures:\n');
|
|
125
|
+
for (const f of report.failed) {
|
|
126
|
+
process.stdout.write(` [${f.type}] ${f.location}\n`);
|
|
127
|
+
if (f.suggestion)
|
|
128
|
+
process.stdout.write(` Suggestion: ${f.suggestion}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
process.stdout.write('\n');
|
|
132
|
+
}
|
|
133
|
+
return report.failed.length > 0 ? 1 : 0;
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|