kirograph 0.15.0 → 0.16.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 +112 -1
- package/dist/bin/commands/docs.js +357 -0
- package/dist/bin/commands/docs.js.map +7 -0
- package/dist/bin/installer/config-prompt.js +17 -1
- package/dist/bin/installer/config-prompt.js.map +2 -2
- package/dist/bin/installer/index.js +6 -1
- package/dist/bin/installer/index.js.map +2 -2
- package/dist/bin/installer/steering.js +21 -0
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/installer/targets/index.js.map +1 -1
- package/dist/bin/installer/targets/kiro.js +2 -2
- package/dist/bin/installer/targets/kiro.js.map +2 -2
- package/dist/bin/kirograph.js +3 -1
- package/dist/bin/kirograph.js.map +3 -3
- package/dist/compression/naive-cost.js +19 -0
- package/dist/compression/naive-cost.js.map +2 -2
- package/dist/compression/tracker.js +11 -2
- package/dist/compression/tracker.js.map +2 -2
- package/dist/compression/types.js.map +1 -1
- package/dist/config.js +34 -1
- package/dist/config.js.map +2 -2
- package/dist/core/pipeline.js +23 -0
- package/dist/core/pipeline.js.map +2 -2
- package/dist/db/database.js +11 -0
- package/dist/db/database.js.map +2 -2
- package/dist/db/docs-schema.sql +50 -0
- package/dist/docs/formats/asciidoc.js +108 -0
- package/dist/docs/formats/asciidoc.js.map +7 -0
- package/dist/docs/formats/html.js +100 -0
- package/dist/docs/formats/html.js.map +7 -0
- package/dist/docs/formats/index.js +81 -0
- package/dist/docs/formats/index.js.map +7 -0
- package/dist/docs/formats/markdown.js +146 -0
- package/dist/docs/formats/markdown.js.map +7 -0
- package/dist/docs/formats/openapi.js +228 -0
- package/dist/docs/formats/openapi.js.map +7 -0
- package/dist/docs/formats/org.js +117 -0
- package/dist/docs/formats/org.js.map +7 -0
- package/dist/docs/formats/plaintext.js +119 -0
- package/dist/docs/formats/plaintext.js.map +7 -0
- package/dist/docs/formats/rdoc.js +105 -0
- package/dist/docs/formats/rdoc.js.map +7 -0
- package/dist/docs/formats/rst.js +121 -0
- package/dist/docs/formats/rst.js.map +7 -0
- package/dist/docs/indexer.js +340 -0
- package/dist/docs/indexer.js.map +7 -0
- package/dist/docs/linker.js +151 -0
- package/dist/docs/linker.js.map +7 -0
- package/dist/docs/lint.js +92 -0
- package/dist/docs/lint.js.map +7 -0
- package/dist/docs/queries.js +295 -0
- package/dist/docs/queries.js.map +7 -0
- package/dist/docs/section-id.js +57 -0
- package/dist/docs/section-id.js.map +7 -0
- package/dist/docs/summarizer.js +55 -0
- package/dist/docs/summarizer.js.map +7 -0
- package/dist/docs/types.js +17 -0
- package/dist/docs/types.js.map +7 -0
- package/dist/docs/vectors.js +217 -0
- package/dist/docs/vectors.js.map +7 -0
- package/dist/mcp/tool-names.js +6 -1
- package/dist/mcp/tool-names.js.map +2 -2
- package/dist/mcp/tools.js +245 -1
- package/dist/mcp/tools.js.map +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var indexer_exports = {};
|
|
30
|
+
__export(indexer_exports, {
|
|
31
|
+
DocsIndexer: () => DocsIndexer
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(indexer_exports);
|
|
34
|
+
var fs = __toESM(require("fs"));
|
|
35
|
+
var path = __toESM(require("path"));
|
|
36
|
+
var crypto = __toESM(require("crypto"));
|
|
37
|
+
var import_picomatch = __toESM(require("picomatch"));
|
|
38
|
+
var import_section_id = require("./section-id");
|
|
39
|
+
var import_formats = require("./formats/index");
|
|
40
|
+
var import_linker = require("./linker");
|
|
41
|
+
var import_vectors = require("./vectors");
|
|
42
|
+
class DocsIndexer {
|
|
43
|
+
constructor(db, config, projectRoot) {
|
|
44
|
+
this.db = db;
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.projectRoot = projectRoot;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Index all documentation files in the project.
|
|
50
|
+
* Skips files whose content hash hasn't changed (incremental).
|
|
51
|
+
*/
|
|
52
|
+
async indexAll(opts) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const result = {
|
|
55
|
+
filesIndexed: 0,
|
|
56
|
+
sectionsCreated: 0,
|
|
57
|
+
sectionsUpdated: 0,
|
|
58
|
+
sectionsRemoved: 0,
|
|
59
|
+
codeRefsCreated: 0,
|
|
60
|
+
errors: [],
|
|
61
|
+
duration: 0
|
|
62
|
+
};
|
|
63
|
+
const files = this.scanDocFiles();
|
|
64
|
+
opts?.onProgress?.(`docs: found ${files.length} documentation files`);
|
|
65
|
+
for (const relPath of files) {
|
|
66
|
+
try {
|
|
67
|
+
const changed = await this.indexFile(relPath, opts?.force ?? false);
|
|
68
|
+
if (changed) {
|
|
69
|
+
result.filesIndexed++;
|
|
70
|
+
result.sectionsCreated += changed.created;
|
|
71
|
+
result.sectionsUpdated += changed.updated;
|
|
72
|
+
result.sectionsRemoved += changed.removed;
|
|
73
|
+
result.codeRefsCreated += changed.codeRefs;
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
result.errors.push(`${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const removedFromDeleted = this.removeDeletedFiles(files);
|
|
80
|
+
result.sectionsRemoved += removedFromDeleted;
|
|
81
|
+
if (this.config.enableEmbeddings) {
|
|
82
|
+
try {
|
|
83
|
+
opts?.onProgress?.("docs: embedding sections");
|
|
84
|
+
const vectorMgr = new import_vectors.DocsVectorManager(this.config, this.db);
|
|
85
|
+
const allSections = this.db.all("SELECT * FROM doc_sections ORDER BY file_path, position");
|
|
86
|
+
const sections = allSections.map((row) => ({
|
|
87
|
+
id: row.id,
|
|
88
|
+
filePath: row.file_path,
|
|
89
|
+
title: row.title,
|
|
90
|
+
level: row.level,
|
|
91
|
+
parentId: row.parent_id ?? null,
|
|
92
|
+
summary: row.summary ?? null,
|
|
93
|
+
byteStart: row.byte_start,
|
|
94
|
+
byteEnd: row.byte_end,
|
|
95
|
+
contentHash: row.content_hash,
|
|
96
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
97
|
+
position: row.position,
|
|
98
|
+
updatedAt: row.updated_at
|
|
99
|
+
}));
|
|
100
|
+
await vectorMgr.embedBatch(sections);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
result.duration = Date.now() - start;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Index a single file. Returns null if unchanged (hash match).
|
|
109
|
+
*/
|
|
110
|
+
async indexFile(relPath, force = false) {
|
|
111
|
+
const absPath = path.join(this.projectRoot, relPath);
|
|
112
|
+
if (!fs.existsSync(absPath)) return null;
|
|
113
|
+
const stat = fs.statSync(absPath);
|
|
114
|
+
if (stat.size > this.config.docsMaxFileSize) return null;
|
|
115
|
+
const content = fs.readFileSync(absPath, "utf8");
|
|
116
|
+
const fileHash = crypto.createHash("sha256").update(content).digest("hex");
|
|
117
|
+
if (!force) {
|
|
118
|
+
const existing = this.db.get(
|
|
119
|
+
"SELECT content_hash FROM doc_sections WHERE file_path = ? LIMIT 1",
|
|
120
|
+
[relPath]
|
|
121
|
+
);
|
|
122
|
+
if (existing) {
|
|
123
|
+
const allSections = this.db.all(
|
|
124
|
+
"SELECT content_hash FROM doc_sections WHERE file_path = ?",
|
|
125
|
+
[relPath]
|
|
126
|
+
);
|
|
127
|
+
const firstSection = this.db.get(
|
|
128
|
+
"SELECT tags FROM doc_sections WHERE file_path = ? AND parent_id IS NULL LIMIT 1",
|
|
129
|
+
[relPath]
|
|
130
|
+
);
|
|
131
|
+
if (firstSection?.tags) {
|
|
132
|
+
try {
|
|
133
|
+
const tags = JSON.parse(firstSection.tags);
|
|
134
|
+
if (tags._fileHash === fileHash) return null;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const parseResult = (0, import_formats.parseDocFile)(content, relPath);
|
|
141
|
+
if (!parseResult) return null;
|
|
142
|
+
const flatSections = this.flattenSections(parseResult.sections, relPath, fileHash);
|
|
143
|
+
const existingIds = new Set(
|
|
144
|
+
this.db.all("SELECT id FROM doc_sections WHERE file_path = ?", [relPath]).map((r) => r.id)
|
|
145
|
+
);
|
|
146
|
+
let created = 0;
|
|
147
|
+
let updated = 0;
|
|
148
|
+
let codeRefsCount = 0;
|
|
149
|
+
this.db.run("BEGIN");
|
|
150
|
+
try {
|
|
151
|
+
for (const section of flatSections) {
|
|
152
|
+
if (existingIds.has(section.id)) {
|
|
153
|
+
this.db.run(`
|
|
154
|
+
UPDATE doc_sections SET
|
|
155
|
+
title = ?, level = ?, parent_id = ?, summary = ?,
|
|
156
|
+
byte_start = ?, byte_end = ?, content_hash = ?,
|
|
157
|
+
tags = ?, position = ?, updated_at = ?
|
|
158
|
+
WHERE id = ?
|
|
159
|
+
`, [
|
|
160
|
+
section.title,
|
|
161
|
+
section.level,
|
|
162
|
+
section.parentId,
|
|
163
|
+
section.summary,
|
|
164
|
+
section.byteStart,
|
|
165
|
+
section.byteEnd,
|
|
166
|
+
section.contentHash,
|
|
167
|
+
JSON.stringify(section.tags),
|
|
168
|
+
section.position,
|
|
169
|
+
section.updatedAt,
|
|
170
|
+
section.id
|
|
171
|
+
]);
|
|
172
|
+
existingIds.delete(section.id);
|
|
173
|
+
updated++;
|
|
174
|
+
} else {
|
|
175
|
+
this.db.run(`
|
|
176
|
+
INSERT INTO doc_sections (id, file_path, title, level, parent_id, summary, byte_start, byte_end, content_hash, tags, position, updated_at)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
178
|
+
`, [
|
|
179
|
+
section.id,
|
|
180
|
+
section.filePath,
|
|
181
|
+
section.title,
|
|
182
|
+
section.level,
|
|
183
|
+
section.parentId,
|
|
184
|
+
section.summary,
|
|
185
|
+
section.byteStart,
|
|
186
|
+
section.byteEnd,
|
|
187
|
+
section.contentHash,
|
|
188
|
+
JSON.stringify(section.tags),
|
|
189
|
+
section.position,
|
|
190
|
+
section.updatedAt
|
|
191
|
+
]);
|
|
192
|
+
created++;
|
|
193
|
+
}
|
|
194
|
+
this.db.run("INSERT OR REPLACE INTO doc_sections_fts (id, title, summary) VALUES (?, ?, ?)", [
|
|
195
|
+
section.id,
|
|
196
|
+
section.title,
|
|
197
|
+
section.summary ?? ""
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
for (const staleId of existingIds) {
|
|
201
|
+
this.db.run("DELETE FROM doc_sections WHERE id = ?", [staleId]);
|
|
202
|
+
this.db.run("DELETE FROM doc_sections_fts WHERE id = ?", [staleId]);
|
|
203
|
+
this.db.run("DELETE FROM doc_code_refs WHERE section_id = ?", [staleId]);
|
|
204
|
+
}
|
|
205
|
+
if (this.config.docsLinkCode) {
|
|
206
|
+
for (const section of flatSections) {
|
|
207
|
+
const sectionContent = Buffer.from(content, "utf8").slice(section.byteStart, section.byteEnd).toString("utf8");
|
|
208
|
+
const refs = (0, import_linker.linkSection)(section.id, sectionContent, this.db);
|
|
209
|
+
this.db.run("DELETE FROM doc_code_refs WHERE section_id = ?", [section.id]);
|
|
210
|
+
for (const ref of refs) {
|
|
211
|
+
this.db.run(
|
|
212
|
+
"INSERT OR REPLACE INTO doc_code_refs (section_id, qualified_name, ref_type, confidence) VALUES (?, ?, ?, ?)",
|
|
213
|
+
[ref.sectionId, ref.qualifiedName, ref.refType, ref.confidence]
|
|
214
|
+
);
|
|
215
|
+
codeRefsCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.db.run("COMMIT");
|
|
220
|
+
} catch (err) {
|
|
221
|
+
this.db.run("ROLLBACK");
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
return { created, updated, removed: existingIds.size, codeRefs: codeRefsCount };
|
|
225
|
+
}
|
|
226
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
227
|
+
/**
|
|
228
|
+
* Scan the project for documentation files matching include/exclude patterns.
|
|
229
|
+
*/
|
|
230
|
+
scanDocFiles() {
|
|
231
|
+
const includeMatchers = this.config.docsInclude.map((p) => (0, import_picomatch.default)(p));
|
|
232
|
+
const excludeMatchers = this.config.docsExclude.map((p) => (0, import_picomatch.default)(p));
|
|
233
|
+
const results = [];
|
|
234
|
+
const walk = (dir) => {
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const full = path.join(dir, entry.name);
|
|
243
|
+
const rel = path.relative(this.projectRoot, full).replace(/\\/g, "/");
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (excludeMatchers.some((m) => m(rel + "/"))) continue;
|
|
246
|
+
walk(full);
|
|
247
|
+
} else if (entry.isFile()) {
|
|
248
|
+
if (excludeMatchers.some((m) => m(rel))) continue;
|
|
249
|
+
if (includeMatchers.some((m) => m(rel))) {
|
|
250
|
+
results.push(rel);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
walk(this.projectRoot);
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Flatten a parsed section tree into a flat array of DocSection objects.
|
|
260
|
+
*/
|
|
261
|
+
flattenSections(sections, filePath, fileHash) {
|
|
262
|
+
const flat = [];
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
const walk = (nodes, parentId, ancestorSlugs) => {
|
|
265
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
266
|
+
const node = nodes[i];
|
|
267
|
+
const id = (0, import_section_id.generateSectionId)(filePath, ancestorSlugs, node.title, node.level);
|
|
268
|
+
const contentHash = crypto.createHash("sha256").update(node.content).digest("hex");
|
|
269
|
+
const summary = this.extractFirstSentence(node.content, node.title);
|
|
270
|
+
const tags = [];
|
|
271
|
+
if (parentId === null && i === 0) {
|
|
272
|
+
tags._fileHash = fileHash;
|
|
273
|
+
}
|
|
274
|
+
flat.push({
|
|
275
|
+
id,
|
|
276
|
+
filePath,
|
|
277
|
+
title: node.title,
|
|
278
|
+
level: node.level,
|
|
279
|
+
parentId,
|
|
280
|
+
summary,
|
|
281
|
+
byteStart: node.byteStart,
|
|
282
|
+
byteEnd: node.byteEnd,
|
|
283
|
+
contentHash,
|
|
284
|
+
tags: parentId === null && i === 0 ? [JSON.stringify({ _fileHash: fileHash })] : tags,
|
|
285
|
+
position: i,
|
|
286
|
+
updatedAt: now
|
|
287
|
+
});
|
|
288
|
+
if (node.children.length > 0) {
|
|
289
|
+
const currentSlug = (0, import_section_id.slugify)(node.title);
|
|
290
|
+
walk(node.children, id, [...ancestorSlugs, currentSlug]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
walk(sections, null, []);
|
|
295
|
+
return flat;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Extract the first meaningful sentence from section content.
|
|
299
|
+
* Skips the heading line itself.
|
|
300
|
+
*/
|
|
301
|
+
extractFirstSentence(content, title) {
|
|
302
|
+
const lines = content.split("\n");
|
|
303
|
+
const bodyLines = lines.filter((l) => {
|
|
304
|
+
const trimmed = l.trim();
|
|
305
|
+
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("```") || trimmed.startsWith("~~~")) return false;
|
|
306
|
+
if (/^[=-]+\s*$/.test(trimmed)) return false;
|
|
307
|
+
return true;
|
|
308
|
+
});
|
|
309
|
+
if (bodyLines.length === 0) return null;
|
|
310
|
+
const firstLine = bodyLines[0].trim();
|
|
311
|
+
const sentenceEnd = firstLine.search(/[.!?]\s|[.!?]$/);
|
|
312
|
+
if (sentenceEnd > 0) {
|
|
313
|
+
return firstLine.slice(0, sentenceEnd + 1).trim();
|
|
314
|
+
}
|
|
315
|
+
return firstLine.length > 200 ? firstLine.slice(0, 200) + "\u2026" : firstLine;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Remove sections for files that no longer exist on disk.
|
|
319
|
+
*/
|
|
320
|
+
removeDeletedFiles(currentFiles) {
|
|
321
|
+
const currentSet = new Set(currentFiles);
|
|
322
|
+
const indexedFiles = this.db.all("SELECT DISTINCT file_path FROM doc_sections").map((r) => r.file_path);
|
|
323
|
+
let removed = 0;
|
|
324
|
+
for (const filePath of indexedFiles) {
|
|
325
|
+
if (!currentSet.has(filePath)) {
|
|
326
|
+
const count = this.db.get("SELECT COUNT(*) as cnt FROM doc_sections WHERE file_path = ?", [filePath])?.cnt ?? 0;
|
|
327
|
+
this.db.run("DELETE FROM doc_sections WHERE file_path = ?", [filePath]);
|
|
328
|
+
this.db.run("DELETE FROM doc_sections_fts WHERE id IN (SELECT id FROM doc_sections WHERE file_path = ?)", [filePath]);
|
|
329
|
+
this.db.run("DELETE FROM doc_code_refs WHERE section_id IN (SELECT id FROM doc_sections WHERE file_path = ?)", [filePath]);
|
|
330
|
+
removed += count;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return removed;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
337
|
+
0 && (module.exports = {
|
|
338
|
+
DocsIndexer
|
|
339
|
+
});
|
|
340
|
+
//# sourceMappingURL=indexer.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/docs/indexer.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Documentation Indexer\n *\n * Orchestrates: scan doc files \u2192 parse sections \u2192 persist to DB.\n * Supports incremental re-indexing via content hashes.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as crypto from 'crypto';\nimport picomatch from 'picomatch';\nimport type { KiroGraphConfig } from '../config';\nimport type { DocSection, ParsedSection, DocIndexResult } from './types';\nimport { generateSectionId, slugify } from './section-id';\nimport { parseDocFile } from './formats/index';\nimport { linkSection } from './linker';\nimport { DocsVectorManager } from './vectors';\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class DocsIndexer {\n private readonly db: any; // raw sqlite db handle\n private readonly config: KiroGraphConfig;\n private readonly projectRoot: string;\n\n constructor(db: any, config: KiroGraphConfig, projectRoot: string) {\n this.db = db;\n this.config = config;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Index all documentation files in the project.\n * Skips files whose content hash hasn't changed (incremental).\n */\n async indexAll(opts?: { force?: boolean; onProgress?: (msg: string) => void }): Promise<DocIndexResult> {\n const start = Date.now();\n const result: DocIndexResult = {\n filesIndexed: 0, sectionsCreated: 0, sectionsUpdated: 0,\n sectionsRemoved: 0, codeRefsCreated: 0, errors: [], duration: 0,\n };\n\n const files = this.scanDocFiles();\n opts?.onProgress?.(`docs: found ${files.length} documentation files`);\n\n for (const relPath of files) {\n try {\n const changed = await this.indexFile(relPath, opts?.force ?? false);\n if (changed) {\n result.filesIndexed++;\n result.sectionsCreated += changed.created;\n result.sectionsUpdated += changed.updated;\n result.sectionsRemoved += changed.removed;\n result.codeRefsCreated += changed.codeRefs;\n }\n } catch (err) {\n result.errors.push(`${relPath}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // Remove sections for files that no longer exist\n const removedFromDeleted = this.removeDeletedFiles(files);\n result.sectionsRemoved += removedFromDeleted;\n\n // Embed all sections (if embeddings enabled)\n if (this.config.enableEmbeddings) {\n try {\n opts?.onProgress?.('docs: embedding sections');\n const vectorMgr = new DocsVectorManager(this.config, this.db);\n const allSections = this.db.all('SELECT * FROM doc_sections ORDER BY file_path, position') as any[];\n const sections: DocSection[] = allSections.map((row: any) => ({\n id: row.id,\n filePath: row.file_path,\n title: row.title,\n level: row.level,\n parentId: row.parent_id ?? null,\n summary: row.summary ?? null,\n byteStart: row.byte_start,\n byteEnd: row.byte_end,\n contentHash: row.content_hash,\n tags: row.tags ? JSON.parse(row.tags) : [],\n position: row.position,\n updatedAt: row.updated_at,\n }));\n await vectorMgr.embedBatch(sections);\n } catch { /* embedding is non-critical */ }\n }\n\n result.duration = Date.now() - start;\n return result;\n }\n\n /**\n * Index a single file. Returns null if unchanged (hash match).\n */\n async indexFile(relPath: string, force = false): Promise<{ created: number; updated: number; removed: number; codeRefs: number } | null> {\n const absPath = path.join(this.projectRoot, relPath);\n if (!fs.existsSync(absPath)) return null;\n\n const stat = fs.statSync(absPath);\n if (stat.size > this.config.docsMaxFileSize) return null;\n\n const content = fs.readFileSync(absPath, 'utf8');\n const fileHash = crypto.createHash('sha256').update(content).digest('hex');\n\n // Check if file has changed (skip if hash matches and not forced)\n if (!force) {\n const existing = this.db.get(\n 'SELECT content_hash FROM doc_sections WHERE file_path = ? LIMIT 1',\n [relPath]\n );\n // If any section exists with same file and we can verify the file hasn't changed\n if (existing) {\n const allSections = this.db.all(\n 'SELECT content_hash FROM doc_sections WHERE file_path = ?',\n [relPath]\n );\n // Use a file-level hash stored as a tag on the first section\n const firstSection = this.db.get(\n \"SELECT tags FROM doc_sections WHERE file_path = ? AND parent_id IS NULL LIMIT 1\",\n [relPath]\n );\n if (firstSection?.tags) {\n try {\n const tags = JSON.parse(firstSection.tags);\n if (tags._fileHash === fileHash) return null; // unchanged\n } catch { /* proceed with re-index */ }\n }\n }\n }\n\n // Parse the file\n const parseResult = parseDocFile(content, relPath);\n if (!parseResult) return null;\n\n // Flatten sections and generate IDs\n const flatSections = this.flattenSections(parseResult.sections, relPath, fileHash);\n\n // Get existing section IDs for this file\n const existingIds = new Set<string>(\n (this.db.all('SELECT id FROM doc_sections WHERE file_path = ?', [relPath]) as Array<{ id: string }>)\n .map(r => r.id)\n );\n\n let created = 0;\n let updated = 0;\n let codeRefsCount = 0;\n\n // Upsert sections in a transaction\n this.db.run('BEGIN');\n try {\n for (const section of flatSections) {\n if (existingIds.has(section.id)) {\n // Update existing\n this.db.run(`\n UPDATE doc_sections SET\n title = ?, level = ?, parent_id = ?, summary = ?,\n byte_start = ?, byte_end = ?, content_hash = ?,\n tags = ?, position = ?, updated_at = ?\n WHERE id = ?\n `, [\n section.title, section.level, section.parentId, section.summary,\n section.byteStart, section.byteEnd, section.contentHash,\n JSON.stringify(section.tags), section.position, section.updatedAt,\n section.id,\n ]);\n existingIds.delete(section.id);\n updated++;\n } else {\n // Insert new\n this.db.run(`\n INSERT INTO doc_sections (id, file_path, title, level, parent_id, summary, byte_start, byte_end, content_hash, tags, position, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `, [\n section.id, section.filePath, section.title, section.level,\n section.parentId, section.summary, section.byteStart, section.byteEnd,\n section.contentHash, JSON.stringify(section.tags), section.position, section.updatedAt,\n ]);\n created++;\n }\n\n // Update FTS\n this.db.run('INSERT OR REPLACE INTO doc_sections_fts (id, title, summary) VALUES (?, ?, ?)', [\n section.id, section.title, section.summary ?? '',\n ]);\n }\n\n // Remove sections that no longer exist in the file\n for (const staleId of existingIds) {\n this.db.run('DELETE FROM doc_sections WHERE id = ?', [staleId]);\n this.db.run('DELETE FROM doc_sections_fts WHERE id = ?', [staleId]);\n this.db.run('DELETE FROM doc_code_refs WHERE section_id = ?', [staleId]);\n }\n\n // Run code linker if enabled\n if (this.config.docsLinkCode) {\n for (const section of flatSections) {\n // Read section content for linking\n const sectionContent = Buffer.from(content, 'utf8')\n .slice(section.byteStart, section.byteEnd).toString('utf8');\n const refs = linkSection(section.id, sectionContent, this.db);\n // Clear old refs for this section and insert new ones\n this.db.run('DELETE FROM doc_code_refs WHERE section_id = ?', [section.id]);\n for (const ref of refs) {\n this.db.run(\n 'INSERT OR REPLACE INTO doc_code_refs (section_id, qualified_name, ref_type, confidence) VALUES (?, ?, ?, ?)',\n [ref.sectionId, ref.qualifiedName, ref.refType, ref.confidence],\n );\n codeRefsCount++;\n }\n }\n }\n\n this.db.run('COMMIT');\n } catch (err) {\n this.db.run('ROLLBACK');\n throw err;\n }\n\n return { created, updated, removed: existingIds.size, codeRefs: codeRefsCount };\n }\n\n // \u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Scan the project for documentation files matching include/exclude patterns.\n */\n private scanDocFiles(): string[] {\n const includeMatchers = this.config.docsInclude.map(p => picomatch(p));\n const excludeMatchers = this.config.docsExclude.map(p => picomatch(p));\n const results: string[] = [];\n\n const walk = (dir: string) => {\n let entries: fs.Dirent[];\n try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }\n\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n const rel = path.relative(this.projectRoot, full).replace(/\\\\/g, '/');\n\n if (entry.isDirectory()) {\n // Skip excluded directories early\n if (excludeMatchers.some(m => m(rel + '/'))) continue;\n walk(full);\n } else if (entry.isFile()) {\n // Check exclude first\n if (excludeMatchers.some(m => m(rel))) continue;\n // Check include\n if (includeMatchers.some(m => m(rel))) {\n results.push(rel);\n }\n }\n }\n };\n\n walk(this.projectRoot);\n return results;\n }\n\n /**\n * Flatten a parsed section tree into a flat array of DocSection objects.\n */\n private flattenSections(\n sections: ParsedSection[],\n filePath: string,\n fileHash: string,\n ): DocSection[] {\n const flat: DocSection[] = [];\n const now = Date.now();\n\n const walk = (nodes: ParsedSection[], parentId: string | null, ancestorSlugs: string[]) => {\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i];\n const id = generateSectionId(filePath, ancestorSlugs, node.title, node.level);\n const contentHash = crypto.createHash('sha256').update(node.content).digest('hex');\n const summary = this.extractFirstSentence(node.content, node.title);\n\n // Store file hash in tags of root-level sections for incremental detection\n const tags: string[] = [];\n if (parentId === null && i === 0) {\n (tags as any)._fileHash = fileHash;\n }\n\n flat.push({\n id,\n filePath,\n title: node.title,\n level: node.level,\n parentId,\n summary,\n byteStart: node.byteStart,\n byteEnd: node.byteEnd,\n contentHash,\n tags: parentId === null && i === 0 ? [JSON.stringify({ _fileHash: fileHash })] : tags,\n position: i,\n updatedAt: now,\n });\n\n if (node.children.length > 0) {\n const currentSlug = slugify(node.title);\n walk(node.children, id, [...ancestorSlugs, currentSlug]);\n }\n }\n };\n\n walk(sections, null, []);\n return flat;\n }\n\n /**\n * Extract the first meaningful sentence from section content.\n * Skips the heading line itself.\n */\n private extractFirstSentence(content: string, title: string): string | null {\n // Remove the heading line\n const lines = content.split('\\n');\n const bodyLines = lines.filter(l => {\n const trimmed = l.trim();\n // Skip heading lines, empty lines, and code fence markers\n if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('```') || trimmed.startsWith('~~~')) return false;\n // Skip setext underlines\n if (/^[=-]+\\s*$/.test(trimmed)) return false;\n return true;\n });\n\n if (bodyLines.length === 0) return null;\n\n // Take first non-empty line, truncate at sentence boundary\n const firstLine = bodyLines[0].trim();\n const sentenceEnd = firstLine.search(/[.!?]\\s|[.!?]$/);\n if (sentenceEnd > 0) {\n return firstLine.slice(0, sentenceEnd + 1).trim();\n }\n\n // No sentence boundary \u2014 take the whole first line (truncated)\n return firstLine.length > 200 ? firstLine.slice(0, 200) + '\u2026' : firstLine;\n }\n\n /**\n * Remove sections for files that no longer exist on disk.\n */\n private removeDeletedFiles(currentFiles: string[]): number {\n const currentSet = new Set(currentFiles);\n const indexedFiles = (this.db.all('SELECT DISTINCT file_path FROM doc_sections') as Array<{ file_path: string }>)\n .map(r => r.file_path);\n\n let removed = 0;\n for (const filePath of indexedFiles) {\n if (!currentSet.has(filePath)) {\n const count = this.db.get('SELECT COUNT(*) as cnt FROM doc_sections WHERE file_path = ?', [filePath])?.cnt ?? 0;\n this.db.run('DELETE FROM doc_sections WHERE file_path = ?', [filePath]);\n this.db.run('DELETE FROM doc_sections_fts WHERE id IN (SELECT id FROM doc_sections WHERE file_path = ?)', [filePath]);\n this.db.run('DELETE FROM doc_code_refs WHERE section_id IN (SELECT id FROM doc_sections WHERE file_path = ?)', [filePath]);\n removed += count;\n }\n }\n\n return removed;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["picomatch"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var linker_exports = {};
|
|
20
|
+
__export(linker_exports, {
|
|
21
|
+
extractCandidateRefs: () => extractCandidateRefs,
|
|
22
|
+
linkSection: () => linkSection,
|
|
23
|
+
resolveCodeRefs: () => resolveCodeRefs
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(linker_exports);
|
|
26
|
+
const BACKTICK_RE = /`([^`\n]+)`/g;
|
|
27
|
+
const IDENTIFIER_RE = /\b([A-Z][a-zA-Z0-9]+(?:\.[a-zA-Z][a-zA-Z0-9]*)?)\b/g;
|
|
28
|
+
const SNAKE_CASE_RE = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b/g;
|
|
29
|
+
const IMPORT_PATH_RE = /(?:from|import)\s+['"]([^'"]+)['"]/g;
|
|
30
|
+
const IGNORE_WORDS = /* @__PURE__ */ new Set([
|
|
31
|
+
"TODO",
|
|
32
|
+
"NOTE",
|
|
33
|
+
"FIXME",
|
|
34
|
+
"HACK",
|
|
35
|
+
"WARNING",
|
|
36
|
+
"IMPORTANT",
|
|
37
|
+
"DEPRECATED",
|
|
38
|
+
"README",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"CHANGELOG",
|
|
41
|
+
"CONTRIBUTING",
|
|
42
|
+
"HTTP",
|
|
43
|
+
"HTTPS",
|
|
44
|
+
"JSON",
|
|
45
|
+
"HTML",
|
|
46
|
+
"CSS",
|
|
47
|
+
"SQL",
|
|
48
|
+
"API",
|
|
49
|
+
"REST",
|
|
50
|
+
"CRUD",
|
|
51
|
+
"GET",
|
|
52
|
+
"POST",
|
|
53
|
+
"PUT",
|
|
54
|
+
"DELETE",
|
|
55
|
+
"PATCH",
|
|
56
|
+
"HEAD",
|
|
57
|
+
"OPTIONS",
|
|
58
|
+
"String",
|
|
59
|
+
"Number",
|
|
60
|
+
"Boolean",
|
|
61
|
+
"Object",
|
|
62
|
+
"Array",
|
|
63
|
+
"Function",
|
|
64
|
+
"Promise",
|
|
65
|
+
"Error",
|
|
66
|
+
"Date",
|
|
67
|
+
"Map",
|
|
68
|
+
"Set",
|
|
69
|
+
"RegExp",
|
|
70
|
+
"Buffer",
|
|
71
|
+
"Symbol",
|
|
72
|
+
"True",
|
|
73
|
+
"False",
|
|
74
|
+
"None",
|
|
75
|
+
"Null",
|
|
76
|
+
"Undefined",
|
|
77
|
+
"This",
|
|
78
|
+
"Self",
|
|
79
|
+
"Super",
|
|
80
|
+
"Class",
|
|
81
|
+
"Interface",
|
|
82
|
+
"Type"
|
|
83
|
+
]);
|
|
84
|
+
function extractCandidateRefs(content) {
|
|
85
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
86
|
+
let match;
|
|
87
|
+
BACKTICK_RE.lastIndex = 0;
|
|
88
|
+
while ((match = BACKTICK_RE.exec(content)) !== null) {
|
|
89
|
+
const ref = match[1].trim();
|
|
90
|
+
if (ref.includes(" ") || ref.startsWith("/") || ref.startsWith("http") || ref.startsWith("$")) continue;
|
|
91
|
+
if (ref.includes(".")) {
|
|
92
|
+
const parts = ref.split(".");
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
if (part.length >= 2 && !IGNORE_WORDS.has(part)) candidates.add(part);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (ref.length >= 2 && !IGNORE_WORDS.has(ref)) candidates.add(ref);
|
|
98
|
+
}
|
|
99
|
+
IDENTIFIER_RE.lastIndex = 0;
|
|
100
|
+
while ((match = IDENTIFIER_RE.exec(content)) !== null) {
|
|
101
|
+
const ref = match[1];
|
|
102
|
+
if (!IGNORE_WORDS.has(ref) && ref.length >= 3) {
|
|
103
|
+
candidates.add(ref);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
SNAKE_CASE_RE.lastIndex = 0;
|
|
107
|
+
while ((match = SNAKE_CASE_RE.exec(content)) !== null) {
|
|
108
|
+
const ref = match[1];
|
|
109
|
+
if (ref.length >= 4) candidates.add(ref);
|
|
110
|
+
}
|
|
111
|
+
return [...candidates];
|
|
112
|
+
}
|
|
113
|
+
function resolveCodeRefs(sectionId, candidates, db) {
|
|
114
|
+
if (candidates.length === 0) return [];
|
|
115
|
+
const refs = [];
|
|
116
|
+
const seen = /* @__PURE__ */ new Set();
|
|
117
|
+
const CHUNK_SIZE = 50;
|
|
118
|
+
for (let i = 0; i < candidates.length; i += CHUNK_SIZE) {
|
|
119
|
+
const chunk = candidates.slice(i, i + CHUNK_SIZE);
|
|
120
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
121
|
+
const rows = db.all(
|
|
122
|
+
`SELECT name, qualified_name FROM nodes WHERE name IN (${placeholders}) GROUP BY qualified_name`,
|
|
123
|
+
chunk
|
|
124
|
+
);
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
const key = `${sectionId}:${row.qualified_name}`;
|
|
127
|
+
if (seen.has(key)) continue;
|
|
128
|
+
seen.add(key);
|
|
129
|
+
const refType = "mentions";
|
|
130
|
+
const confidence = chunk.includes(row.name) ? 0.8 : 0.6;
|
|
131
|
+
refs.push({
|
|
132
|
+
sectionId,
|
|
133
|
+
qualifiedName: row.qualified_name,
|
|
134
|
+
refType,
|
|
135
|
+
confidence
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return refs;
|
|
140
|
+
}
|
|
141
|
+
function linkSection(sectionId, content, db) {
|
|
142
|
+
const candidates = extractCandidateRefs(content);
|
|
143
|
+
return resolveCodeRefs(sectionId, candidates, db);
|
|
144
|
+
}
|
|
145
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
146
|
+
0 && (module.exports = {
|
|
147
|
+
extractCandidateRefs,
|
|
148
|
+
linkSection,
|
|
149
|
+
resolveCodeRefs
|
|
150
|
+
});
|
|
151
|
+
//# sourceMappingURL=linker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/docs/linker.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Code \u2194 Documentation Linker\n *\n * Detects references to code symbols within documentation sections.\n * Strategies:\n * 1. Backtick references: `functionName`, `ClassName.method`\n * 2. Import-style paths: from './auth/service'\n * 3. Identifier patterns: CamelCase, snake_case words that match indexed symbols\n */\n\nimport type { DocCodeRef, DocRefType } from './types';\n\n// \u2500\u2500 Patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Backtick-wrapped code references\nconst BACKTICK_RE = /`([^`\\n]+)`/g;\n\n// Common identifier patterns (CamelCase, snake_case, SCREAMING_SNAKE)\nconst IDENTIFIER_RE = /\\b([A-Z][a-zA-Z0-9]+(?:\\.[a-zA-Z][a-zA-Z0-9]*)?)\\b/g;\nconst SNAKE_CASE_RE = /\\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\\b/g;\n\n// Import-style paths\nconst IMPORT_PATH_RE = /(?:from|import)\\s+['\"]([^'\"]+)['\"]/g;\n\n// Words to ignore (too common to be meaningful symbol references)\nconst IGNORE_WORDS = new Set([\n 'TODO', 'NOTE', 'FIXME', 'HACK', 'WARNING', 'IMPORTANT', 'DEPRECATED',\n 'README', 'LICENSE', 'CHANGELOG', 'CONTRIBUTING',\n 'HTTP', 'HTTPS', 'JSON', 'HTML', 'CSS', 'SQL', 'API', 'REST', 'CRUD',\n 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',\n 'String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'Promise',\n 'Error', 'Date', 'Map', 'Set', 'RegExp', 'Buffer', 'Symbol',\n 'True', 'False', 'None', 'Null', 'Undefined',\n 'This', 'Self', 'Super', 'Class', 'Interface', 'Type',\n]);\n\n/**\n * Extract candidate symbol references from documentation content.\n * Returns unique candidate names that should be looked up against the graph.\n */\nexport function extractCandidateRefs(content: string): string[] {\n const candidates = new Set<string>();\n\n // 1. Backtick references (highest confidence)\n let match: RegExpExecArray | null;\n BACKTICK_RE.lastIndex = 0;\n while ((match = BACKTICK_RE.exec(content)) !== null) {\n const ref = match[1].trim();\n // Skip if it looks like a command, path, or URL\n if (ref.includes(' ') || ref.startsWith('/') || ref.startsWith('http') || ref.startsWith('$')) continue;\n // Handle Class.method notation\n if (ref.includes('.')) {\n const parts = ref.split('.');\n for (const part of parts) {\n if (part.length >= 2 && !IGNORE_WORDS.has(part)) candidates.add(part);\n }\n }\n if (ref.length >= 2 && !IGNORE_WORDS.has(ref)) candidates.add(ref);\n }\n\n // 2. CamelCase identifiers (medium confidence)\n IDENTIFIER_RE.lastIndex = 0;\n while ((match = IDENTIFIER_RE.exec(content)) !== null) {\n const ref = match[1];\n if (!IGNORE_WORDS.has(ref) && ref.length >= 3) {\n candidates.add(ref);\n }\n }\n\n // 3. snake_case identifiers (medium confidence)\n SNAKE_CASE_RE.lastIndex = 0;\n while ((match = SNAKE_CASE_RE.exec(content)) !== null) {\n const ref = match[1];\n if (ref.length >= 4) candidates.add(ref);\n }\n\n return [...candidates];\n}\n\n/**\n * Resolve candidate references against the code graph.\n * Returns DocCodeRef entries for symbols that actually exist in the index.\n *\n * @param sectionId - The doc section ID\n * @param candidates - Candidate symbol names extracted from the section\n * @param db - Raw SQLite database handle\n */\nexport function resolveCodeRefs(\n sectionId: string,\n candidates: string[],\n db: any,\n): DocCodeRef[] {\n if (candidates.length === 0) return [];\n\n const refs: DocCodeRef[] = [];\n const seen = new Set<string>();\n\n // Batch lookup: find nodes matching any candidate name\n // Use a chunked approach to avoid SQLite variable limits\n const CHUNK_SIZE = 50;\n for (let i = 0; i < candidates.length; i += CHUNK_SIZE) {\n const chunk = candidates.slice(i, i + CHUNK_SIZE);\n const placeholders = chunk.map(() => '?').join(',');\n const rows = db.all(\n `SELECT name, qualified_name FROM nodes WHERE name IN (${placeholders}) GROUP BY qualified_name`,\n chunk,\n ) as Array<{ name: string; qualified_name: string }>;\n\n for (const row of rows) {\n const key = `${sectionId}:${row.qualified_name}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n // Determine ref type based on context\n const refType: DocRefType = 'mentions';\n const confidence = chunk.includes(row.name) ? 0.8 : 0.6;\n\n refs.push({\n sectionId,\n qualifiedName: row.qualified_name,\n refType,\n confidence,\n });\n }\n }\n\n return refs;\n}\n\n/**\n * Run the full linking pipeline for a section.\n * Extracts candidates from content, resolves against the graph, returns refs.\n */\nexport function linkSection(\n sectionId: string,\n content: string,\n db: any,\n): DocCodeRef[] {\n const candidates = extractCandidateRefs(content);\n return resolveCodeRefs(sectionId, candidates, db);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var lint_exports = {};
|
|
30
|
+
__export(lint_exports, {
|
|
31
|
+
docsLint: () => docsLint
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(lint_exports);
|
|
34
|
+
var fs = __toESM(require("fs"));
|
|
35
|
+
var path = __toESM(require("path"));
|
|
36
|
+
var crypto = __toESM(require("crypto"));
|
|
37
|
+
function docsLint(db, projectRoot) {
|
|
38
|
+
const result = {
|
|
39
|
+
brokenRefs: [],
|
|
40
|
+
staleSections: [],
|
|
41
|
+
ftsDesync: null,
|
|
42
|
+
orphanRefs: 0,
|
|
43
|
+
totalIssues: 0
|
|
44
|
+
};
|
|
45
|
+
const refs = db.all(`
|
|
46
|
+
SELECT r.section_id, r.qualified_name
|
|
47
|
+
FROM doc_code_refs r
|
|
48
|
+
LEFT JOIN nodes n ON n.qualified_name = r.qualified_name
|
|
49
|
+
WHERE n.id IS NULL
|
|
50
|
+
`);
|
|
51
|
+
result.brokenRefs = refs.map((r) => ({ sectionId: r.section_id, qualifiedName: r.qualified_name }));
|
|
52
|
+
const sections = db.all(`
|
|
53
|
+
SELECT id, file_path, byte_start, byte_end, content_hash
|
|
54
|
+
FROM doc_sections
|
|
55
|
+
`);
|
|
56
|
+
for (const section of sections) {
|
|
57
|
+
const absPath = path.join(projectRoot, section.file_path);
|
|
58
|
+
try {
|
|
59
|
+
if (!fs.existsSync(absPath)) {
|
|
60
|
+
result.staleSections.push({ id: section.id, filePath: section.file_path });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const buffer = fs.readFileSync(absPath);
|
|
64
|
+
const content = buffer.slice(section.byte_start, section.byte_end).toString("utf8");
|
|
65
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
66
|
+
if (hash !== section.content_hash) {
|
|
67
|
+
result.staleSections.push({ id: section.id, filePath: section.file_path });
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
result.staleSections.push({ id: section.id, filePath: section.file_path });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const sectionCount = db.get("SELECT COUNT(*) as cnt FROM doc_sections")?.cnt ?? 0;
|
|
74
|
+
const ftsCount = db.get("SELECT COUNT(*) as cnt FROM doc_sections_fts")?.cnt ?? 0;
|
|
75
|
+
if (sectionCount !== ftsCount) {
|
|
76
|
+
result.ftsDesync = { sections: sectionCount, ftsRows: ftsCount };
|
|
77
|
+
}
|
|
78
|
+
const orphans = db.get(`
|
|
79
|
+
SELECT COUNT(*) as cnt
|
|
80
|
+
FROM doc_code_refs r
|
|
81
|
+
LEFT JOIN doc_sections s ON s.id = r.section_id
|
|
82
|
+
WHERE s.id IS NULL
|
|
83
|
+
`)?.cnt ?? 0;
|
|
84
|
+
result.orphanRefs = orphans;
|
|
85
|
+
result.totalIssues = result.brokenRefs.length + result.staleSections.length + (result.ftsDesync ? 1 : 0) + result.orphanRefs;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
89
|
+
0 && (module.exports = {
|
|
90
|
+
docsLint
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=lint.js.map
|