project-librarian 0.4.0 → 0.4.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/README.ko.md +21 -6
- package/README.md +14 -0
- package/dist/args.js +5 -2
- package/dist/code-index/evidence.js +79 -0
- package/dist/code-index/extractors/config.js +58 -0
- package/dist/code-index/extractors/light-languages.js +52 -0
- package/dist/code-index/extractors/registry.js +137 -0
- package/dist/code-index/extractors/shared.js +36 -0
- package/dist/code-index/extractors/tree-sitter.js +319 -0
- package/dist/code-index/extractors/types.js +2 -0
- package/dist/code-index/extractors/typescript.js +211 -0
- package/dist/code-index/incremental.js +16 -0
- package/dist/code-index/index-health.js +164 -0
- package/dist/code-index/modes.js +303 -0
- package/dist/code-index/ownership.js +183 -0
- package/dist/code-index/reports.js +322 -0
- package/dist/code-index/schema.js +196 -0
- package/dist/code-index/search.js +153 -0
- package/dist/code-index-file-policy.js +21 -27
- package/dist/code-index.js +167 -1850
- package/dist/init-project-wiki.js +38 -40
- package/dist/mcp-server.js +60 -6
- package/dist/migration.js +10 -6
- package/dist/modes.js +73 -42
- package/dist/path-ignore-policy.js +30 -0
- package/dist/wiki-corpus.js +25 -0
- package/dist/wiki-diagnostics.js +19 -0
- package/dist/wiki-files.js +2 -1
- package/dist/wiki-graph.js +1 -2
- package/package.json +9 -3
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.evidenceCoverage = evidenceCoverage;
|
|
37
|
+
exports.workspaceSummary = workspaceSummary;
|
|
38
|
+
exports.workspaceDependencyGraph = workspaceDependencyGraph;
|
|
39
|
+
exports.validCodeReportSections = validCodeReportSections;
|
|
40
|
+
exports.resolveCodeReportSection = resolveCodeReportSection;
|
|
41
|
+
exports.invalidCodeReportSectionMessage = invalidCodeReportSectionMessage;
|
|
42
|
+
exports.codeReportMetadata = codeReportMetadata;
|
|
43
|
+
exports.codeReportForRequestedSection = codeReportForRequestedSection;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const workspace_1 = require("../workspace");
|
|
47
|
+
const ownership_1 = require("./ownership");
|
|
48
|
+
const schema_1 = require("./schema");
|
|
49
|
+
function evidenceCoverage(database) {
|
|
50
|
+
const rows = database.prepare(`
|
|
51
|
+
SELECT 'files' AS table_name, count(*) AS rows FROM files
|
|
52
|
+
UNION ALL SELECT 'symbols', count(*) FROM symbols
|
|
53
|
+
UNION ALL SELECT 'imports', count(*) FROM imports
|
|
54
|
+
UNION ALL SELECT 'routes', count(*) FROM routes
|
|
55
|
+
UNION ALL SELECT 'configs', count(*) FROM configs
|
|
56
|
+
UNION ALL SELECT 'edges', count(*) FROM edges
|
|
57
|
+
`).all();
|
|
58
|
+
return Object.fromEntries(rows.map((row) => [String(row.table_name), Number(row.rows ?? 0)]));
|
|
59
|
+
}
|
|
60
|
+
function incrementOwnerField(owners, context, filePath, field, increment = 1) {
|
|
61
|
+
const info = (0, ownership_1.ownershipInfo)(filePath, context);
|
|
62
|
+
const key = info.owner;
|
|
63
|
+
const current = owners.get(key) ?? {
|
|
64
|
+
bytes: 0,
|
|
65
|
+
codeowners: info.codeowners,
|
|
66
|
+
configs: 0,
|
|
67
|
+
file_count: 0,
|
|
68
|
+
imports: 0,
|
|
69
|
+
languages: "",
|
|
70
|
+
lines: 0,
|
|
71
|
+
owner: key,
|
|
72
|
+
owner_source: info.owner_source,
|
|
73
|
+
routes: 0,
|
|
74
|
+
symbols: 0,
|
|
75
|
+
};
|
|
76
|
+
if (info.codeowners && !current.codeowners.split(", ").includes(info.codeowners))
|
|
77
|
+
current.codeowners = current.codeowners ? `${current.codeowners}; ${info.codeowners}` : info.codeowners;
|
|
78
|
+
current[field] += increment;
|
|
79
|
+
owners.set(key, current);
|
|
80
|
+
}
|
|
81
|
+
function ownershipSummary(database) {
|
|
82
|
+
const files = database.prepare("SELECT path, language, profile, lines, bytes FROM files ORDER BY path").all();
|
|
83
|
+
const context = (0, ownership_1.ownershipContext)();
|
|
84
|
+
const owners = new Map();
|
|
85
|
+
const ownerLanguages = new Map();
|
|
86
|
+
for (const row of files) {
|
|
87
|
+
const filePath = String(row.path);
|
|
88
|
+
const key = (0, ownership_1.ownershipInfo)(filePath, context).owner;
|
|
89
|
+
incrementOwnerField(owners, context, filePath, "file_count");
|
|
90
|
+
incrementOwnerField(owners, context, filePath, "lines", Number(row.lines ?? 0));
|
|
91
|
+
incrementOwnerField(owners, context, filePath, "bytes", Number(row.bytes ?? 0));
|
|
92
|
+
const languages = ownerLanguages.get(key) ?? new Set();
|
|
93
|
+
languages.add(String(row.language));
|
|
94
|
+
ownerLanguages.set(key, languages);
|
|
95
|
+
}
|
|
96
|
+
for (const row of database.prepare("SELECT file_path, count(*) AS count FROM symbols GROUP BY file_path").all())
|
|
97
|
+
incrementOwnerField(owners, context, String(row.file_path), "symbols", Number(row.count ?? 0));
|
|
98
|
+
for (const row of database.prepare("SELECT file_path, count(*) AS count FROM routes GROUP BY file_path").all())
|
|
99
|
+
incrementOwnerField(owners, context, String(row.file_path), "routes", Number(row.count ?? 0));
|
|
100
|
+
for (const row of database.prepare("SELECT from_file, count(*) AS count FROM imports GROUP BY from_file").all())
|
|
101
|
+
incrementOwnerField(owners, context, String(row.from_file), "imports", Number(row.count ?? 0));
|
|
102
|
+
for (const row of database.prepare("SELECT file_path, count(*) AS count FROM configs GROUP BY file_path").all())
|
|
103
|
+
incrementOwnerField(owners, context, String(row.file_path), "configs", Number(row.count ?? 0));
|
|
104
|
+
return Array.from(owners.values()).map((owner) => ({
|
|
105
|
+
...owner,
|
|
106
|
+
languages: Array.from(ownerLanguages.get(String(owner.owner)) ?? []).sort().join(", "),
|
|
107
|
+
})).sort((left, right) => right.file_count - left.file_count || left.owner.localeCompare(right.owner)).slice(0, 25);
|
|
108
|
+
}
|
|
109
|
+
function languageProfileSummary(database) {
|
|
110
|
+
return database.prepare("SELECT language, profile, count(*) AS files, sum(lines) AS lines, sum(bytes) AS bytes FROM files GROUP BY language, profile ORDER BY files DESC, language").all();
|
|
111
|
+
}
|
|
112
|
+
function parserBackendSummary(database, runtime) {
|
|
113
|
+
return database.prepare("SELECT language, profile, count(*) AS files, sum(lines) AS lines, sum(bytes) AS bytes FROM files GROUP BY language, profile ORDER BY files DESC, language").all().map((row) => {
|
|
114
|
+
const profile = String(row.profile);
|
|
115
|
+
const backend = runtime.parserBackendForProfile(profile);
|
|
116
|
+
return {
|
|
117
|
+
language: row.language,
|
|
118
|
+
profile,
|
|
119
|
+
backend: backend.id,
|
|
120
|
+
label: backend.label,
|
|
121
|
+
extraction_strength: backend.strength,
|
|
122
|
+
files: row.files,
|
|
123
|
+
lines: row.lines,
|
|
124
|
+
bytes: row.bytes,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function workspaceSummary(database) {
|
|
129
|
+
const context = (0, ownership_1.ownershipContext)();
|
|
130
|
+
const counts = new Map();
|
|
131
|
+
for (const workspace of context.workspaces) {
|
|
132
|
+
counts.set(workspace.root, { ...workspace, bytes: 0, files: 0, lines: 0 });
|
|
133
|
+
}
|
|
134
|
+
for (const row of database.prepare("SELECT path, lines, bytes FROM files ORDER BY path").all()) {
|
|
135
|
+
const workspace = (0, ownership_1.matchingWorkspace)(String(row.path), context.workspaces);
|
|
136
|
+
if (!workspace)
|
|
137
|
+
continue;
|
|
138
|
+
const current = counts.get(workspace.root) ?? { ...workspace, bytes: 0, files: 0, lines: 0 };
|
|
139
|
+
current.files += 1;
|
|
140
|
+
current.lines += Number(row.lines ?? 0);
|
|
141
|
+
current.bytes += Number(row.bytes ?? 0);
|
|
142
|
+
counts.set(workspace.root, current);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
workspace_packages: Array.from(counts.values()).sort((left, right) => left.root.localeCompare(right.root)),
|
|
146
|
+
codeowners: context.codeownerRules.map((rule) => ({
|
|
147
|
+
file_path: rule.file_path,
|
|
148
|
+
line: rule.line,
|
|
149
|
+
pattern: rule.pattern,
|
|
150
|
+
owners: rule.owners.join(", "),
|
|
151
|
+
})),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function packageManagerFromLockfile(filePath) {
|
|
155
|
+
const base = path.basename(filePath);
|
|
156
|
+
if (base === "package-lock.json" || base === "npm-shrinkwrap.json")
|
|
157
|
+
return "npm";
|
|
158
|
+
if (base === "pnpm-lock.yaml")
|
|
159
|
+
return "pnpm";
|
|
160
|
+
if (base === "yarn.lock")
|
|
161
|
+
return "yarn";
|
|
162
|
+
if (base === "bun.lockb" || base === "bun.lock")
|
|
163
|
+
return "bun";
|
|
164
|
+
return "unknown";
|
|
165
|
+
}
|
|
166
|
+
function workspaceDependencyGraph() {
|
|
167
|
+
const workspaces = (0, ownership_1.workspacePackages)();
|
|
168
|
+
const byName = new Map(workspaces.map((workspace) => [workspace.name, workspace]));
|
|
169
|
+
const lockfiles = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock"]
|
|
170
|
+
.filter((filePath) => fs.existsSync((0, workspace_1.abs)(filePath)))
|
|
171
|
+
.map((filePath) => ({ file_path: filePath, package_manager: packageManagerFromLockfile(filePath), scope: "root" }));
|
|
172
|
+
const workspaceRows = [];
|
|
173
|
+
const internalEdges = [];
|
|
174
|
+
const externalDependencies = new Map();
|
|
175
|
+
for (const workspace of workspaces) {
|
|
176
|
+
const packageJsonPath = (0, workspace_1.normalizePath)(path.join(workspace.root, "package.json"));
|
|
177
|
+
const packageJson = (0, ownership_1.readJsonObject)(packageJsonPath);
|
|
178
|
+
const dependencyFields = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
179
|
+
const dependencyCounts = {};
|
|
180
|
+
const workspaceInternalEdges = [];
|
|
181
|
+
for (const field of dependencyFields) {
|
|
182
|
+
const dependencies = packageJson?.[field];
|
|
183
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies))
|
|
184
|
+
continue;
|
|
185
|
+
for (const [dependencyName, version] of Object.entries(dependencies)) {
|
|
186
|
+
dependencyCounts[field] = (dependencyCounts[field] ?? 0) + 1;
|
|
187
|
+
const target = byName.get(dependencyName);
|
|
188
|
+
if (target) {
|
|
189
|
+
const edge = {
|
|
190
|
+
from_workspace: workspace.root,
|
|
191
|
+
from_package: workspace.name,
|
|
192
|
+
to_workspace: target.root,
|
|
193
|
+
to_package: target.name,
|
|
194
|
+
dependency_type: field,
|
|
195
|
+
version: typeof version === "string" ? version : String(version),
|
|
196
|
+
};
|
|
197
|
+
internalEdges.push(edge);
|
|
198
|
+
workspaceInternalEdges.push(edge);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const key = `${dependencyName}\0${field}`;
|
|
202
|
+
const current = externalDependencies.get(key) ?? { dependency: dependencyName, dependency_type: field, workspaces: new Set() };
|
|
203
|
+
current.workspaces.add(workspace.root);
|
|
204
|
+
externalDependencies.set(key, current);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const lockfileName of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock"]) {
|
|
209
|
+
const lockfilePath = (0, workspace_1.normalizePath)(path.join(workspace.root, lockfileName));
|
|
210
|
+
if (fs.existsSync((0, workspace_1.abs)(lockfilePath))) {
|
|
211
|
+
lockfiles.push({ file_path: lockfilePath, package_manager: packageManagerFromLockfile(lockfilePath), scope: workspace.root });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
workspaceRows.push({
|
|
215
|
+
name: workspace.name,
|
|
216
|
+
root: workspace.root,
|
|
217
|
+
dependency_counts: dependencyCounts,
|
|
218
|
+
internal_dependency_count: workspaceInternalEdges.length,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
workspace_count: workspaces.length,
|
|
223
|
+
package_managers: Array.from(new Set(lockfiles.map((lockfile) => lockfile.package_manager))).sort(),
|
|
224
|
+
lockfiles,
|
|
225
|
+
workspaces: workspaceRows.sort((left, right) => String(left.root).localeCompare(String(right.root))),
|
|
226
|
+
internal_dependencies: internalEdges.sort((left, right) => String(left.from_workspace).localeCompare(String(right.from_workspace)) || String(left.to_workspace).localeCompare(String(right.to_workspace))),
|
|
227
|
+
external_dependency_hotspots: Array.from(externalDependencies.values()).map((entry) => ({
|
|
228
|
+
dependency: entry.dependency,
|
|
229
|
+
dependency_type: entry.dependency_type,
|
|
230
|
+
workspace_count: entry.workspaces.size,
|
|
231
|
+
workspaces: Array.from(entry.workspaces).sort().join(", "),
|
|
232
|
+
})).sort((left, right) => right.workspace_count - left.workspace_count || left.dependency.localeCompare(right.dependency)).slice(0, 100),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function routeInventory(database) {
|
|
236
|
+
return database.prepare("SELECT method, route, file_path, line, handler FROM routes ORDER BY file_path, line LIMIT 100").all();
|
|
237
|
+
}
|
|
238
|
+
function dependencyHotspots(database) {
|
|
239
|
+
return {
|
|
240
|
+
imports: database.prepare("SELECT to_ref, count(DISTINCT from_file) AS importing_files, count(*) AS reference_count FROM imports GROUP BY to_ref ORDER BY importing_files DESC, reference_count DESC, to_ref LIMIT 50").all(),
|
|
241
|
+
package_dependencies: database.prepare("SELECT substr(key, 12) AS package, value AS version, file_path FROM configs WHERE key LIKE 'dependency:%' ORDER BY file_path, package LIMIT 100").all(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function configInventory(database) {
|
|
245
|
+
return database.prepare("SELECT key, value, file_path, line FROM configs WHERE key LIKE 'script:%' OR key LIKE 'dependency:%' OR key LIKE 'devDependency:%' ORDER BY file_path, key LIMIT 150").all();
|
|
246
|
+
}
|
|
247
|
+
function edgeSummary(database) {
|
|
248
|
+
return {
|
|
249
|
+
by_kind: database.prepare("SELECT kind, count(*) AS edges FROM edges GROUP BY kind ORDER BY edges DESC, kind").all(),
|
|
250
|
+
fanout: database.prepare("SELECT source_kind, source, kind, count(DISTINCT target) AS targets, file_path FROM edges GROUP BY source_kind, source, kind, file_path ORDER BY targets DESC, source LIMIT 50").all(),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const reportSectionDefinitions = [
|
|
254
|
+
{ key: "coverage", outputKey: "evidence_coverage", aliases: ["coverage", "evidence", "evidence_coverage"], render: (database) => evidenceCoverage(database) },
|
|
255
|
+
{ key: "ownership", outputKey: "ownership_summary", aliases: ["ownership", "ownership_summary"], render: (database) => ownershipSummary(database) },
|
|
256
|
+
{ key: "languages", outputKey: "language_profile_summary", aliases: ["language", "language_profile_summary", "languages"], render: (database) => languageProfileSummary(database) },
|
|
257
|
+
{ key: "parsers", outputKey: "parser_backend_summary", aliases: ["parser", "parser_backend_summary", "parser_backends", "parsers"], render: (database, runtime) => parserBackendSummary(database, runtime) },
|
|
258
|
+
{ key: "workspaces", outputKey: "workspace_summary", aliases: ["workspace", "workspace_summary", "workspaces"], render: (database) => workspaceSummary(database) },
|
|
259
|
+
{ key: "workspace-graph", outputKey: "workspace_dependency_graph", aliases: ["workspace_graph", "workspace-graph", "workspacegraph", "monorepo", "monorepo_graph"], render: () => workspaceDependencyGraph() },
|
|
260
|
+
{ key: "routes", outputKey: "route_inventory", aliases: ["route", "route_inventory", "routes"], render: (database) => routeInventory(database) },
|
|
261
|
+
{ key: "hotspots", outputKey: "dependency_hotspots", aliases: ["dependencies", "dependency", "dependency_hotspots", "hotspot", "hotspots"], render: (database) => dependencyHotspots(database) },
|
|
262
|
+
{ key: "configs", outputKey: "config_inventory", aliases: ["config", "configs"], render: (database) => configInventory(database) },
|
|
263
|
+
{ key: "edges", outputKey: "edge_summary", aliases: ["edge", "edge_summary", "edges"], render: (database) => edgeSummary(database) },
|
|
264
|
+
];
|
|
265
|
+
const reportSectionByKey = new Map(reportSectionDefinitions.map((section) => [section.key, section]));
|
|
266
|
+
const reportSectionByAlias = new Map(reportSectionDefinitions.flatMap((section) => section.aliases.map((alias) => [alias, section])));
|
|
267
|
+
function validCodeReportSections() {
|
|
268
|
+
return reportSectionDefinitions.map((section) => section.key);
|
|
269
|
+
}
|
|
270
|
+
function resolveCodeReportSection(requestedSection) {
|
|
271
|
+
const requested = requestedSection.trim().toLowerCase();
|
|
272
|
+
if (!requested || requested === "all" || requested === "full")
|
|
273
|
+
return "";
|
|
274
|
+
return reportSectionByAlias.get(requested)?.key;
|
|
275
|
+
}
|
|
276
|
+
function invalidCodeReportSectionMessage(requestedSection) {
|
|
277
|
+
return `invalid --code-report-section: ${requestedSection}; expected one of: ${validCodeReportSections().join(", ")}`;
|
|
278
|
+
}
|
|
279
|
+
function codeReportMetadata(database, runtime) {
|
|
280
|
+
const staleness = runtime.staleness;
|
|
281
|
+
return {
|
|
282
|
+
schema_version: 1,
|
|
283
|
+
generated_at: new Date().toISOString(),
|
|
284
|
+
database: runtime.databaseRelativePath,
|
|
285
|
+
scopes: (0, schema_1.indexedScopes)(database),
|
|
286
|
+
parser_mode: (0, schema_1.indexedParserMode)(database),
|
|
287
|
+
stale: {
|
|
288
|
+
files: staleness.added + staleness.changed + staleness.deleted,
|
|
289
|
+
changed: staleness.changed,
|
|
290
|
+
added: staleness.added,
|
|
291
|
+
deleted: staleness.deleted,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function codeReportSectionData(database, section, runtime) {
|
|
296
|
+
const definition = reportSectionByKey.get(section);
|
|
297
|
+
if (!definition)
|
|
298
|
+
return undefined;
|
|
299
|
+
return definition.render(database, runtime);
|
|
300
|
+
}
|
|
301
|
+
function codeReport(database, runtime) {
|
|
302
|
+
const report = {
|
|
303
|
+
...codeReportMetadata(database, runtime),
|
|
304
|
+
report_sections: reportSectionDefinitions.map((section) => section.outputKey),
|
|
305
|
+
};
|
|
306
|
+
for (const section of reportSectionDefinitions) {
|
|
307
|
+
report[section.outputKey] = section.render(database, runtime);
|
|
308
|
+
}
|
|
309
|
+
return report;
|
|
310
|
+
}
|
|
311
|
+
function codeReportForRequestedSection(database, requestedSection, runtime) {
|
|
312
|
+
const section = resolveCodeReportSection(requestedSection);
|
|
313
|
+
if (section === undefined)
|
|
314
|
+
return undefined;
|
|
315
|
+
if (!section)
|
|
316
|
+
return codeReport(database, runtime);
|
|
317
|
+
return {
|
|
318
|
+
...codeReportMetadata(database, runtime),
|
|
319
|
+
section,
|
|
320
|
+
data: codeReportSectionData(database, section, runtime),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.codeIndexSchemaVersion = void 0;
|
|
4
|
+
exports.setupDatabase = setupDatabase;
|
|
5
|
+
exports.createIndexStatements = createIndexStatements;
|
|
6
|
+
exports.removeIndexedFile = removeIndexedFile;
|
|
7
|
+
exports.writeIndexMetadata = writeIndexMetadata;
|
|
8
|
+
exports.readMetaValue = readMetaValue;
|
|
9
|
+
exports.indexedScopes = indexedScopes;
|
|
10
|
+
exports.indexedParserMode = indexedParserMode;
|
|
11
|
+
exports.incrementalCompatibility = incrementalCompatibility;
|
|
12
|
+
exports.codeIndexSnapshot = codeIndexSnapshot;
|
|
13
|
+
const workspace_1 = require("../workspace");
|
|
14
|
+
exports.codeIndexSchemaVersion = "4";
|
|
15
|
+
function setupDatabase(database) {
|
|
16
|
+
database.exec(`
|
|
17
|
+
PRAGMA journal_mode = WAL;
|
|
18
|
+
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
19
|
+
CREATE TABLE files (
|
|
20
|
+
path TEXT PRIMARY KEY,
|
|
21
|
+
language TEXT NOT NULL,
|
|
22
|
+
profile TEXT NOT NULL,
|
|
23
|
+
kind TEXT NOT NULL,
|
|
24
|
+
bytes INTEGER NOT NULL,
|
|
25
|
+
lines INTEGER NOT NULL,
|
|
26
|
+
hash TEXT NOT NULL,
|
|
27
|
+
mtime_ms REAL NOT NULL,
|
|
28
|
+
size INTEGER NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE TABLE symbols (
|
|
31
|
+
id INTEGER PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
kind TEXT NOT NULL,
|
|
34
|
+
file_path TEXT NOT NULL,
|
|
35
|
+
line INTEGER NOT NULL,
|
|
36
|
+
signature TEXT NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE imports (
|
|
39
|
+
id INTEGER PRIMARY KEY,
|
|
40
|
+
from_file TEXT NOT NULL,
|
|
41
|
+
to_ref TEXT NOT NULL,
|
|
42
|
+
imported TEXT NOT NULL,
|
|
43
|
+
line INTEGER NOT NULL,
|
|
44
|
+
raw TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
CREATE TABLE routes (
|
|
47
|
+
id INTEGER PRIMARY KEY,
|
|
48
|
+
method TEXT NOT NULL,
|
|
49
|
+
route TEXT NOT NULL,
|
|
50
|
+
file_path TEXT NOT NULL,
|
|
51
|
+
line INTEGER NOT NULL,
|
|
52
|
+
handler TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
CREATE TABLE configs (
|
|
55
|
+
id INTEGER PRIMARY KEY,
|
|
56
|
+
key TEXT NOT NULL,
|
|
57
|
+
value TEXT NOT NULL,
|
|
58
|
+
file_path TEXT NOT NULL,
|
|
59
|
+
line INTEGER NOT NULL
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE edges (
|
|
62
|
+
id INTEGER PRIMARY KEY,
|
|
63
|
+
kind TEXT NOT NULL,
|
|
64
|
+
source_kind TEXT NOT NULL,
|
|
65
|
+
source TEXT NOT NULL,
|
|
66
|
+
target_kind TEXT NOT NULL,
|
|
67
|
+
target TEXT NOT NULL,
|
|
68
|
+
file_path TEXT NOT NULL,
|
|
69
|
+
line INTEGER NOT NULL,
|
|
70
|
+
evidence TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
CREATE VIRTUAL TABLE files_fts USING fts5(path, language, profile, content);
|
|
73
|
+
CREATE VIRTUAL TABLE symbols_fts USING fts5(name, kind, file_path, signature);
|
|
74
|
+
CREATE INDEX idx_symbols_file ON symbols(file_path);
|
|
75
|
+
CREATE INDEX idx_symbols_name ON symbols(name);
|
|
76
|
+
CREATE INDEX idx_imports_from ON imports(from_file);
|
|
77
|
+
CREATE INDEX idx_routes_path ON routes(route);
|
|
78
|
+
CREATE INDEX idx_configs_file ON configs(file_path);
|
|
79
|
+
CREATE INDEX idx_edges_source ON edges(source_kind, source);
|
|
80
|
+
CREATE INDEX idx_edges_target ON edges(target_kind, target);
|
|
81
|
+
CREATE INDEX idx_edges_kind ON edges(kind);
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
function createIndexStatements(database) {
|
|
85
|
+
return {
|
|
86
|
+
deleteConfig: database.prepare("DELETE FROM configs WHERE file_path = ?"),
|
|
87
|
+
deleteEdge: database.prepare("DELETE FROM edges WHERE file_path = ?"),
|
|
88
|
+
deleteFile: database.prepare("DELETE FROM files WHERE path = ?"),
|
|
89
|
+
deleteFileFts: database.prepare("DELETE FROM files_fts WHERE path = ?"),
|
|
90
|
+
deleteImport: database.prepare("DELETE FROM imports WHERE from_file = ?"),
|
|
91
|
+
deleteRoute: database.prepare("DELETE FROM routes WHERE file_path = ?"),
|
|
92
|
+
deleteSymbol: database.prepare("DELETE FROM symbols WHERE file_path = ?"),
|
|
93
|
+
deleteSymbolFts: database.prepare("DELETE FROM symbols_fts WHERE file_path = ?"),
|
|
94
|
+
insertConfig: database.prepare("INSERT INTO configs (key, value, file_path, line) VALUES (?, ?, ?, ?)"),
|
|
95
|
+
insertEdge: database.prepare("INSERT INTO edges (kind, source_kind, source, target_kind, target, file_path, line, evidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
96
|
+
insertFile: database.prepare("INSERT INTO files (path, language, profile, kind, bytes, lines, hash, mtime_ms, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
97
|
+
insertFileFts: database.prepare("INSERT INTO files_fts (path, language, profile, content) VALUES (?, ?, ?, ?)"),
|
|
98
|
+
insertImport: database.prepare("INSERT INTO imports (from_file, to_ref, imported, line, raw) VALUES (?, ?, ?, ?, ?)"),
|
|
99
|
+
insertMeta: database.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),
|
|
100
|
+
insertRoute: database.prepare("INSERT INTO routes (method, route, file_path, line, handler) VALUES (?, ?, ?, ?, ?)"),
|
|
101
|
+
insertSymbol: database.prepare("INSERT INTO symbols (name, kind, file_path, line, signature) VALUES (?, ?, ?, ?, ?)"),
|
|
102
|
+
insertSymbolFts: database.prepare("INSERT INTO symbols_fts (name, kind, file_path, signature) VALUES (?, ?, ?, ?)"),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function removeIndexedFile(filePath, statements) {
|
|
106
|
+
statements.deleteConfig.run(filePath);
|
|
107
|
+
statements.deleteEdge.run(filePath);
|
|
108
|
+
statements.deleteImport.run(filePath);
|
|
109
|
+
statements.deleteRoute.run(filePath);
|
|
110
|
+
statements.deleteSymbol.run(filePath);
|
|
111
|
+
statements.deleteSymbolFts.run(filePath);
|
|
112
|
+
statements.deleteFileFts.run(filePath);
|
|
113
|
+
statements.deleteFile.run(filePath);
|
|
114
|
+
}
|
|
115
|
+
function writeIndexMetadata(scopes, parserMode, statements) {
|
|
116
|
+
statements.insertMeta.run("schema_version", exports.codeIndexSchemaVersion);
|
|
117
|
+
statements.insertMeta.run("updated_at", new Date().toISOString());
|
|
118
|
+
statements.insertMeta.run("root", workspace_1.root);
|
|
119
|
+
statements.insertMeta.run("scopes", scopes.join(", "));
|
|
120
|
+
statements.insertMeta.run("scopes_json", JSON.stringify(scopes));
|
|
121
|
+
statements.insertMeta.run("parser_mode", parserMode);
|
|
122
|
+
statements.insertMeta.run("terminology", "code evidence index");
|
|
123
|
+
}
|
|
124
|
+
function readMetaValue(database, key) {
|
|
125
|
+
const rows = database.prepare("SELECT value FROM meta WHERE key = ?").all(key);
|
|
126
|
+
const value = rows[0]?.value;
|
|
127
|
+
return typeof value === "string" ? value : "";
|
|
128
|
+
}
|
|
129
|
+
function indexedScopes(database) {
|
|
130
|
+
const scopesJson = readMetaValue(database, "scopes_json");
|
|
131
|
+
if (scopesJson) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(scopesJson);
|
|
134
|
+
if (Array.isArray(parsed) && parsed.every((scope) => typeof scope === "string"))
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Fall back to the legacy comma-separated scope metadata below.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return readMetaValue(database, "scopes")
|
|
142
|
+
.split(",")
|
|
143
|
+
.map((scope) => scope.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
function indexedParserMode(database) {
|
|
147
|
+
const mode = readMetaValue(database, "parser_mode");
|
|
148
|
+
return mode === "tree-sitter" ? "tree-sitter" : "default";
|
|
149
|
+
}
|
|
150
|
+
function scopesMatch(left, right) {
|
|
151
|
+
return left.length === right.length && left.every((scope, index) => scope === right[index]);
|
|
152
|
+
}
|
|
153
|
+
function incrementalCompatibility(database, scopes, parserMode) {
|
|
154
|
+
const existingSchemaVersion = readMetaValue(database, "schema_version");
|
|
155
|
+
if (existingSchemaVersion !== exports.codeIndexSchemaVersion) {
|
|
156
|
+
return {
|
|
157
|
+
compatible: false,
|
|
158
|
+
reason: `existing schema version ${existingSchemaVersion || "(missing)"} does not match ${exports.codeIndexSchemaVersion}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const existingScopes = indexedScopes(database);
|
|
162
|
+
if (!scopesMatch(existingScopes, scopes)) {
|
|
163
|
+
return {
|
|
164
|
+
compatible: false,
|
|
165
|
+
reason: `indexed scopes do not match requested scopes: indexed [${existingScopes.join(", ")}], requested [${scopes.join(", ")}]`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const existingParserMode = indexedParserMode(database);
|
|
169
|
+
if (existingParserMode !== parserMode) {
|
|
170
|
+
return {
|
|
171
|
+
compatible: false,
|
|
172
|
+
reason: `indexed parser mode ${existingParserMode} does not match requested parser mode ${parserMode}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { compatible: true, reason: "" };
|
|
176
|
+
}
|
|
177
|
+
function snapshotRows(database, sql) {
|
|
178
|
+
return database.prepare(sql).all().map((row) => {
|
|
179
|
+
const normalized = {};
|
|
180
|
+
for (const key of Object.keys(row).sort()) {
|
|
181
|
+
const value = row[key];
|
|
182
|
+
normalized[key] = typeof value === "string" || typeof value === "number" || value === null ? value : String(value);
|
|
183
|
+
}
|
|
184
|
+
return normalized;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function codeIndexSnapshot(database) {
|
|
188
|
+
return {
|
|
189
|
+
configs: snapshotRows(database, "SELECT file_path, line, key, value FROM configs ORDER BY file_path, line, key, value"),
|
|
190
|
+
edges: snapshotRows(database, "SELECT file_path, line, kind, source_kind, source, target_kind, target, evidence FROM edges ORDER BY file_path, line, kind, source, target, evidence"),
|
|
191
|
+
files: snapshotRows(database, "SELECT path, language, profile, kind, lines, bytes FROM files ORDER BY path"),
|
|
192
|
+
imports: snapshotRows(database, "SELECT from_file, line, to_ref, imported, raw FROM imports ORDER BY from_file, line, to_ref, imported, raw"),
|
|
193
|
+
routes: snapshotRows(database, "SELECT file_path, line, method, route, handler FROM routes ORDER BY file_path, line, method, route, handler"),
|
|
194
|
+
symbols: snapshotRows(database, "SELECT file_path, line, kind, name, signature FROM symbols ORDER BY file_path, line, kind, name, signature"),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.containsLikePattern = containsLikePattern;
|
|
4
|
+
exports.shouldUseFtsSearchForScale = shouldUseFtsSearchForScale;
|
|
5
|
+
exports.searchFiles = searchFiles;
|
|
6
|
+
exports.searchSymbols = searchSymbols;
|
|
7
|
+
const code_index_file_policy_1 = require("../code-index-file-policy");
|
|
8
|
+
function escapeLikeTerm(term) {
|
|
9
|
+
return term.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
10
|
+
}
|
|
11
|
+
function containsLikePattern(term) {
|
|
12
|
+
return `%${escapeLikeTerm(term)}%`;
|
|
13
|
+
}
|
|
14
|
+
function prefixLikePattern(term) {
|
|
15
|
+
return `${escapeLikeTerm(term)}%`;
|
|
16
|
+
}
|
|
17
|
+
function ftsTokens(term) {
|
|
18
|
+
return Array.from(new Set(term.match(/[\p{L}\p{N}_]+/gu) ?? []));
|
|
19
|
+
}
|
|
20
|
+
function ftsPrefixQuery(term) {
|
|
21
|
+
const tokens = ftsTokens(term);
|
|
22
|
+
return tokens.slice(0, 8).map((token) => `"${token.replace(/"/g, "\"\"")}"*`).join(" AND ");
|
|
23
|
+
}
|
|
24
|
+
function indexedFileCount(database) {
|
|
25
|
+
const row = database.prepare("SELECT count(*) AS count FROM files").all()[0] ?? {};
|
|
26
|
+
return Number(row.count ?? 0);
|
|
27
|
+
}
|
|
28
|
+
function shouldUseFtsSearchForScale(term, fileCount) {
|
|
29
|
+
const tokens = Array.from(new Set(term.match(/[\p{L}\p{N}_]+/gu) ?? []));
|
|
30
|
+
if (tokens.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
if (tokens.length > 1)
|
|
33
|
+
return true;
|
|
34
|
+
return fileCount >= code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD;
|
|
35
|
+
}
|
|
36
|
+
function shouldUseFtsSearch(database, term) {
|
|
37
|
+
const tokens = ftsTokens(term);
|
|
38
|
+
if (tokens.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
if (tokens.length > 1)
|
|
41
|
+
return true;
|
|
42
|
+
return indexedFileCount(database) >= code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD;
|
|
43
|
+
}
|
|
44
|
+
function stringValue(row, key) {
|
|
45
|
+
const value = row[key];
|
|
46
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
47
|
+
}
|
|
48
|
+
function addRankedRow(rowsByKey, row, key, score) {
|
|
49
|
+
const current = rowsByKey.get(key);
|
|
50
|
+
if (!current || score > current.score)
|
|
51
|
+
rowsByKey.set(key, { row, score });
|
|
52
|
+
}
|
|
53
|
+
function rankedRows(rowsByKey, limit, stableKeys) {
|
|
54
|
+
return Array.from(rowsByKey.values())
|
|
55
|
+
.sort((left, right) => {
|
|
56
|
+
const scoreDelta = right.score - left.score;
|
|
57
|
+
if (scoreDelta !== 0)
|
|
58
|
+
return scoreDelta;
|
|
59
|
+
for (const key of stableKeys) {
|
|
60
|
+
const compared = stringValue(left.row, key).localeCompare(stringValue(right.row, key));
|
|
61
|
+
if (compared !== 0)
|
|
62
|
+
return compared;
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
})
|
|
66
|
+
.slice(0, limit)
|
|
67
|
+
.map((ranked) => ranked.row);
|
|
68
|
+
}
|
|
69
|
+
function searchFiles(database, term, limit = 25) {
|
|
70
|
+
const normalized = term.trim();
|
|
71
|
+
if (!normalized)
|
|
72
|
+
return [];
|
|
73
|
+
const contains = containsLikePattern(normalized);
|
|
74
|
+
const prefix = prefixLikePattern(normalized);
|
|
75
|
+
const rowsByKey = new Map();
|
|
76
|
+
const exactRows = database.prepare("SELECT path, language, profile, lines, bytes FROM files WHERE path = ? ORDER BY path LIMIT ?").all(normalized, limit);
|
|
77
|
+
exactRows.forEach((row) => addRankedRow(rowsByKey, row, stringValue(row, "path"), 900));
|
|
78
|
+
const prefixRows = database.prepare("SELECT path, language, profile, lines, bytes FROM files WHERE path LIKE ? ESCAPE '\\' ORDER BY path LIMIT ?").all(prefix, limit);
|
|
79
|
+
prefixRows.forEach((row) => addRankedRow(rowsByKey, row, stringValue(row, "path"), 750));
|
|
80
|
+
const ftsQuery = shouldUseFtsSearch(database, normalized) ? ftsPrefixQuery(normalized) : "";
|
|
81
|
+
if (ftsQuery) {
|
|
82
|
+
const ftsRows = database.prepare(`
|
|
83
|
+
SELECT files.path, files.language, files.profile, files.lines, files.bytes
|
|
84
|
+
FROM files_fts
|
|
85
|
+
JOIN files ON files.path = files_fts.path
|
|
86
|
+
WHERE files_fts MATCH ?
|
|
87
|
+
ORDER BY bm25(files_fts, 8.0, 1.0, 1.0, 0.25), files.path
|
|
88
|
+
LIMIT ?
|
|
89
|
+
`).all(ftsQuery, limit);
|
|
90
|
+
ftsRows.forEach((row, index) => addRankedRow(rowsByKey, row, stringValue(row, "path"), 650 - index));
|
|
91
|
+
}
|
|
92
|
+
const containsRows = database.prepare("SELECT path, language, profile, lines, bytes FROM files WHERE path LIKE ? ESCAPE '\\' ORDER BY path LIMIT ?").all(contains, limit);
|
|
93
|
+
containsRows.forEach((row) => addRankedRow(rowsByKey, row, stringValue(row, "path"), 500));
|
|
94
|
+
return rankedRows(rowsByKey, limit, ["path"]);
|
|
95
|
+
}
|
|
96
|
+
function symbolKey(row) {
|
|
97
|
+
return [
|
|
98
|
+
stringValue(row, "file_path"),
|
|
99
|
+
stringValue(row, "line"),
|
|
100
|
+
stringValue(row, "kind"),
|
|
101
|
+
stringValue(row, "name"),
|
|
102
|
+
stringValue(row, "signature"),
|
|
103
|
+
].join("\u0000");
|
|
104
|
+
}
|
|
105
|
+
function searchSymbols(database, term, limit = 50) {
|
|
106
|
+
const normalized = term.trim();
|
|
107
|
+
if (!normalized)
|
|
108
|
+
return [];
|
|
109
|
+
const contains = containsLikePattern(normalized);
|
|
110
|
+
const prefix = prefixLikePattern(normalized);
|
|
111
|
+
const rowsByKey = new Map();
|
|
112
|
+
const exactRows = database.prepare(`
|
|
113
|
+
SELECT name, kind, file_path, line, signature
|
|
114
|
+
FROM symbols
|
|
115
|
+
WHERE name = ? OR signature = ?
|
|
116
|
+
ORDER BY file_path, line
|
|
117
|
+
LIMIT ?
|
|
118
|
+
`).all(normalized, normalized, limit);
|
|
119
|
+
exactRows.forEach((row) => addRankedRow(rowsByKey, row, symbolKey(row), 1000));
|
|
120
|
+
const prefixRows = database.prepare(`
|
|
121
|
+
SELECT name, kind, file_path, line, signature
|
|
122
|
+
FROM symbols
|
|
123
|
+
WHERE name LIKE ? ESCAPE '\\' OR signature LIKE ? ESCAPE '\\'
|
|
124
|
+
ORDER BY file_path, line
|
|
125
|
+
LIMIT ?
|
|
126
|
+
`).all(prefix, prefix, limit);
|
|
127
|
+
prefixRows.forEach((row) => addRankedRow(rowsByKey, row, symbolKey(row), 850));
|
|
128
|
+
const ftsQuery = shouldUseFtsSearch(database, normalized) ? ftsPrefixQuery(normalized) : "";
|
|
129
|
+
if (ftsQuery) {
|
|
130
|
+
const ftsRows = database.prepare(`
|
|
131
|
+
SELECT symbols.name, symbols.kind, symbols.file_path, symbols.line, symbols.signature
|
|
132
|
+
FROM symbols_fts
|
|
133
|
+
JOIN symbols
|
|
134
|
+
ON symbols.name = symbols_fts.name
|
|
135
|
+
AND symbols.kind = symbols_fts.kind
|
|
136
|
+
AND symbols.file_path = symbols_fts.file_path
|
|
137
|
+
AND symbols.signature = symbols_fts.signature
|
|
138
|
+
WHERE symbols_fts MATCH ?
|
|
139
|
+
ORDER BY bm25(symbols_fts, 8.0, 1.0, 4.0, 2.0), symbols.file_path, symbols.line
|
|
140
|
+
LIMIT ?
|
|
141
|
+
`).all(ftsQuery, limit);
|
|
142
|
+
ftsRows.forEach((row, index) => addRankedRow(rowsByKey, row, symbolKey(row), 700 - index));
|
|
143
|
+
}
|
|
144
|
+
const containsRows = database.prepare(`
|
|
145
|
+
SELECT name, kind, file_path, line, signature
|
|
146
|
+
FROM symbols
|
|
147
|
+
WHERE name LIKE ? ESCAPE '\\' OR signature LIKE ? ESCAPE '\\' OR file_path LIKE ? ESCAPE '\\'
|
|
148
|
+
ORDER BY file_path, line
|
|
149
|
+
LIMIT ?
|
|
150
|
+
`).all(contains, contains, contains, limit);
|
|
151
|
+
containsRows.forEach((row) => addRankedRow(rowsByKey, row, symbolKey(row), 500));
|
|
152
|
+
return rankedRows(rowsByKey, limit, ["file_path", "line", "kind", "name", "signature"]);
|
|
153
|
+
}
|