memorix 1.8.6 → 1.8.7
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/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli/index.js +2208 -1160
- package/dist/dashboard/static/app.js +51 -3
- package/dist/dashboard/static/style.css +88 -0
- package/dist/index.js +1726 -736
- package/dist/maintenance-runner.js +855 -218
- package/dist/media-audio-runner.js +856 -219
- package/dist/media-video-runner.js +854 -217
- package/dist/memcode-runtime/CHANGELOG.md +33 -0
- package/dist/sdk.js +1716 -726
- package/dist/vector-backfill-runner.js +853 -216
- package/docs/1.8.7-CODEGRAPH.md +166 -0
- package/docs/API_REFERENCE.md +23 -2
- package/docs/ARCHITECTURE.md +13 -0
- package/docs/README.md +4 -1
- package/package.json +3 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/codegraph.ts +25 -11
- package/src/cli/commands/doctor.ts +11 -2
- package/src/cli/commands/serve-http.ts +27 -1
- package/src/codegraph/auto-context.ts +19 -10
- package/src/codegraph/context-pack.ts +20 -6
- package/src/codegraph/dashboard.ts +82 -0
- package/src/codegraph/external-provider.ts +10 -1
- package/src/codegraph/freshness.ts +1 -1
- package/src/codegraph/lite-provider.ts +81 -0
- package/src/codegraph/project-context.ts +18 -5
- package/src/codegraph/scip-provider.ts +181 -0
- package/src/codegraph/semantic-context.ts +139 -0
- package/src/codegraph/semantic-provider.ts +404 -0
- package/src/codegraph/store.ts +253 -18
- package/src/codegraph/types.ts +25 -3
- package/src/dashboard/server.ts +6 -0
- package/src/dashboard/static/style.css +88 -0
- package/src/knowledge/workset.ts +46 -44
- package/src/server/mcp-binding-store.ts +17 -0
- package/src/store/sqlite-db.ts +13 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { CodeGraphOutline, CodeGraphProviderQuality, CodeGraphStatus, CodeSymbol } from './types.js';
|
|
2
|
+
import type { CodeGraphStore } from './store.js';
|
|
3
|
+
|
|
4
|
+
interface SemanticContextInput {
|
|
5
|
+
store: CodeGraphStore;
|
|
6
|
+
projectId: string;
|
|
7
|
+
task: string;
|
|
8
|
+
preferredPaths?: string[];
|
|
9
|
+
maxNodes?: number;
|
|
10
|
+
maxEdges?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function tokens(text: string): string[] {
|
|
14
|
+
return [...new Set((text.toLowerCase().match(/[a-z0-9_$./-]+|[\u4e00-\u9fff]+/g) ?? [])
|
|
15
|
+
.filter(token => token.length > 1))];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function scoreSymbol(symbol: CodeSymbol, taskTokens: string[], preferred: Set<string>): number {
|
|
19
|
+
const name = `${symbol.name} ${symbol.qualifiedName} ${symbol.path}`.toLowerCase();
|
|
20
|
+
let score = preferred.has(symbol.path) ? 100 : 0;
|
|
21
|
+
for (const token of taskTokens) {
|
|
22
|
+
if (symbol.name.toLowerCase() === token) score += 20;
|
|
23
|
+
else if (name.includes(token)) score += 6;
|
|
24
|
+
}
|
|
25
|
+
if (score > 0 && (symbol.kind === 'function' || symbol.kind === 'method')) score += 1;
|
|
26
|
+
return score;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Build a small internal semantic outline from the persisted compiler graph. */
|
|
30
|
+
export function buildSemanticContext(input: SemanticContextInput): CodeGraphOutline | undefined {
|
|
31
|
+
const maxNodes = Math.max(2, Math.min(16, Math.floor(input.maxNodes ?? 8)));
|
|
32
|
+
const maxEdges = Math.max(1, Math.min(24, Math.floor(input.maxEdges ?? 12)));
|
|
33
|
+
const taskTokens = tokens(input.task);
|
|
34
|
+
const preferred = new Set((input.preferredPaths ?? []).map(path => path.replace(/\\/g, '/')));
|
|
35
|
+
const symbols = input.store.listSymbols(input.projectId)
|
|
36
|
+
.filter(symbol => symbol.source === 'typescript-compiler' && !symbol.stale)
|
|
37
|
+
.map((symbol, index) => ({ symbol, score: scoreSymbol(symbol, taskTokens, preferred), index }))
|
|
38
|
+
.filter(item => item.score > 0)
|
|
39
|
+
.sort((a, b) => b.score - a.score || a.symbol.path.localeCompare(b.symbol.path) || a.symbol.startLine! - b.symbol.startLine! || a.index - b.index)
|
|
40
|
+
.slice(0, maxNodes)
|
|
41
|
+
.map(item => item.symbol);
|
|
42
|
+
if (symbols.length === 0) return undefined;
|
|
43
|
+
|
|
44
|
+
const byId = new Map(symbols.map(symbol => [symbol.id, symbol]));
|
|
45
|
+
const edges = input.store.listEdges(input.projectId)
|
|
46
|
+
.filter(edge => edge.source === 'typescript-compiler')
|
|
47
|
+
.filter(edge => (edge.fromSymbolId && byId.has(edge.fromSymbolId)) || (edge.toSymbolId && byId.has(edge.toSymbolId)))
|
|
48
|
+
.slice(0, maxEdges);
|
|
49
|
+
const endpointIds = new Set<string>();
|
|
50
|
+
for (const edge of edges) {
|
|
51
|
+
if (edge.fromSymbolId) endpointIds.add(edge.fromSymbolId);
|
|
52
|
+
if (edge.toSymbolId) endpointIds.add(edge.toSymbolId);
|
|
53
|
+
}
|
|
54
|
+
const endpointSymbols = input.store.listSymbols(input.projectId)
|
|
55
|
+
.filter(symbol => symbol.source === 'typescript-compiler' && endpointIds.has(symbol.id));
|
|
56
|
+
const allSymbols = [...symbols, ...endpointSymbols]
|
|
57
|
+
.filter((symbol, index, items) => items.findIndex(item => item.id === symbol.id) === index)
|
|
58
|
+
.slice(0, maxNodes + maxEdges);
|
|
59
|
+
const allById = new Map(allSymbols.map(symbol => [symbol.id, symbol]));
|
|
60
|
+
const relations = edges.flatMap(edge => {
|
|
61
|
+
const from = edge.fromSymbolId ? allById.get(edge.fromSymbolId) : undefined;
|
|
62
|
+
const to = edge.toSymbolId ? allById.get(edge.toSymbolId) : undefined;
|
|
63
|
+
if (!from || !to) return [];
|
|
64
|
+
return [{
|
|
65
|
+
from: {
|
|
66
|
+
id: from.id,
|
|
67
|
+
name: from.name,
|
|
68
|
+
qualifiedName: from.qualifiedName,
|
|
69
|
+
kind: from.kind,
|
|
70
|
+
path: from.path,
|
|
71
|
+
...(from.startLine ? { startLine: from.startLine } : {}),
|
|
72
|
+
...(from.endLine ? { endLine: from.endLine } : {}),
|
|
73
|
+
language: 'typescript',
|
|
74
|
+
},
|
|
75
|
+
to: {
|
|
76
|
+
id: to.id,
|
|
77
|
+
name: to.name,
|
|
78
|
+
qualifiedName: to.qualifiedName,
|
|
79
|
+
kind: to.kind,
|
|
80
|
+
path: to.path,
|
|
81
|
+
...(to.startLine ? { startLine: to.startLine } : {}),
|
|
82
|
+
...(to.endLine ? { endLine: to.endLine } : {}),
|
|
83
|
+
language: 'typescript',
|
|
84
|
+
},
|
|
85
|
+
kind: edge.type,
|
|
86
|
+
...(edge.evidence?.match(/line (\d+)/)?.[1] ? { line: Number(edge.evidence.match(/line (\d+)/)![1]) } : {}),
|
|
87
|
+
}];
|
|
88
|
+
});
|
|
89
|
+
const entryPoints = symbols.slice(0, Math.min(5, symbols.length)).map(symbol => ({
|
|
90
|
+
id: symbol.id,
|
|
91
|
+
name: symbol.name,
|
|
92
|
+
qualifiedName: symbol.qualifiedName,
|
|
93
|
+
kind: symbol.kind,
|
|
94
|
+
path: symbol.path,
|
|
95
|
+
...(symbol.startLine ? { startLine: symbol.startLine } : {}),
|
|
96
|
+
...(symbol.endLine ? { endLine: symbol.endLine } : {}),
|
|
97
|
+
language: 'typescript',
|
|
98
|
+
}));
|
|
99
|
+
const files = input.store.listFiles(input.projectId);
|
|
100
|
+
const fileById = new Map(files.map(file => [file.id, file.path]));
|
|
101
|
+
const relatedFiles = [...new Set([
|
|
102
|
+
...allSymbols.map(symbol => symbol.path),
|
|
103
|
+
...edges.flatMap(edge => [
|
|
104
|
+
edge.fromFileId ? fileById.get(edge.fromFileId) : undefined,
|
|
105
|
+
edge.toFileId ? fileById.get(edge.toFileId) : undefined,
|
|
106
|
+
]),
|
|
107
|
+
].filter((path): path is string => Boolean(path)))].slice(0, 8);
|
|
108
|
+
return {
|
|
109
|
+
provider: 'semantic',
|
|
110
|
+
entryPoints,
|
|
111
|
+
relations,
|
|
112
|
+
relatedFiles,
|
|
113
|
+
stats: {
|
|
114
|
+
nodes: allSymbols.length,
|
|
115
|
+
edges: relations.length,
|
|
116
|
+
files: relatedFiles.length,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function qualityWithPersistedSemantic(
|
|
122
|
+
quality: CodeGraphProviderQuality,
|
|
123
|
+
status: Pick<CodeGraphStatus, 'semanticSymbols' | 'semanticEdges' | 'parserErrors'>,
|
|
124
|
+
): CodeGraphProviderQuality {
|
|
125
|
+
const hasSemantic = status.semanticSymbols > 0 || status.semanticEdges > 0;
|
|
126
|
+
const semantic = {
|
|
127
|
+
state: status.parserErrors > 0 ? 'partial' as const : !hasSemantic ? 'not-indexed' as const : 'ready' as const,
|
|
128
|
+
symbols: status.semanticSymbols,
|
|
129
|
+
edges: status.semanticEdges,
|
|
130
|
+
parserErrors: status.parserErrors,
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
...quality,
|
|
134
|
+
semantic,
|
|
135
|
+
...(hasSemantic && quality.selected !== 'external'
|
|
136
|
+
? { selected: 'semantic' as const, selectedQuality: 'semantic' as const }
|
|
137
|
+
: {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import type { CodeEdge, CodeFile, CodeSymbol } from './types.js';
|
|
6
|
+
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
7
|
+
import { makeCodeEdgeId, makeCodeFileId, makeCodeSymbolId, normalizeCodePath } from './ids.js';
|
|
8
|
+
|
|
9
|
+
const SEMANTIC_SOURCE = 'typescript-compiler' as const;
|
|
10
|
+
const SUPPORTED_LANGUAGES = new Set(['typescript', 'javascript']);
|
|
11
|
+
|
|
12
|
+
export interface SemanticIndexOptions {
|
|
13
|
+
projectId: string;
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
files: CodeFile[];
|
|
16
|
+
exclude?: string[];
|
|
17
|
+
/** Paths whose symbols/edges should be emitted; all files remain available to the checker. */
|
|
18
|
+
includePaths?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SemanticIndexResult {
|
|
22
|
+
symbols: CodeSymbol[];
|
|
23
|
+
edges: CodeEdge[];
|
|
24
|
+
parsedFiles: number;
|
|
25
|
+
parserErrors: number;
|
|
26
|
+
unresolvedCalls: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
diagnostics: Array<{ path: string; message: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SymbolRecord {
|
|
32
|
+
node: ts.Declaration;
|
|
33
|
+
symbol: CodeSymbol;
|
|
34
|
+
checkerSymbol?: ts.Symbol;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hashText(value: string): string {
|
|
38
|
+
return createHash('sha256').update(value).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function canonicalPath(value: string): string {
|
|
42
|
+
const normalized = resolve(value).replace(/\\/g, '/');
|
|
43
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isInside(root: string, candidate: string): boolean {
|
|
47
|
+
const rootPath = canonicalPath(root).replace(/\/$/, '');
|
|
48
|
+
const candidatePath = canonicalPath(candidate);
|
|
49
|
+
return candidatePath === rootPath || candidatePath.startsWith(`${rootPath}/`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scriptKind(path: string): ts.ScriptKind {
|
|
53
|
+
if (path.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
54
|
+
if (path.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
55
|
+
if (path.endsWith('.json')) return ts.ScriptKind.JSON;
|
|
56
|
+
if (path.endsWith('.js') || path.endsWith('.mjs') || path.endsWith('.cjs')) return ts.ScriptKind.JS;
|
|
57
|
+
return ts.ScriptKind.TS;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isTestPath(path: string): boolean {
|
|
61
|
+
return /(^|[/\\])__tests__([/\\])|(?:\.test|\.spec)\.[^.]+$/i.test(path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function declarationName(node: ts.Node): string | undefined {
|
|
65
|
+
const named = node as ts.Declaration & { name?: ts.PropertyName | ts.BindingName };
|
|
66
|
+
const name = named.name;
|
|
67
|
+
return name && ts.isIdentifier(name) ? name.text : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function symbolKind(node: ts.Declaration): CodeSymbol['kind'] | undefined {
|
|
71
|
+
if (ts.isFunctionDeclaration(node)) return 'function';
|
|
72
|
+
if (ts.isClassDeclaration(node)) return 'class';
|
|
73
|
+
if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isConstructorDeclaration(node)
|
|
74
|
+
|| ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) return 'method';
|
|
75
|
+
if (ts.isInterfaceDeclaration(node)) return 'interface';
|
|
76
|
+
if (ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) return 'type';
|
|
77
|
+
if (ts.isEnumMember(node)) return 'constant';
|
|
78
|
+
if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) return 'property';
|
|
79
|
+
if (ts.isVariableDeclaration(node)) return node.initializer && (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer))
|
|
80
|
+
? 'function'
|
|
81
|
+
: 'constant';
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isDeclarationNode(node: ts.Node): node is ts.Declaration {
|
|
86
|
+
return Boolean(symbolKind(node as ts.Declaration) && declarationName(node));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function qualifiedName(node: ts.Declaration, name: string, ordinalByKey: Map<string, number>): string {
|
|
90
|
+
const parts = [name];
|
|
91
|
+
let parent: ts.Node | undefined = node.parent;
|
|
92
|
+
while (parent && !ts.isSourceFile(parent)) {
|
|
93
|
+
if (isDeclarationNode(parent)) {
|
|
94
|
+
const parentName = declarationName(parent);
|
|
95
|
+
if (parentName) parts.unshift(parentName);
|
|
96
|
+
}
|
|
97
|
+
parent = parent.parent;
|
|
98
|
+
}
|
|
99
|
+
const base = parts.join('::');
|
|
100
|
+
const key = `${base}\n${symbolKind(node)}`;
|
|
101
|
+
const ordinal = ordinalByKey.get(key) ?? 0;
|
|
102
|
+
ordinalByKey.set(key, ordinal + 1);
|
|
103
|
+
return ordinal === 0 ? base : `${base}#${ordinal + 1}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function lineAt(sourceFile: ts.SourceFile, position: number): number {
|
|
107
|
+
return sourceFile.getLineAndCharacterOfPosition(position).line + 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function signatureFor(checker: ts.TypeChecker, node: ts.Declaration, sourceFile: ts.SourceFile): string | undefined {
|
|
111
|
+
const signature = ts.isFunctionLike(node) ? checker.getSignatureFromDeclaration(node) : undefined;
|
|
112
|
+
if (signature) {
|
|
113
|
+
try {
|
|
114
|
+
return checker.signatureToString(signature, node, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope).slice(0, 240);
|
|
115
|
+
} catch {
|
|
116
|
+
// Broken or incomplete syntax should use the declaration text below.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const text = sourceFile.text.slice(node.getStart(sourceFile), Math.min(node.end, node.getStart(sourceFile) + 240));
|
|
120
|
+
return text.replace(/\s+/g, ' ').trim() || undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function makeCompilerOptions(projectRoot: string): ts.CompilerOptions {
|
|
124
|
+
const configPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, 'tsconfig.json');
|
|
125
|
+
let options: ts.CompilerOptions = {
|
|
126
|
+
allowJs: true,
|
|
127
|
+
checkJs: false,
|
|
128
|
+
noEmit: true,
|
|
129
|
+
noLib: true,
|
|
130
|
+
skipLibCheck: true,
|
|
131
|
+
module: ts.ModuleKind.ESNext,
|
|
132
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
133
|
+
target: ts.ScriptTarget.ES2022,
|
|
134
|
+
jsx: ts.JsxEmit.Preserve,
|
|
135
|
+
types: [],
|
|
136
|
+
};
|
|
137
|
+
if (configPath) {
|
|
138
|
+
try {
|
|
139
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
140
|
+
if (!config.error) {
|
|
141
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, projectRoot);
|
|
142
|
+
options = { ...options, ...parsed.options, noEmit: true, noLib: true, skipLibCheck: true, allowJs: true, checkJs: false, types: [] };
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// A malformed project config must not disable the local fallback parser.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return options;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveLocalFile(
|
|
152
|
+
projectRoot: string,
|
|
153
|
+
filesByCanonicalPath: Map<string, CodeFile>,
|
|
154
|
+
resolvedFileName: string | undefined,
|
|
155
|
+
): CodeFile | undefined {
|
|
156
|
+
if (!resolvedFileName || !isInside(projectRoot, resolvedFileName)) return undefined;
|
|
157
|
+
return filesByCanonicalPath.get(canonicalPath(resolvedFileName));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function isSemanticLanguage(language: string | undefined): boolean {
|
|
161
|
+
return Boolean(language && SUPPORTED_LANGUAGES.has(language));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildTypeScriptSemanticIndex(input: SemanticIndexOptions): SemanticIndexResult {
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
const semanticFiles = input.files.filter(file => isSemanticLanguage(file.language)
|
|
167
|
+
&& !isCodeGraphExcludedPath(file.path, input.exclude));
|
|
168
|
+
if (semanticFiles.length === 0) {
|
|
169
|
+
return { symbols: [], edges: [], parsedFiles: 0, parserErrors: 0, unresolvedCalls: 0, durationMs: Date.now() - started, diagnostics: [] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const projectRoot = resolve(input.projectRoot);
|
|
173
|
+
const filesByCanonicalPath = new Map(semanticFiles.map(file => [canonicalPath(resolve(projectRoot, file.path)), file]));
|
|
174
|
+
const includePaths = input.includePaths
|
|
175
|
+
? new Set(input.includePaths.map(normalizeCodePath))
|
|
176
|
+
: undefined;
|
|
177
|
+
const includedFileIds = new Set(
|
|
178
|
+
semanticFiles.filter(file => !includePaths || includePaths.has(normalizeCodePath(file.path))).map(file => file.id),
|
|
179
|
+
);
|
|
180
|
+
const rootNames = semanticFiles.map(file => resolve(projectRoot, file.path));
|
|
181
|
+
const baseHost = ts.createCompilerHost(makeCompilerOptions(projectRoot), true);
|
|
182
|
+
const compilerOptions = makeCompilerOptions(projectRoot);
|
|
183
|
+
const host: ts.CompilerHost = {
|
|
184
|
+
...baseHost,
|
|
185
|
+
getCurrentDirectory: () => projectRoot,
|
|
186
|
+
fileExists: fileName => isInside(projectRoot, fileName) && baseHost.fileExists(fileName),
|
|
187
|
+
readFile: fileName => isInside(projectRoot, fileName) ? baseHost.readFile(fileName) : undefined,
|
|
188
|
+
realpath: fileName => fileName,
|
|
189
|
+
getSourceFile: (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
|
|
190
|
+
if (!isInside(projectRoot, fileName)) return undefined;
|
|
191
|
+
const text = baseHost.readFile(fileName);
|
|
192
|
+
if (text === undefined) return undefined;
|
|
193
|
+
return ts.createSourceFile(fileName, text, languageVersion, true, scriptKind(fileName));
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
const program = ts.createProgram({ rootNames, options: compilerOptions, host });
|
|
197
|
+
const checker = program.getTypeChecker();
|
|
198
|
+
const symbols: CodeSymbol[] = [];
|
|
199
|
+
const allSymbols: CodeSymbol[] = [];
|
|
200
|
+
const declarationToRecord = new Map<ts.Node, SymbolRecord>();
|
|
201
|
+
const checkerToRecord = new Map<ts.Symbol, SymbolRecord>();
|
|
202
|
+
const declarationNameNodes = new Set<ts.Node>();
|
|
203
|
+
const ordinalByKey = new Map<string, number>();
|
|
204
|
+
const sourceFiles = program.getSourceFiles().filter(sourceFile => filesByCanonicalPath.has(canonicalPath(sourceFile.fileName)));
|
|
205
|
+
|
|
206
|
+
for (const sourceFile of sourceFiles) {
|
|
207
|
+
const file = filesByCanonicalPath.get(canonicalPath(sourceFile.fileName));
|
|
208
|
+
if (!file) continue;
|
|
209
|
+
const visitDeclarations = (node: ts.Node) => {
|
|
210
|
+
if (isDeclarationNode(node)) {
|
|
211
|
+
const name = declarationName(node);
|
|
212
|
+
const kind = symbolKind(node);
|
|
213
|
+
if (name && kind) {
|
|
214
|
+
const qName = qualifiedName(node, name, ordinalByKey);
|
|
215
|
+
const start = node.getStart(sourceFile);
|
|
216
|
+
const symbol: CodeSymbol = {
|
|
217
|
+
id: makeCodeSymbolId({ projectId: input.projectId, path: file.path, qualifiedName: qName, kind }),
|
|
218
|
+
projectId: input.projectId,
|
|
219
|
+
fileId: file.id || makeCodeFileId(input.projectId, file.path),
|
|
220
|
+
path: file.path,
|
|
221
|
+
name,
|
|
222
|
+
qualifiedName: qName,
|
|
223
|
+
kind,
|
|
224
|
+
startLine: lineAt(sourceFile, start),
|
|
225
|
+
endLine: lineAt(sourceFile, node.end),
|
|
226
|
+
signature: signatureFor(checker, node, sourceFile),
|
|
227
|
+
contentHash: hashText(sourceFile.text.slice(start, node.end)),
|
|
228
|
+
indexedAt: new Date().toISOString(),
|
|
229
|
+
source: SEMANTIC_SOURCE,
|
|
230
|
+
};
|
|
231
|
+
const namedNode = node as ts.Declaration & { name?: ts.Node };
|
|
232
|
+
const checkerSymbol = namedNode.name ? checker.getSymbolAtLocation(namedNode.name) : undefined;
|
|
233
|
+
const record = { node, symbol, ...(checkerSymbol ? { checkerSymbol } : {}) };
|
|
234
|
+
allSymbols.push(symbol);
|
|
235
|
+
if (!includePaths || includePaths.has(normalizeCodePath(file.path))) symbols.push(symbol);
|
|
236
|
+
declarationToRecord.set(node, record);
|
|
237
|
+
if (checkerSymbol) {
|
|
238
|
+
checkerToRecord.set(checkerSymbol, record);
|
|
239
|
+
for (const declaration of checkerSymbol.declarations ?? []) declarationToRecord.set(declaration, record);
|
|
240
|
+
}
|
|
241
|
+
if (namedNode.name) declarationNameNodes.add(namedNode.name);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
node.forEachChild(visitDeclarations);
|
|
245
|
+
};
|
|
246
|
+
visitDeclarations(sourceFile);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const edges = new Map<string, CodeEdge>();
|
|
250
|
+
const addEdge = (inputEdge: Omit<CodeEdge, 'id' | 'projectId' | 'indexedAt' | 'source'>) => {
|
|
251
|
+
const from = inputEdge.fromSymbolId ?? inputEdge.fromFileId ?? '';
|
|
252
|
+
const to = inputEdge.toSymbolId ?? inputEdge.toFileId
|
|
253
|
+
?? (inputEdge.type === 'imports' && inputEdge.evidence ? `unresolved:${inputEdge.evidence}` : '');
|
|
254
|
+
if (!from || !to) return;
|
|
255
|
+
if (includePaths) {
|
|
256
|
+
const endpointFileIds = new Set<string>();
|
|
257
|
+
if (inputEdge.fromFileId) endpointFileIds.add(inputEdge.fromFileId);
|
|
258
|
+
if (inputEdge.toFileId) endpointFileIds.add(inputEdge.toFileId);
|
|
259
|
+
for (const symbol of allSymbols) {
|
|
260
|
+
if (symbol.id === inputEdge.fromSymbolId || symbol.id === inputEdge.toSymbolId) endpointFileIds.add(symbol.fileId);
|
|
261
|
+
}
|
|
262
|
+
if (![...endpointFileIds].some(fileId => includedFileIds.has(fileId))) return;
|
|
263
|
+
}
|
|
264
|
+
const id = makeCodeEdgeId(input.projectId, from, inputEdge.type, to);
|
|
265
|
+
if (edges.has(id)) return;
|
|
266
|
+
edges.set(id, {
|
|
267
|
+
...inputEdge,
|
|
268
|
+
id,
|
|
269
|
+
projectId: input.projectId,
|
|
270
|
+
indexedAt: new Date().toISOString(),
|
|
271
|
+
source: SEMANTIC_SOURCE,
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
const resolveRecord = (symbol: ts.Symbol | undefined): SymbolRecord | undefined => {
|
|
275
|
+
if (!symbol) return undefined;
|
|
276
|
+
let target = symbol;
|
|
277
|
+
if (target.flags & ts.SymbolFlags.Alias) {
|
|
278
|
+
try { target = checker.getAliasedSymbol(target); } catch { /* unresolved alias */ }
|
|
279
|
+
}
|
|
280
|
+
return checkerToRecord.get(target)
|
|
281
|
+
?? (target.declarations ?? []).map(declaration => declarationToRecord.get(declaration)).find(Boolean);
|
|
282
|
+
};
|
|
283
|
+
const currentRecord = (node: ts.Node): SymbolRecord | undefined => {
|
|
284
|
+
let parent: ts.Node | undefined = node;
|
|
285
|
+
while (parent) {
|
|
286
|
+
const record = declarationToRecord.get(parent);
|
|
287
|
+
if (record) return record;
|
|
288
|
+
parent = parent.parent;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
};
|
|
292
|
+
const fileForSource = (sourceFile: ts.SourceFile): CodeFile | undefined => filesByCanonicalPath.get(canonicalPath(sourceFile.fileName));
|
|
293
|
+
const addFileRelation = (
|
|
294
|
+
sourceFile: ts.SourceFile,
|
|
295
|
+
target: CodeFile | undefined,
|
|
296
|
+
type: CodeEdge['type'],
|
|
297
|
+
node: ts.Node,
|
|
298
|
+
evidence?: string,
|
|
299
|
+
) => {
|
|
300
|
+
const source = fileForSource(sourceFile);
|
|
301
|
+
if (!source || source.id === target?.id) return;
|
|
302
|
+
addEdge({
|
|
303
|
+
fromFileId: source.id,
|
|
304
|
+
...(target ? { toFileId: target.id } : {}),
|
|
305
|
+
type,
|
|
306
|
+
confidence: target ? 0.99 : 0.55,
|
|
307
|
+
evidence: evidence ?? `${type} at line ${lineAt(sourceFile, node.getStart(sourceFile))}`,
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const moduleTarget = (sourceFile: ts.SourceFile, moduleName: string): CodeFile | undefined => {
|
|
311
|
+
const resolvedModule = ts.resolveModuleName(moduleName, sourceFile.fileName, compilerOptions, host).resolvedModule;
|
|
312
|
+
return resolveLocalFile(projectRoot, filesByCanonicalPath, resolvedModule?.resolvedFileName);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
let unresolvedCalls = 0;
|
|
316
|
+
for (const sourceFile of sourceFiles) {
|
|
317
|
+
const file = fileForSource(sourceFile);
|
|
318
|
+
if (!file) continue;
|
|
319
|
+
const visit = (node: ts.Node) => {
|
|
320
|
+
const owner = currentRecord(node);
|
|
321
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
322
|
+
addFileRelation(sourceFile, moduleTarget(sourceFile, node.moduleSpecifier.text), 'imports', node, `imports ${node.moduleSpecifier.text} at line ${lineAt(sourceFile, node.getStart(sourceFile))}`);
|
|
323
|
+
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
324
|
+
addFileRelation(sourceFile, moduleTarget(sourceFile, node.moduleSpecifier.text), 'exports', node, `exports ${node.moduleSpecifier.text} at line ${lineAt(sourceFile, node.getStart(sourceFile))}`);
|
|
325
|
+
} else if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
326
|
+
const signature = checker.getResolvedSignature(node);
|
|
327
|
+
const declarationNameNode = signature?.declaration
|
|
328
|
+
? (signature.declaration as ts.Declaration & { name?: ts.Node }).name
|
|
329
|
+
: undefined;
|
|
330
|
+
const target = resolveRecord(declarationNameNode ? checker.getSymbolAtLocation(declarationNameNode) : undefined)
|
|
331
|
+
?? (signature?.declaration ? declarationToRecord.get(signature.declaration) : undefined)
|
|
332
|
+
?? resolveRecord(checker.getSymbolAtLocation(node.expression));
|
|
333
|
+
if (target) {
|
|
334
|
+
const from = owner?.symbol.id;
|
|
335
|
+
addEdge({
|
|
336
|
+
...(from ? { fromSymbolId: from } : { fromFileId: file.id }),
|
|
337
|
+
toSymbolId: target.symbol.id,
|
|
338
|
+
type: 'calls',
|
|
339
|
+
confidence: 0.98,
|
|
340
|
+
evidence: `resolved call at line ${lineAt(sourceFile, node.getStart(sourceFile))}`,
|
|
341
|
+
});
|
|
342
|
+
if (isTestPath(file.path)) {
|
|
343
|
+
addEdge({
|
|
344
|
+
...(from ? { fromSymbolId: from } : { fromFileId: file.id }),
|
|
345
|
+
toSymbolId: target.symbol.id,
|
|
346
|
+
type: 'tests',
|
|
347
|
+
confidence: 0.97,
|
|
348
|
+
evidence: `resolved test call at line ${lineAt(sourceFile, node.getStart(sourceFile))}`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
unresolvedCalls++;
|
|
353
|
+
}
|
|
354
|
+
} else if (ts.isIdentifier(node) && !declarationNameNodes.has(node)
|
|
355
|
+
&& !ts.isImportSpecifier(node.parent) && !ts.isExportSpecifier(node.parent)) {
|
|
356
|
+
const target = resolveRecord(checker.getSymbolAtLocation(node));
|
|
357
|
+
if (target) {
|
|
358
|
+
addEdge({
|
|
359
|
+
...(owner?.symbol.id ? { fromSymbolId: owner.symbol.id } : { fromFileId: file.id }),
|
|
360
|
+
toSymbolId: target.symbol.id,
|
|
361
|
+
type: 'references',
|
|
362
|
+
confidence: 0.95,
|
|
363
|
+
evidence: `resolved reference at line ${lineAt(sourceFile, node.getStart(sourceFile))}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
} else if (ts.isHeritageClause(node)) {
|
|
367
|
+
const ownerRecord = currentRecord(node);
|
|
368
|
+
for (const type of node.types) {
|
|
369
|
+
const target = resolveRecord(checker.getSymbolAtLocation(type.expression));
|
|
370
|
+
if (ownerRecord && target) {
|
|
371
|
+
addEdge({
|
|
372
|
+
fromSymbolId: ownerRecord.symbol.id,
|
|
373
|
+
toSymbolId: target.symbol.id,
|
|
374
|
+
type: node.token === ts.SyntaxKind.ExtendsKeyword ? 'extends' : 'implements',
|
|
375
|
+
confidence: 0.99,
|
|
376
|
+
evidence: `compiler-resolved heritage at line ${lineAt(sourceFile, type.getStart(sourceFile))}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
node.forEachChild(visit);
|
|
382
|
+
};
|
|
383
|
+
visit(sourceFile);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const diagnostics = sourceFiles.flatMap(sourceFile => {
|
|
387
|
+
const file = fileForSource(sourceFile);
|
|
388
|
+
if (!file || (includePaths && !includePaths.has(normalizeCodePath(file.path)))) return [];
|
|
389
|
+
return program.getSyntacticDiagnostics(sourceFile).slice(0, 4).map(diagnostic => ({
|
|
390
|
+
path: file.path,
|
|
391
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ').slice(0, 240),
|
|
392
|
+
}));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
symbols,
|
|
397
|
+
edges: [...edges.values()],
|
|
398
|
+
parsedFiles: sourceFiles.length,
|
|
399
|
+
parserErrors: diagnostics.length,
|
|
400
|
+
unresolvedCalls,
|
|
401
|
+
durationMs: Date.now() - started,
|
|
402
|
+
diagnostics,
|
|
403
|
+
};
|
|
404
|
+
}
|