ragcode-context-engine 0.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/LICENSE +21 -0
- package/README.md +366 -0
- package/README.zh-CN.md +363 -0
- package/dist/src/cli/configure/app.d.ts +6 -0
- package/dist/src/cli/configure/app.js +81 -0
- package/dist/src/cli/configure/run.d.ts +5 -0
- package/dist/src/cli/configure/run.js +85 -0
- package/dist/src/cli/configure/state.d.ts +42 -0
- package/dist/src/cli/configure/state.js +174 -0
- package/dist/src/cli/configure.d.ts +31 -0
- package/dist/src/cli/configure.js +101 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +503 -0
- package/dist/src/cli/tui/index-progress.d.ts +12 -0
- package/dist/src/cli/tui/index-progress.js +49 -0
- package/dist/src/cli/tui/watch-status.d.ts +10 -0
- package/dist/src/cli/tui/watch-status.js +27 -0
- package/dist/src/cli/update.d.ts +18 -0
- package/dist/src/cli/update.js +111 -0
- package/dist/src/config/dotenv.d.ts +1 -0
- package/dist/src/config/dotenv.js +14 -0
- package/dist/src/config/graph-runtime.d.ts +13 -0
- package/dist/src/config/graph-runtime.js +29 -0
- package/dist/src/config/runtime-config.d.ts +87 -0
- package/dist/src/config/runtime-config.js +215 -0
- package/dist/src/config/semantic-runtime.d.ts +24 -0
- package/dist/src/config/semantic-runtime.js +89 -0
- package/dist/src/context/context-builder.d.ts +20 -0
- package/dist/src/context/context-builder.js +277 -0
- package/dist/src/context/expansion-policy.d.ts +6 -0
- package/dist/src/context/expansion-policy.js +49 -0
- package/dist/src/context/skeletonizer.d.ts +2 -0
- package/dist/src/context/skeletonizer.js +79 -0
- package/dist/src/context/snippet-renderer.d.ts +2 -0
- package/dist/src/context/snippet-renderer.js +67 -0
- package/dist/src/core/contracts.d.ts +74 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/engine.d.ts +64 -0
- package/dist/src/core/engine.js +442 -0
- package/dist/src/core/types.d.ts +490 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/diagnostics/doctor.d.ts +66 -0
- package/dist/src/diagnostics/doctor.js +193 -0
- package/dist/src/diagnostics/embedding-test.d.ts +24 -0
- package/dist/src/diagnostics/embedding-test.js +83 -0
- package/dist/src/graph/diff-files.d.ts +1 -0
- package/dist/src/graph/diff-files.js +14 -0
- package/dist/src/graph/impact-report.d.ts +10 -0
- package/dist/src/graph/impact-report.js +173 -0
- package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
- package/dist/src/graph/in-memory-graph-store.js +395 -0
- package/dist/src/graph/owner-ranking.d.ts +2 -0
- package/dist/src/graph/owner-ranking.js +41 -0
- package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
- package/dist/src/graph/sqlite-graph-store.js +724 -0
- package/dist/src/graph/sqlite-statements.d.ts +36 -0
- package/dist/src/graph/sqlite-statements.js +105 -0
- package/dist/src/graph/target-matcher.d.ts +13 -0
- package/dist/src/graph/target-matcher.js +64 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/registry.d.ts +5 -0
- package/dist/src/indexing/analyzers/registry.js +23 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
- package/dist/src/indexing/analyzers/types.d.ts +17 -0
- package/dist/src/indexing/analyzers/types.js +1 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
- package/dist/src/indexing/ast-analyzer.d.ts +11 -0
- package/dist/src/indexing/ast-analyzer.js +11 -0
- package/dist/src/indexing/chunker.d.ts +11 -0
- package/dist/src/indexing/chunker.js +157 -0
- package/dist/src/indexing/ignore-policy.d.ts +6 -0
- package/dist/src/indexing/ignore-policy.js +40 -0
- package/dist/src/indexing/indexer.d.ts +13 -0
- package/dist/src/indexing/indexer.js +189 -0
- package/dist/src/indexing/language.d.ts +3 -0
- package/dist/src/indexing/language.js +24 -0
- package/dist/src/indexing/scanner.d.ts +13 -0
- package/dist/src/indexing/scanner.js +87 -0
- package/dist/src/lsp/definition-resolver.d.ts +6 -0
- package/dist/src/lsp/definition-resolver.js +60 -0
- package/dist/src/lsp/typescript-language-service.d.ts +21 -0
- package/dist/src/lsp/typescript-language-service.js +82 -0
- package/dist/src/mcp/server.d.ts +11 -0
- package/dist/src/mcp/server.js +64 -0
- package/dist/src/mcp/tools.d.ts +266 -0
- package/dist/src/mcp/tools.js +309 -0
- package/dist/src/project/project-identity.d.ts +2 -0
- package/dist/src/project/project-identity.js +24 -0
- package/dist/src/project/project-registry.d.ts +12 -0
- package/dist/src/project/project-registry.js +49 -0
- package/dist/src/project/workspace-resolver.d.ts +20 -0
- package/dist/src/project/workspace-resolver.js +62 -0
- package/dist/src/retrieval/graph-reranker.d.ts +11 -0
- package/dist/src/retrieval/graph-reranker.js +0 -0
- package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
- package/dist/src/retrieval/hybrid-retriever.js +111 -0
- package/dist/src/retrieval/path-classification.d.ts +6 -0
- package/dist/src/retrieval/path-classification.js +22 -0
- package/dist/src/retrieval/query-matching.d.ts +22 -0
- package/dist/src/retrieval/query-matching.js +166 -0
- package/dist/src/retrieval/query-planner.d.ts +5 -0
- package/dist/src/retrieval/query-planner.js +77 -0
- package/dist/src/retrieval/ranking-signals.d.ts +19 -0
- package/dist/src/retrieval/ranking-signals.js +97 -0
- package/dist/src/retrieval/topology-distance.d.ts +21 -0
- package/dist/src/retrieval/topology-distance.js +116 -0
- package/dist/src/reuse/reuse-detector.d.ts +12 -0
- package/dist/src/reuse/reuse-detector.js +564 -0
- package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
- package/dist/src/semantic/deterministic-embedding.js +31 -0
- package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
- package/dist/src/semantic/in-memory-semantic-store.js +65 -0
- package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
- package/dist/src/semantic/lance-semantic-store.js +623 -0
- package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
- package/dist/src/semantic/openai-compatible-embedding.js +75 -0
- package/dist/src/service/service-identity.d.ts +13 -0
- package/dist/src/service/service-identity.js +48 -0
- package/dist/src/service/service-manager.d.ts +29 -0
- package/dist/src/service/service-manager.js +231 -0
- package/dist/src/service/service-templates.d.ts +22 -0
- package/dist/src/service/service-templates.js +101 -0
- package/dist/src/subgraph/impact-explainer.d.ts +2 -0
- package/dist/src/subgraph/impact-explainer.js +54 -0
- package/dist/src/subgraph/node-expander.d.ts +13 -0
- package/dist/src/subgraph/node-expander.js +139 -0
- package/dist/src/subgraph/output-preset.d.ts +3 -0
- package/dist/src/subgraph/output-preset.js +102 -0
- package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
- package/dist/src/subgraph/subgraph-builder.js +688 -0
- package/dist/src/topology/export-index.d.ts +7 -0
- package/dist/src/topology/export-index.js +14 -0
- package/dist/src/topology/framework-topology.d.ts +3 -0
- package/dist/src/topology/framework-topology.js +460 -0
- package/dist/src/topology/import-resolver.d.ts +2 -0
- package/dist/src/topology/import-resolver.js +29 -0
- package/dist/src/topology/orm-topology.d.ts +3 -0
- package/dist/src/topology/orm-topology.js +200 -0
- package/dist/src/topology/runtime-topology.d.ts +3 -0
- package/dist/src/topology/runtime-topology.js +204 -0
- package/dist/src/topology/symbol-resolver.d.ts +6 -0
- package/dist/src/topology/symbol-resolver.js +74 -0
- package/dist/src/topology/test-topology.d.ts +2 -0
- package/dist/src/topology/test-topology.js +82 -0
- package/dist/src/utils/hash.d.ts +2 -0
- package/dist/src/utils/hash.js +7 -0
- package/dist/src/utils/path.d.ts +2 -0
- package/dist/src/utils/path.js +7 -0
- package/dist/src/watch/event-journal.d.ts +17 -0
- package/dist/src/watch/event-journal.js +81 -0
- package/dist/src/watch/file-event-coalescer.d.ts +9 -0
- package/dist/src/watch/file-event-coalescer.js +39 -0
- package/dist/src/watch/index-scheduler.d.ts +52 -0
- package/dist/src/watch/index-scheduler.js +190 -0
- package/dist/src/watch/watch-daemon.d.ts +73 -0
- package/dist/src/watch/watch-daemon.js +368 -0
- package/dist/src/watch/watcher-liveness.d.ts +47 -0
- package/dist/src/watch/watcher-liveness.js +168 -0
- package/dist/src/web/server.d.ts +1 -0
- package/dist/src/web/server.js +375 -0
- package/package.json +94 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildImpactAnalysis, impactReference } from "./impact-report.js";
|
|
4
|
+
import { isIncomingImpactEdge, isOutgoingImpactEdge, matchesImpactTarget, parseImpactTarget } from "./target-matcher.js";
|
|
5
|
+
import { normalizeUserPath } from "../utils/path.js";
|
|
6
|
+
import { SqliteStatements } from "./sqlite-statements.js";
|
|
7
|
+
import { coalesceFileEvents } from "../watch/file-event-coalescer.js";
|
|
8
|
+
import { buildQueryMatchProfile, scoreChunkText, scoreSymbolText } from "../retrieval/query-matching.js";
|
|
9
|
+
import { extractChangedFiles } from "./diff-files.js";
|
|
10
|
+
import { applyOwnerPathIntent } from "./owner-ranking.js";
|
|
11
|
+
export class SQLiteGraphStore {
|
|
12
|
+
db;
|
|
13
|
+
sql;
|
|
14
|
+
constructor(dbPath) {
|
|
15
|
+
this.db = new DatabaseSync(dbPath);
|
|
16
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
17
|
+
// WAL lets readers (search/context/status) run without blocking the single background
|
|
18
|
+
// writer (the watch daemon's indexer). Concurrent writers are still serialized by SQLite's
|
|
19
|
+
// write lock, but with WAL they no longer abort each other on contention the way DELETE
|
|
20
|
+
// mode does. This is the prerequisite for running a long-lived watcher alongside ad-hoc
|
|
21
|
+
// CLI/MCP index calls. SQLiteGraphStore is only ever constructed with an on-disk path
|
|
22
|
+
// (the `memory` graph kind routes to InMemoryGraphStore), so WAL always applies.
|
|
23
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
24
|
+
// NORMAL is the standard durability/throughput tradeoff under WAL: durable across app
|
|
25
|
+
// crashes, only at risk on OS/power loss, which is acceptable for a rebuildable index.
|
|
26
|
+
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
27
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
28
|
+
this.migrate();
|
|
29
|
+
this.sql = new SqliteStatements(this.db);
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
this.db.close();
|
|
33
|
+
}
|
|
34
|
+
async getProjectByRoot(repoRoot) {
|
|
35
|
+
const root = normalizeRepoRoot(repoRoot);
|
|
36
|
+
const row = this.sql.selectProjectByRoot.get(root, root);
|
|
37
|
+
return row ? projectFromRow(row) : undefined;
|
|
38
|
+
}
|
|
39
|
+
async listProjects() {
|
|
40
|
+
return this.sql.listProjects.all().map(projectFromRow);
|
|
41
|
+
}
|
|
42
|
+
async getIndexGeneration(repoRoot) {
|
|
43
|
+
const root = normalizeRepoRoot(repoRoot);
|
|
44
|
+
const row = this.sql.selectProjectByRoot.get(root, root);
|
|
45
|
+
return row ? Number(row.index_generation ?? 0) : 0;
|
|
46
|
+
}
|
|
47
|
+
async recordFileEvents(repoRoot, filePaths, options) {
|
|
48
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
49
|
+
const coalesced = coalesceFileEvents(repoRoot, filePaths, options);
|
|
50
|
+
this.transaction(() => {
|
|
51
|
+
for (const filePath of coalesced.dirtyFiles) {
|
|
52
|
+
const eventCount = coalesced.eventCountByFile.get(filePath) ?? 1;
|
|
53
|
+
this.db.prepare(`
|
|
54
|
+
INSERT INTO dirty_files(project_id, file_path, status, reason, first_seen_at_ms, last_seen_at_ms, event_count)
|
|
55
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?)
|
|
56
|
+
ON CONFLICT(project_id, file_path) DO UPDATE SET
|
|
57
|
+
status = 'pending',
|
|
58
|
+
reason = excluded.reason,
|
|
59
|
+
last_seen_at_ms = excluded.last_seen_at_ms,
|
|
60
|
+
event_count = dirty_files.event_count + excluded.event_count
|
|
61
|
+
`).run(projectId, filePath, coalesced.burstMode ? "watcher burst event" : "watcher file event", coalesced.lastEventAtMs, coalesced.lastEventAtMs, eventCount);
|
|
62
|
+
}
|
|
63
|
+
this.db.prepare(`
|
|
64
|
+
INSERT INTO watcher_state(project_id, burst_mode, dropped_events, last_event_at_ms, updated_at_ms)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?)
|
|
66
|
+
ON CONFLICT(project_id) DO UPDATE SET
|
|
67
|
+
burst_mode = CASE WHEN watcher_state.burst_mode = 1 OR excluded.burst_mode = 1 THEN 1 ELSE 0 END,
|
|
68
|
+
dropped_events = watcher_state.dropped_events + excluded.dropped_events,
|
|
69
|
+
last_event_at_ms = excluded.last_event_at_ms,
|
|
70
|
+
updated_at_ms = excluded.updated_at_ms
|
|
71
|
+
`).run(projectId, coalesced.burstMode ? 1 : 0, coalesced.droppedEvents, coalesced.lastEventAtMs, coalesced.lastEventAtMs);
|
|
72
|
+
});
|
|
73
|
+
return this.watcherStateForProject(projectId);
|
|
74
|
+
}
|
|
75
|
+
async getWatcherState(repoRoot) {
|
|
76
|
+
return this.watcherStateForProject(this.requireProjectId(repoRoot));
|
|
77
|
+
}
|
|
78
|
+
async markDirtyFilesIndexing(repoRoot, filePaths) {
|
|
79
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
this.transaction(() => {
|
|
82
|
+
for (const filePath of filePaths) {
|
|
83
|
+
this.db.prepare(`
|
|
84
|
+
UPDATE dirty_files
|
|
85
|
+
SET status = 'indexing', reason = 'background batch indexing', last_seen_at_ms = ?
|
|
86
|
+
WHERE project_id = ? AND file_path = ?
|
|
87
|
+
`).run(now, projectId, filePath);
|
|
88
|
+
}
|
|
89
|
+
this.db.prepare(`
|
|
90
|
+
INSERT INTO watcher_state(project_id, burst_mode, dropped_events, last_event_at_ms, updated_at_ms)
|
|
91
|
+
VALUES (?, 0, 0, ?, ?)
|
|
92
|
+
ON CONFLICT(project_id) DO UPDATE SET updated_at_ms = excluded.updated_at_ms
|
|
93
|
+
`).run(projectId, now, now);
|
|
94
|
+
});
|
|
95
|
+
return this.watcherStateForProject(projectId);
|
|
96
|
+
}
|
|
97
|
+
async markDirtyFilesDeadLetter(repoRoot, filePaths, reason) {
|
|
98
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
this.transaction(() => {
|
|
101
|
+
for (const filePath of filePaths) {
|
|
102
|
+
this.db.prepare(`
|
|
103
|
+
UPDATE dirty_files
|
|
104
|
+
SET status = 'dead_letter', reason = ?, last_seen_at_ms = ?
|
|
105
|
+
WHERE project_id = ? AND file_path = ?
|
|
106
|
+
`).run(reason, now, projectId, filePath);
|
|
107
|
+
}
|
|
108
|
+
this.db.prepare(`
|
|
109
|
+
INSERT INTO watcher_state(project_id, burst_mode, dropped_events, last_event_at_ms, updated_at_ms)
|
|
110
|
+
VALUES (?, 0, 0, ?, ?)
|
|
111
|
+
ON CONFLICT(project_id) DO UPDATE SET updated_at_ms = excluded.updated_at_ms
|
|
112
|
+
`).run(projectId, now, now);
|
|
113
|
+
});
|
|
114
|
+
return this.watcherStateForProject(projectId);
|
|
115
|
+
}
|
|
116
|
+
async clearDirtyFiles(repoRoot, filePaths) {
|
|
117
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
118
|
+
this.transaction(() => this.clearDirtyRows(projectId, filePaths));
|
|
119
|
+
}
|
|
120
|
+
async resetRepo(repoRoot) {
|
|
121
|
+
const projectId = this.projectIdForRoot(repoRoot);
|
|
122
|
+
if (!projectId)
|
|
123
|
+
return;
|
|
124
|
+
this.transaction(() => this.deleteProjectRows(projectId));
|
|
125
|
+
}
|
|
126
|
+
async upsertIndex(index) {
|
|
127
|
+
const repoRoot = normalizeRepoRoot(index.repoRoot);
|
|
128
|
+
const project = index.project ?? fallbackProjectIdentity(index.projectId, repoRoot, index.indexedAtMs);
|
|
129
|
+
const symbolsByFile = groupByPath(index.symbols);
|
|
130
|
+
const edgesByFile = groupEdgesByPath(index.edges);
|
|
131
|
+
const chunksByFile = groupByPath(index.chunks);
|
|
132
|
+
this.transaction(() => {
|
|
133
|
+
this.sql.upsertProject.run(project.projectId, normalizeRepoRoot(project.repoRoot), normalizeRepoRoot(project.canonicalRoot), project.displayName, project.gitRemote ?? null, project.gitHead ?? null, project.createdAtMs, project.lastIndexedAtMs ?? index.indexedAtMs, index.indexedAtMs, index.indexGeneration);
|
|
134
|
+
const changedOrDeleted = refreshedOrDeletedFiles(index);
|
|
135
|
+
if (index.fullReindex) {
|
|
136
|
+
const nextFilePaths = new Set(index.files.map((file) => file.path));
|
|
137
|
+
for (const stalePath of this.filePathsForProject(index.projectId)) {
|
|
138
|
+
if (!nextFilePaths.has(stalePath))
|
|
139
|
+
this.deleteFileRows(index.projectId, stalePath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const filePath of changedOrDeleted)
|
|
143
|
+
this.deleteFileRows(index.projectId, filePath);
|
|
144
|
+
this.sql.deleteSkippedFiles.run(index.projectId);
|
|
145
|
+
const filesToWrite = index.fullReindex ? index.files : index.files.filter((file) => changedOrDeleted.has(file.path));
|
|
146
|
+
for (const file of filesToWrite) {
|
|
147
|
+
this.sql.insertFile.run(file.projectId, file.path, file.absolutePath, file.language, file.sizeBytes, file.contentHash, file.modifiedAtMs, index.indexedAtMs, "fresh", index.indexGeneration);
|
|
148
|
+
for (const symbol of symbolsByFile.get(file.path) ?? []) {
|
|
149
|
+
this.sql.insertSymbol.run(symbol.projectId, symbol.id, symbol.filePath, symbol.name, symbol.kind, symbol.language, symbol.startLine, symbol.endLine, symbol.signature ?? null, symbol.exported ? 1 : 0, index.indexGeneration);
|
|
150
|
+
}
|
|
151
|
+
for (const edge of edgesByFile.get(file.path) ?? []) {
|
|
152
|
+
this.sql.insertEdge.run(edge.projectId, edge.sourceId, edge.targetId, edge.kind, JSON.stringify(edge.metadata ?? {}), edgeFilePath(edge), index.indexGeneration);
|
|
153
|
+
}
|
|
154
|
+
for (const chunk of chunksByFile.get(file.path) ?? []) {
|
|
155
|
+
this.sql.insertChunk.run(chunk.projectId, chunk.id, repoRoot, chunk.filePath, chunk.language, chunk.kind, chunk.symbolName ?? null, chunk.startLine, chunk.endLine, chunk.content, chunk.contentHash, index.indexGeneration);
|
|
156
|
+
this.sql.insertFts.run(chunk.projectId, chunk.id, chunk.filePath, chunk.symbolName ?? null, chunk.content);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const skipped of index.skippedFiles) {
|
|
160
|
+
this.sql.insertSkippedFile.run(index.projectId, skipped.filePath, skipped.reason);
|
|
161
|
+
}
|
|
162
|
+
this.clearDirtyRows(index.projectId, index.affectedFiles);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async getFiles(repoRoot) {
|
|
166
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
167
|
+
return this.sql.selectFiles.all(projectId).map(fileFromRow);
|
|
168
|
+
}
|
|
169
|
+
async getChunks(repoRoot) {
|
|
170
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
171
|
+
return this.chunksForProject(projectId);
|
|
172
|
+
}
|
|
173
|
+
async getSkippedFiles(repoRoot) {
|
|
174
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
175
|
+
return this.sql.selectSkippedFiles.all(projectId).map((row) => ({
|
|
176
|
+
filePath: String(row.file_path),
|
|
177
|
+
reason: String(row.reason)
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
async getSymbols(repoRoot) {
|
|
181
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
182
|
+
return this.symbolsForProject(projectId);
|
|
183
|
+
}
|
|
184
|
+
async getEdges(repoRoot, kind) {
|
|
185
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
186
|
+
const rows = kind
|
|
187
|
+
? this.sql.selectEdgesByKind.all(projectId, kind)
|
|
188
|
+
: this.sql.selectEdges.all(projectId);
|
|
189
|
+
return rows.map(edgeFromRow);
|
|
190
|
+
}
|
|
191
|
+
async findSymbol(repoRoot, name) {
|
|
192
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
193
|
+
const needle = `%${escapeLike(name.toLowerCase())}%`;
|
|
194
|
+
return this.sql.selectSymbolByNameLike.all(projectId, needle).map(symbolFromRow);
|
|
195
|
+
}
|
|
196
|
+
async explainFile(repoRoot, filePath) {
|
|
197
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
198
|
+
const normalized = normalizeUserPath(filePath);
|
|
199
|
+
const file = this.sql.selectFile.get(projectId, normalized);
|
|
200
|
+
return {
|
|
201
|
+
file: file ? fileFromRow(file) : undefined,
|
|
202
|
+
chunks: this.sql.selectChunksByFile.all(projectId, normalized).map(chunkFromRow),
|
|
203
|
+
symbols: this.sql.selectSymbolsByFile.all(projectId, normalized).map(symbolFromRow)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async searchText(query) {
|
|
207
|
+
const repoRoot = requireRepoRoot(query.repoRoot);
|
|
208
|
+
const projectId = this.scopedProjectId(repoRoot, query.projectId);
|
|
209
|
+
const profile = buildQueryMatchProfile(query.query, this.symbolsForProject(projectId));
|
|
210
|
+
if (profile.queryTerms.length === 0)
|
|
211
|
+
return [];
|
|
212
|
+
const ftsQuery = ftsQueryForTerms(profile.ftsTerms);
|
|
213
|
+
if (!ftsQuery)
|
|
214
|
+
return [];
|
|
215
|
+
const rows = this.sql.searchFts.all(projectId, ftsQuery, Math.max(query.limit ?? 20, 20) * 4);
|
|
216
|
+
const hits = [];
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const chunk = chunkFromRow(row);
|
|
219
|
+
const match = scoreChunkText(chunk, profile);
|
|
220
|
+
if (!match)
|
|
221
|
+
continue;
|
|
222
|
+
const rank = Number(row.rank);
|
|
223
|
+
const rankSignal = Number.isFinite(rank) ? Math.min(0.25, Math.log1p(Math.max(0, -rank))) : 0;
|
|
224
|
+
hits.push({
|
|
225
|
+
chunk,
|
|
226
|
+
score: match.score + rankSignal,
|
|
227
|
+
source: "keyword",
|
|
228
|
+
reason: `FTS MATCH ${match.reason}; bm25=${formatRank(rank)}`
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return hits.sort((a, b) => b.score - a.score || a.chunk.filePath.localeCompare(b.chunk.filePath)).slice(0, query.limit ?? 20);
|
|
232
|
+
}
|
|
233
|
+
async findOwner(repoRoot, query, limit = 5) {
|
|
234
|
+
const hits = await this.searchText({ repoRoot, query, limit: limit * 4 });
|
|
235
|
+
const symbols = await this.getSymbols(repoRoot);
|
|
236
|
+
const profile = buildQueryMatchProfile(query, symbols);
|
|
237
|
+
const candidates = new Map();
|
|
238
|
+
for (const hit of hits) {
|
|
239
|
+
const current = candidates.get(hit.chunk.filePath) ?? { filePath: hit.chunk.filePath, score: 0, reasons: [], symbols: [] };
|
|
240
|
+
current.score += hit.score;
|
|
241
|
+
current.reasons.push(hit.reason);
|
|
242
|
+
candidates.set(hit.chunk.filePath, current);
|
|
243
|
+
}
|
|
244
|
+
for (const symbol of symbols) {
|
|
245
|
+
const match = scoreSymbolText(symbol, profile);
|
|
246
|
+
if (!match)
|
|
247
|
+
continue;
|
|
248
|
+
const current = candidates.get(symbol.filePath) ?? { filePath: symbol.filePath, score: 0, reasons: [], symbols: [] };
|
|
249
|
+
current.score += 1 + match.score;
|
|
250
|
+
current.reasons.push(match.reason);
|
|
251
|
+
current.symbols.push(symbol);
|
|
252
|
+
candidates.set(symbol.filePath, current);
|
|
253
|
+
}
|
|
254
|
+
const ranked = applyOwnerPathIntent([...candidates.values()]
|
|
255
|
+
.map((candidate) => ({ ...candidate, reasons: [...new Set(candidate.reasons)], symbols: uniqueSymbols(candidate.symbols) })), query);
|
|
256
|
+
return ranked
|
|
257
|
+
.sort((a, b) => b.score - a.score)
|
|
258
|
+
.slice(0, limit);
|
|
259
|
+
}
|
|
260
|
+
async impactAnalysis(repoRoot, target) {
|
|
261
|
+
const projectId = this.requireProjectId(repoRoot);
|
|
262
|
+
const parsedTarget = parseImpactTarget(target);
|
|
263
|
+
const symbols = this.symbolsForProject(projectId);
|
|
264
|
+
const edges = await this.getEdges(repoRoot);
|
|
265
|
+
const matchedSymbols = symbols.filter((symbol) => matchesImpactTarget(symbol, parsedTarget));
|
|
266
|
+
const matchedIds = new Set(matchedSymbols.map((symbol) => symbol.id));
|
|
267
|
+
const incomingEdges = edges.filter((edge) => isIncomingImpactEdge(edge, matchedIds, parsedTarget));
|
|
268
|
+
const outgoingEdges = edges.filter((edge) => isOutgoingImpactEdge(edge, matchedIds, parsedTarget));
|
|
269
|
+
return buildImpactAnalysis({
|
|
270
|
+
target,
|
|
271
|
+
matchedSymbols,
|
|
272
|
+
incomingEdges,
|
|
273
|
+
outgoingEdges,
|
|
274
|
+
symbols
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async relatedTests(repoRoot, target) {
|
|
278
|
+
const files = await this.getFiles(repoRoot);
|
|
279
|
+
const symbols = await this.getSymbols(repoRoot);
|
|
280
|
+
const edges = await this.getEdges(repoRoot);
|
|
281
|
+
const symbolsById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
|
|
282
|
+
const filesByPath = new Map(files.map((file) => [file.path, file]));
|
|
283
|
+
const normalized = normalizeUserPath(target);
|
|
284
|
+
const basename = normalized.split("/").pop()?.replace(/\.[^.]+$/, "") ?? normalized;
|
|
285
|
+
const matchedIds = new Set(symbols
|
|
286
|
+
.filter((symbol) => matchesTarget(symbol, normalized, target))
|
|
287
|
+
.map((symbol) => symbol.id));
|
|
288
|
+
const graphTestsByPath = new Map();
|
|
289
|
+
const references = [];
|
|
290
|
+
for (const edge of edges) {
|
|
291
|
+
if (edge.kind !== "tested_by")
|
|
292
|
+
continue;
|
|
293
|
+
const sourceFile = typeof edge.metadata?.sourceFile === "string" ? edge.metadata.sourceFile : undefined;
|
|
294
|
+
if (!matchedIds.has(edge.sourceId) && sourceFile !== normalized)
|
|
295
|
+
continue;
|
|
296
|
+
const targetSymbol = symbolsById.get(edge.targetId);
|
|
297
|
+
if (!targetSymbol || !isTestFile(targetSymbol.filePath))
|
|
298
|
+
continue;
|
|
299
|
+
const file = filesByPath.get(targetSymbol.filePath);
|
|
300
|
+
if (file)
|
|
301
|
+
graphTestsByPath.set(file.path, file);
|
|
302
|
+
references.push(impactReference(edge, symbolsById));
|
|
303
|
+
}
|
|
304
|
+
const testsByPath = graphTestsByPath.size > 0 ? graphTestsByPath : filenameTestMatches(files, basename);
|
|
305
|
+
const tests = [...testsByPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
306
|
+
return { target, tests, references, missingLikelyTests: tests.length === 0 ? [`No indexed test file matched ${basename}.`] : [] };
|
|
307
|
+
}
|
|
308
|
+
async traceFlow(repoRoot, entry, maxSteps = 20) {
|
|
309
|
+
const symbols = await this.findSymbol(repoRoot, entry);
|
|
310
|
+
const startIds = new Set(symbols.map((symbol) => symbol.id));
|
|
311
|
+
const allSymbols = await this.getSymbols(repoRoot);
|
|
312
|
+
const steps = (await this.getEdges(repoRoot))
|
|
313
|
+
.filter((edge) => isTraceEdge(edge.kind) && (startIds.has(edge.sourceId) || String(edge.metadata?.sourceFile ?? "").toLowerCase().includes(entry.toLowerCase())))
|
|
314
|
+
.slice(0, maxSteps)
|
|
315
|
+
.map((edge) => {
|
|
316
|
+
const source = allSymbols.find((symbol) => symbol.id === edge.sourceId);
|
|
317
|
+
return {
|
|
318
|
+
filePath: source?.filePath ?? String(edge.metadata?.sourceFile ?? "unknown"),
|
|
319
|
+
symbolName: source?.name ?? "unknown",
|
|
320
|
+
kind: edge.kind,
|
|
321
|
+
targetName: typeof edge.metadata?.targetName === "string" ? edge.metadata.targetName : undefined,
|
|
322
|
+
targetFile: typeof edge.metadata?.targetFile === "string" ? edge.metadata.targetFile : undefined,
|
|
323
|
+
line: typeof edge.metadata?.line === "number" ? edge.metadata.line : undefined
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
return { entry, steps, truncated: steps.length === maxSteps };
|
|
327
|
+
}
|
|
328
|
+
async reviewDiff(repoRoot, diff, changedFiles = []) {
|
|
329
|
+
const files = changedFiles.length > 0 ? changedFiles.map(normalizeUserPath) : extractChangedFiles(diff ?? "");
|
|
330
|
+
const tests = new Set();
|
|
331
|
+
const findings = [];
|
|
332
|
+
let riskScore = 0;
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
const related = await this.relatedTests(repoRoot, file);
|
|
335
|
+
for (const test of related.tests)
|
|
336
|
+
tests.add(test.path);
|
|
337
|
+
if (related.tests.length === 0 && !isTestFile(file))
|
|
338
|
+
findings.push(`No directly related test file found for ${file}.`);
|
|
339
|
+
const impact = await this.impactAnalysis(repoRoot, file);
|
|
340
|
+
riskScore += impact.impactedFiles.length;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
changedFiles: files,
|
|
344
|
+
relatedTests: [...tests].sort(),
|
|
345
|
+
riskLevel: riskScore > 12 ? "high" : riskScore > 4 ? "medium" : "low",
|
|
346
|
+
findings
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
migrate() {
|
|
350
|
+
this.db.exec(`
|
|
351
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
352
|
+
project_id TEXT PRIMARY KEY,
|
|
353
|
+
repo_root TEXT NOT NULL,
|
|
354
|
+
canonical_root TEXT,
|
|
355
|
+
display_name TEXT,
|
|
356
|
+
git_remote TEXT,
|
|
357
|
+
git_head TEXT,
|
|
358
|
+
created_at_ms INTEGER,
|
|
359
|
+
last_indexed_at_ms INTEGER,
|
|
360
|
+
index_generation INTEGER,
|
|
361
|
+
indexed_at_ms INTEGER NOT NULL
|
|
362
|
+
);
|
|
363
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
364
|
+
project_id TEXT NOT NULL,
|
|
365
|
+
path TEXT NOT NULL,
|
|
366
|
+
absolute_path TEXT NOT NULL,
|
|
367
|
+
language TEXT NOT NULL,
|
|
368
|
+
size_bytes INTEGER NOT NULL,
|
|
369
|
+
content_hash TEXT NOT NULL,
|
|
370
|
+
modified_at_ms REAL NOT NULL,
|
|
371
|
+
indexed_at_ms INTEGER NOT NULL,
|
|
372
|
+
status TEXT NOT NULL,
|
|
373
|
+
generation INTEGER NOT NULL,
|
|
374
|
+
PRIMARY KEY(project_id, path)
|
|
375
|
+
);
|
|
376
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
377
|
+
project_id TEXT NOT NULL,
|
|
378
|
+
id TEXT NOT NULL,
|
|
379
|
+
file_path TEXT NOT NULL,
|
|
380
|
+
name TEXT NOT NULL,
|
|
381
|
+
kind TEXT NOT NULL,
|
|
382
|
+
language TEXT NOT NULL,
|
|
383
|
+
start_line INTEGER NOT NULL,
|
|
384
|
+
end_line INTEGER NOT NULL,
|
|
385
|
+
signature TEXT,
|
|
386
|
+
exported INTEGER NOT NULL,
|
|
387
|
+
generation INTEGER NOT NULL,
|
|
388
|
+
PRIMARY KEY(project_id, id)
|
|
389
|
+
);
|
|
390
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
391
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
392
|
+
project_id TEXT NOT NULL,
|
|
393
|
+
source_id TEXT NOT NULL,
|
|
394
|
+
target_id TEXT NOT NULL,
|
|
395
|
+
kind TEXT NOT NULL,
|
|
396
|
+
metadata_json TEXT NOT NULL,
|
|
397
|
+
file_path TEXT,
|
|
398
|
+
generation INTEGER NOT NULL
|
|
399
|
+
);
|
|
400
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
401
|
+
project_id TEXT NOT NULL,
|
|
402
|
+
id TEXT NOT NULL,
|
|
403
|
+
repo_root TEXT NOT NULL,
|
|
404
|
+
file_path TEXT NOT NULL,
|
|
405
|
+
language TEXT NOT NULL,
|
|
406
|
+
kind TEXT NOT NULL,
|
|
407
|
+
symbol_name TEXT,
|
|
408
|
+
start_line INTEGER NOT NULL,
|
|
409
|
+
end_line INTEGER NOT NULL,
|
|
410
|
+
content TEXT NOT NULL,
|
|
411
|
+
content_hash TEXT NOT NULL,
|
|
412
|
+
generation INTEGER NOT NULL,
|
|
413
|
+
PRIMARY KEY(project_id, id)
|
|
414
|
+
);
|
|
415
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(project_id UNINDEXED, id UNINDEXED, file_path, symbol_name, content);
|
|
416
|
+
CREATE TABLE IF NOT EXISTS skipped_files (
|
|
417
|
+
project_id TEXT NOT NULL,
|
|
418
|
+
file_path TEXT NOT NULL,
|
|
419
|
+
reason TEXT NOT NULL,
|
|
420
|
+
PRIMARY KEY(project_id, file_path)
|
|
421
|
+
);
|
|
422
|
+
CREATE TABLE IF NOT EXISTS dirty_files (
|
|
423
|
+
project_id TEXT NOT NULL,
|
|
424
|
+
file_path TEXT NOT NULL,
|
|
425
|
+
status TEXT NOT NULL,
|
|
426
|
+
reason TEXT NOT NULL,
|
|
427
|
+
first_seen_at_ms INTEGER NOT NULL,
|
|
428
|
+
last_seen_at_ms INTEGER NOT NULL,
|
|
429
|
+
event_count INTEGER NOT NULL,
|
|
430
|
+
PRIMARY KEY(project_id, file_path)
|
|
431
|
+
);
|
|
432
|
+
CREATE TABLE IF NOT EXISTS watcher_state (
|
|
433
|
+
project_id TEXT PRIMARY KEY,
|
|
434
|
+
burst_mode INTEGER NOT NULL,
|
|
435
|
+
dropped_events INTEGER NOT NULL,
|
|
436
|
+
last_event_at_ms INTEGER,
|
|
437
|
+
updated_at_ms INTEGER NOT NULL
|
|
438
|
+
);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_project_name ON symbols(project_id, name);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project_kind ON edges(project_id, kind);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_project_file ON chunks(project_id, file_path);
|
|
442
|
+
CREATE INDEX IF NOT EXISTS idx_dirty_files_project_status ON dirty_files(project_id, status);
|
|
443
|
+
`);
|
|
444
|
+
this.ensureProjectColumns();
|
|
445
|
+
this.db.exec(`
|
|
446
|
+
CREATE INDEX IF NOT EXISTS idx_projects_repo_root ON projects(repo_root);
|
|
447
|
+
CREATE INDEX IF NOT EXISTS idx_projects_canonical_root ON projects(canonical_root);
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
450
|
+
ensureProjectColumns() {
|
|
451
|
+
const columns = new Set(this.db.prepare("PRAGMA table_info(projects)").all().map((row) => String(row.name)));
|
|
452
|
+
const additions = {
|
|
453
|
+
canonical_root: "ALTER TABLE projects ADD COLUMN canonical_root TEXT",
|
|
454
|
+
display_name: "ALTER TABLE projects ADD COLUMN display_name TEXT",
|
|
455
|
+
git_remote: "ALTER TABLE projects ADD COLUMN git_remote TEXT",
|
|
456
|
+
git_head: "ALTER TABLE projects ADD COLUMN git_head TEXT",
|
|
457
|
+
created_at_ms: "ALTER TABLE projects ADD COLUMN created_at_ms INTEGER",
|
|
458
|
+
last_indexed_at_ms: "ALTER TABLE projects ADD COLUMN last_indexed_at_ms INTEGER",
|
|
459
|
+
index_generation: "ALTER TABLE projects ADD COLUMN index_generation INTEGER"
|
|
460
|
+
};
|
|
461
|
+
for (const [column, sql] of Object.entries(additions)) {
|
|
462
|
+
if (!columns.has(column))
|
|
463
|
+
this.db.exec(sql);
|
|
464
|
+
}
|
|
465
|
+
this.db.exec("UPDATE projects SET canonical_root = repo_root WHERE canonical_root IS NULL");
|
|
466
|
+
this.db.exec("UPDATE projects SET display_name = project_id WHERE display_name IS NULL");
|
|
467
|
+
this.db.exec("UPDATE projects SET created_at_ms = indexed_at_ms WHERE created_at_ms IS NULL");
|
|
468
|
+
this.db.exec("UPDATE projects SET last_indexed_at_ms = indexed_at_ms WHERE last_indexed_at_ms IS NULL");
|
|
469
|
+
this.db.exec("UPDATE projects SET index_generation = 1 WHERE index_generation IS NULL");
|
|
470
|
+
}
|
|
471
|
+
deleteProjectRows(projectId) {
|
|
472
|
+
for (const table of ["files", "symbols", "edges", "chunks", "chunks_fts", "skipped_files", "dirty_files", "watcher_state"]) {
|
|
473
|
+
this.db.prepare(`DELETE FROM ${table} WHERE project_id = ?`).run(projectId);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
clearDirtyRows(projectId, filePaths) {
|
|
477
|
+
if (!filePaths) {
|
|
478
|
+
this.db.prepare("DELETE FROM dirty_files WHERE project_id = ?").run(projectId);
|
|
479
|
+
this.db.prepare("DELETE FROM watcher_state WHERE project_id = ?").run(projectId);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
for (const filePath of filePaths) {
|
|
483
|
+
this.db.prepare("DELETE FROM dirty_files WHERE project_id = ? AND file_path = ?").run(projectId, filePath);
|
|
484
|
+
}
|
|
485
|
+
const remaining = Number(this.db.prepare("SELECT COUNT(*) AS count FROM dirty_files WHERE project_id = ?").get(projectId)?.count ?? 0);
|
|
486
|
+
if (remaining === 0)
|
|
487
|
+
this.db.prepare("DELETE FROM watcher_state WHERE project_id = ?").run(projectId);
|
|
488
|
+
}
|
|
489
|
+
watcherStateForProject(projectId) {
|
|
490
|
+
const dirtyFiles = this.db.prepare("SELECT * FROM dirty_files WHERE project_id = ? ORDER BY file_path")
|
|
491
|
+
.all(projectId)
|
|
492
|
+
.map(dirtyFileFromRow);
|
|
493
|
+
const state = this.db.prepare("SELECT * FROM watcher_state WHERE project_id = ?").get(projectId);
|
|
494
|
+
return {
|
|
495
|
+
projectId,
|
|
496
|
+
dirtyFiles,
|
|
497
|
+
pendingFiles: dirtyFiles.filter((file) => file.status === "pending").map((file) => file.filePath),
|
|
498
|
+
indexingFiles: dirtyFiles.filter((file) => file.status === "indexing").map((file) => file.filePath),
|
|
499
|
+
burstMode: Boolean(Number(state?.burst_mode ?? 0)),
|
|
500
|
+
droppedEvents: Number(state?.dropped_events ?? 0),
|
|
501
|
+
lastEventAtMs: state?.last_event_at_ms === null || state?.last_event_at_ms === undefined ? undefined : Number(state.last_event_at_ms),
|
|
502
|
+
updatedAtMs: state?.updated_at_ms === null || state?.updated_at_ms === undefined ? undefined : Number(state.updated_at_ms)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
deleteFileRows(projectId, filePath) {
|
|
506
|
+
this.db.prepare("DELETE FROM edges WHERE project_id = ? AND file_path = ?").run(projectId, filePath);
|
|
507
|
+
this.db.prepare("DELETE FROM symbols WHERE project_id = ? AND file_path = ?").run(projectId, filePath);
|
|
508
|
+
this.db.prepare("DELETE FROM chunks_fts WHERE project_id = ? AND file_path = ?").run(projectId, filePath);
|
|
509
|
+
this.db.prepare("DELETE FROM chunks WHERE project_id = ? AND file_path = ?").run(projectId, filePath);
|
|
510
|
+
this.db.prepare("DELETE FROM files WHERE project_id = ? AND path = ?").run(projectId, filePath);
|
|
511
|
+
}
|
|
512
|
+
filePathsForProject(projectId) {
|
|
513
|
+
return this.db.prepare("SELECT path FROM files WHERE project_id = ?").all(projectId).map((row) => String(row.path));
|
|
514
|
+
}
|
|
515
|
+
transaction(fn) {
|
|
516
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
517
|
+
try {
|
|
518
|
+
fn();
|
|
519
|
+
this.db.exec("COMMIT");
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.db.exec("ROLLBACK");
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
projectIdForRoot(repoRoot) {
|
|
527
|
+
const root = normalizeRepoRoot(repoRoot);
|
|
528
|
+
const row = this.sql.selectProjectByRoot.get(root, root);
|
|
529
|
+
return row ? String(row.project_id) : undefined;
|
|
530
|
+
}
|
|
531
|
+
requireProjectId(repoRoot) {
|
|
532
|
+
const projectId = this.projectIdForRoot(repoRoot);
|
|
533
|
+
if (!projectId)
|
|
534
|
+
throw new Error(`Repository is not indexed in SQLiteGraphStore: ${repoRoot}`);
|
|
535
|
+
return projectId;
|
|
536
|
+
}
|
|
537
|
+
scopedProjectId(repoRoot, projectId) {
|
|
538
|
+
const resolvedProjectId = this.requireProjectId(repoRoot);
|
|
539
|
+
if (projectId && projectId !== resolvedProjectId) {
|
|
540
|
+
throw new Error(`Project scope mismatch: repoRoot resolves to ${resolvedProjectId}, but query requested ${projectId}.`);
|
|
541
|
+
}
|
|
542
|
+
return resolvedProjectId;
|
|
543
|
+
}
|
|
544
|
+
chunksForProject(projectId) {
|
|
545
|
+
return this.db.prepare("SELECT * FROM chunks WHERE project_id = ? ORDER BY file_path, start_line").all(projectId).map(chunkFromRow);
|
|
546
|
+
}
|
|
547
|
+
symbolsForProject(projectId) {
|
|
548
|
+
return this.db.prepare("SELECT * FROM symbols WHERE project_id = ? ORDER BY file_path, start_line").all(projectId).map(symbolFromRow);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function fileFromRow(row) {
|
|
552
|
+
return {
|
|
553
|
+
projectId: String(row.project_id),
|
|
554
|
+
path: String(row.path),
|
|
555
|
+
absolutePath: String(row.absolute_path),
|
|
556
|
+
language: String(row.language),
|
|
557
|
+
sizeBytes: Number(row.size_bytes),
|
|
558
|
+
contentHash: String(row.content_hash),
|
|
559
|
+
modifiedAtMs: Number(row.modified_at_ms)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function projectFromRow(row) {
|
|
563
|
+
const repoRoot = String(row.repo_root);
|
|
564
|
+
const canonicalRoot = row.canonical_root === null || row.canonical_root === undefined
|
|
565
|
+
? repoRoot
|
|
566
|
+
: String(row.canonical_root);
|
|
567
|
+
const indexedAtMs = Number(row.indexed_at_ms);
|
|
568
|
+
return {
|
|
569
|
+
projectId: String(row.project_id),
|
|
570
|
+
repoRoot,
|
|
571
|
+
canonicalRoot,
|
|
572
|
+
displayName: row.display_name === null || row.display_name === undefined ? path.basename(canonicalRoot) : String(row.display_name),
|
|
573
|
+
gitRemote: row.git_remote === null || row.git_remote === undefined ? undefined : String(row.git_remote),
|
|
574
|
+
gitHead: row.git_head === null || row.git_head === undefined ? undefined : String(row.git_head),
|
|
575
|
+
createdAtMs: row.created_at_ms === null || row.created_at_ms === undefined ? indexedAtMs : Number(row.created_at_ms),
|
|
576
|
+
lastIndexedAtMs: row.last_indexed_at_ms === null || row.last_indexed_at_ms === undefined ? indexedAtMs : Number(row.last_indexed_at_ms)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function dirtyFileFromRow(row) {
|
|
580
|
+
return {
|
|
581
|
+
projectId: String(row.project_id),
|
|
582
|
+
filePath: String(row.file_path),
|
|
583
|
+
status: String(row.status),
|
|
584
|
+
reason: String(row.reason),
|
|
585
|
+
firstSeenAtMs: Number(row.first_seen_at_ms),
|
|
586
|
+
lastSeenAtMs: Number(row.last_seen_at_ms),
|
|
587
|
+
eventCount: Number(row.event_count)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function fallbackProjectIdentity(projectId, repoRoot, indexedAtMs) {
|
|
591
|
+
return {
|
|
592
|
+
projectId,
|
|
593
|
+
repoRoot,
|
|
594
|
+
canonicalRoot: repoRoot,
|
|
595
|
+
displayName: path.basename(repoRoot),
|
|
596
|
+
createdAtMs: indexedAtMs,
|
|
597
|
+
lastIndexedAtMs: indexedAtMs
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function symbolFromRow(row) {
|
|
601
|
+
return {
|
|
602
|
+
projectId: String(row.project_id),
|
|
603
|
+
id: String(row.id),
|
|
604
|
+
filePath: String(row.file_path),
|
|
605
|
+
name: String(row.name),
|
|
606
|
+
kind: String(row.kind),
|
|
607
|
+
language: String(row.language),
|
|
608
|
+
startLine: Number(row.start_line),
|
|
609
|
+
endLine: Number(row.end_line),
|
|
610
|
+
signature: row.signature === null ? undefined : String(row.signature),
|
|
611
|
+
exported: Boolean(row.exported)
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function edgeFromRow(row) {
|
|
615
|
+
return {
|
|
616
|
+
projectId: String(row.project_id),
|
|
617
|
+
sourceId: String(row.source_id),
|
|
618
|
+
targetId: String(row.target_id),
|
|
619
|
+
kind: String(row.kind),
|
|
620
|
+
metadata: parseMetadata(row.metadata_json)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function chunkFromRow(row) {
|
|
624
|
+
return {
|
|
625
|
+
projectId: String(row.project_id),
|
|
626
|
+
id: String(row.id),
|
|
627
|
+
repoRoot: String(row.repo_root),
|
|
628
|
+
filePath: String(row.file_path),
|
|
629
|
+
language: String(row.language),
|
|
630
|
+
kind: String(row.kind),
|
|
631
|
+
symbolName: row.symbol_name === null ? undefined : String(row.symbol_name),
|
|
632
|
+
startLine: Number(row.start_line),
|
|
633
|
+
endLine: Number(row.end_line),
|
|
634
|
+
content: String(row.content),
|
|
635
|
+
contentHash: String(row.content_hash)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function parseMetadata(value) {
|
|
639
|
+
if (typeof value !== "string")
|
|
640
|
+
return {};
|
|
641
|
+
try {
|
|
642
|
+
const parsed = JSON.parse(value);
|
|
643
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return {};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function ftsQueryForTerms(terms) {
|
|
650
|
+
return [...new Set(terms)]
|
|
651
|
+
.map((part) => part.trim().toLowerCase())
|
|
652
|
+
.filter(Boolean)
|
|
653
|
+
.map((part) => `"${part.replaceAll('"', '""')}"`)
|
|
654
|
+
.join(" OR ");
|
|
655
|
+
}
|
|
656
|
+
function formatRank(rank) {
|
|
657
|
+
return Number.isFinite(rank) ? rank.toFixed(6) : "nan";
|
|
658
|
+
}
|
|
659
|
+
function uniqueSymbols(symbols) {
|
|
660
|
+
return [...new Map(symbols.map((symbol) => [symbol.id, symbol])).values()];
|
|
661
|
+
}
|
|
662
|
+
function groupByPath(items) {
|
|
663
|
+
const grouped = new Map();
|
|
664
|
+
for (const item of items) {
|
|
665
|
+
const current = grouped.get(item.filePath) ?? [];
|
|
666
|
+
current.push(item);
|
|
667
|
+
grouped.set(item.filePath, current);
|
|
668
|
+
}
|
|
669
|
+
return grouped;
|
|
670
|
+
}
|
|
671
|
+
function groupEdgesByPath(edges) {
|
|
672
|
+
const grouped = new Map();
|
|
673
|
+
for (const edge of edges) {
|
|
674
|
+
const filePath = edgeFilePath(edge);
|
|
675
|
+
if (!filePath)
|
|
676
|
+
continue;
|
|
677
|
+
const current = grouped.get(filePath) ?? [];
|
|
678
|
+
current.push(edge);
|
|
679
|
+
grouped.set(filePath, current);
|
|
680
|
+
}
|
|
681
|
+
return grouped;
|
|
682
|
+
}
|
|
683
|
+
function edgeFilePath(edge) {
|
|
684
|
+
return typeof edge.metadata?.sourceFile === "string" ? edge.metadata.sourceFile : null;
|
|
685
|
+
}
|
|
686
|
+
function refreshedOrDeletedFiles(index) {
|
|
687
|
+
return new Set(index.fullReindex ? index.files.map((file) => file.path) : [...(index.refreshedFiles ?? index.changedFiles), ...index.deletedFiles]);
|
|
688
|
+
}
|
|
689
|
+
function isTestFile(filePath) {
|
|
690
|
+
return /(^|\/)(__tests__|tests?)(\/|$)|\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
691
|
+
}
|
|
692
|
+
function filenameTestMatches(files, basename) {
|
|
693
|
+
const tests = new Map();
|
|
694
|
+
for (const file of files) {
|
|
695
|
+
if (isTestFile(file.path) && file.path.toLowerCase().includes(basename.toLowerCase()))
|
|
696
|
+
tests.set(file.path, file);
|
|
697
|
+
}
|
|
698
|
+
return tests;
|
|
699
|
+
}
|
|
700
|
+
function requireRepoRoot(repoRoot) {
|
|
701
|
+
if (!repoRoot)
|
|
702
|
+
throw new Error("Internal error: SQLite graph search requires a resolved repoRoot.");
|
|
703
|
+
return repoRoot;
|
|
704
|
+
}
|
|
705
|
+
function escapeLike(value) {
|
|
706
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
707
|
+
}
|
|
708
|
+
function normalizeRepoRoot(repoRoot) {
|
|
709
|
+
return path.resolve(repoRoot);
|
|
710
|
+
}
|
|
711
|
+
function isTraceEdge(kind) {
|
|
712
|
+
return kind === "calls"
|
|
713
|
+
|| kind === "calls_api"
|
|
714
|
+
|| kind === "routes_to"
|
|
715
|
+
|| kind === "handles_webhook"
|
|
716
|
+
|| kind === "handles_event"
|
|
717
|
+
|| kind === "tested_by"
|
|
718
|
+
|| kind === "uses_middleware"
|
|
719
|
+
|| kind === "reads_from"
|
|
720
|
+
|| kind === "writes_to";
|
|
721
|
+
}
|
|
722
|
+
function matchesTarget(symbol, normalized, target) {
|
|
723
|
+
return matchesImpactTarget(symbol, parseImpactTarget(target || normalized));
|
|
724
|
+
}
|