knodin 0.7.6 → 0.8.2

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.
Files changed (130) hide show
  1. package/README.md +19 -7
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +2164 -108
  4. package/dist/bin/launcher.js +25 -3
  5. package/dist/src/agent-integration.js +304 -0
  6. package/dist/src/artifact-refresh.js +82 -0
  7. package/dist/src/cli-args.js +292 -0
  8. package/dist/src/cli-model.js +384 -0
  9. package/dist/src/codeflow-replay.js +81 -0
  10. package/dist/src/compact-structural.js +96 -0
  11. package/dist/src/compare.js +39 -0
  12. package/dist/src/competitive-cold-mcp.js +40 -0
  13. package/dist/src/competitive-constraints.js +21 -0
  14. package/dist/src/competitive-manifest.js +411 -0
  15. package/dist/src/competitive-measurement.js +183 -0
  16. package/dist/src/competitive-runner.js +487 -0
  17. package/dist/src/competitive-sandbox.js +108 -0
  18. package/dist/src/context-export.js +423 -0
  19. package/dist/src/context.js +102 -0
  20. package/dist/src/deterministic-random.js +34 -0
  21. package/dist/src/diagnostics-write-helper.js +473 -0
  22. package/dist/src/diagnostics.js +1476 -0
  23. package/dist/src/docs-sections.js +141 -0
  24. package/dist/src/doctor.js +382 -0
  25. package/dist/src/engine/ann-hnsw.js +261 -0
  26. package/dist/src/engine/embeddings.js +193 -0
  27. package/dist/src/engine/file-walker.js +49 -0
  28. package/dist/src/engine/git-history.js +289 -0
  29. package/dist/src/engine/index.js +14238 -0
  30. package/dist/src/engine/perf.js +115 -0
  31. package/dist/src/engine/prune.js +112 -0
  32. package/dist/src/engine/sarif-import.js +341 -0
  33. package/dist/src/engine/scip-import.js +423 -0
  34. package/dist/src/engine/source-policy.js +85 -0
  35. package/dist/src/engine/sqlite.js +71 -0
  36. package/dist/src/engine/state-paths.js +175 -0
  37. package/dist/src/engine/symbol-delete.js +58 -0
  38. package/dist/src/execution-profile.js +208 -0
  39. package/dist/src/failure-diagnosis.js +655 -0
  40. package/dist/src/fleet.js +7 -0
  41. package/dist/src/git-executable.js +31 -0
  42. package/dist/src/graph-layout.js +173 -0
  43. package/dist/src/graph-query-health.js +115 -0
  44. package/dist/src/hook-manager-integration.js +156 -0
  45. package/dist/src/index-activity.js +126 -0
  46. package/dist/src/init-progress-worker.js +106 -2
  47. package/dist/src/init-progress.js +155 -0
  48. package/dist/src/init.js +1295 -0
  49. package/dist/src/lifecycle-health.js +282 -0
  50. package/dist/src/lsp-readonly.js +217 -0
  51. package/dist/src/mcp-graph-worker.js +69 -0
  52. package/dist/src/mcp-reliability.js +154 -0
  53. package/dist/src/mcp-worker-supervisor.js +350 -0
  54. package/dist/src/mirror.js +290 -0
  55. package/dist/src/node-runtime.js +157 -0
  56. package/dist/src/output-compression.js +630 -0
  57. package/dist/src/output-telemetry.js +368 -0
  58. package/dist/src/pr-triage.js +638 -0
  59. package/dist/src/progressive-evidence.js +477 -0
  60. package/dist/src/pure-compression-cli.js +102 -0
  61. package/dist/src/relationship-adapters.js +377 -0
  62. package/dist/src/release-attestation.js +533 -0
  63. package/dist/src/release-preflight.js +513 -0
  64. package/dist/src/repair-lease.js +85 -0
  65. package/dist/src/repair-progress-worker.js +120 -2
  66. package/dist/src/repair-progress.js +262 -0
  67. package/dist/src/repository-init-process.js +177 -0
  68. package/dist/src/repository-management.js +1261 -0
  69. package/dist/src/response-budget.js +196 -0
  70. package/dist/src/server.js +217 -0
  71. package/dist/src/structural-fast-path.js +344 -0
  72. package/dist/src/structural-snapshot.js +37 -0
  73. package/dist/src/system-config.js +638 -0
  74. package/dist/src/terminal-help.js +83 -0
  75. package/dist/src/tools/knodin-tools.js +1640 -0
  76. package/dist/src/update-ceremony.js +162 -0
  77. package/dist/src/update-policy.js +944 -0
  78. package/dist/src/update-trust.js +504 -0
  79. package/dist/src/version.js +13 -0
  80. package/dist/src/visualization.js +515 -0
  81. package/dist/src/wait-for-fresh.js +98 -0
  82. package/dist/src/worktree-lifecycle.js +234 -0
  83. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  84. package/docs/CLI.md +20 -1
  85. package/docs/COMPARISON.md +403 -0
  86. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  87. package/docs/CONTAINED-EXECUTION.md +77 -0
  88. package/docs/DIAGNOSTICS.md +80 -0
  89. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  90. package/docs/HANDOFF.md +180 -0
  91. package/docs/INSTALLATION.md +21 -18
  92. package/docs/MCP.md +59 -8
  93. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  94. package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
  95. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  96. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  97. package/docs/SCIP-IMPORT.md +62 -0
  98. package/docs/SIGNED-UPDATES.md +151 -0
  99. package/docs/TELEMETRY.md +46 -0
  100. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  101. package/docs/assets/knodin-favicon.svg +4 -0
  102. package/docs/releases/0.3.0.md +46 -0
  103. package/docs/releases/0.4.0.md +68 -0
  104. package/docs/releases/0.4.1.md +28 -0
  105. package/docs/releases/0.4.2.md +27 -0
  106. package/docs/releases/0.4.3.md +23 -0
  107. package/docs/releases/0.5.0.md +29 -0
  108. package/docs/releases/0.5.1.md +17 -0
  109. package/docs/releases/0.6.0.md +18 -0
  110. package/docs/releases/0.7.0.md +24 -0
  111. package/docs/releases/0.7.1.md +21 -0
  112. package/docs/releases/0.7.2.md +21 -0
  113. package/docs/releases/0.7.3.md +23 -0
  114. package/docs/releases/0.7.4.md +17 -0
  115. package/docs/releases/0.7.5.md +20 -0
  116. package/docs/releases/0.8.0.md +74 -0
  117. package/docs/releases/0.8.2.md +34 -0
  118. package/package.json +127 -4
  119. package/roadmap/competitive-roadmap.md +3801 -0
  120. package/schemas/release-attestation-v1.schema.json +210 -0
  121. package/schemas/support-bundle-v2.schema.json +212 -0
  122. package/dist/chunks/chunk-DMQAGX77.js +0 -654
  123. package/dist/chunks/chunk-F4Z3Z766.js +0 -4
  124. package/dist/chunks/chunk-SIJAQVSX.js +0 -3
  125. package/dist/chunks/chunk-X6M4HUUE.js +0 -2
  126. package/dist/chunks/chunk-YPRMY2LP.js +0 -8
  127. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
  128. package/dist/chunks/server-7EDF4CBY.js +0 -14
  129. package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
  130. package/docs/releases/0.7.6.md +0 -25
@@ -0,0 +1,423 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { compareBytes } from "../compare.js";
4
+ export const SCIP_DEFAULT_LIMITS = {
5
+ maxBytes: 64 * 1024 * 1024,
6
+ maxFiles: 10_000,
7
+ maxFacts: 250_000,
8
+ timeoutMs: 30_000,
9
+ };
10
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
11
+ let activeDeadline = Number.POSITIVE_INFINITY;
12
+ let activeTimeoutMs = 0;
13
+ function fail(message) {
14
+ activeDeadline = Number.POSITIVE_INFINITY;
15
+ activeTimeoutMs = 0;
16
+ throw new Error(`knodin SCIP import: ${message}`);
17
+ }
18
+ function checkDeadline() {
19
+ if (performance.now() > activeDeadline)
20
+ fail(`import exceeded the ${activeTimeoutMs} ms limit. Raise it with --scip-timeout-ms.`);
21
+ }
22
+ function readVarint(bytes, offset) {
23
+ let value = 0;
24
+ let shift = 0;
25
+ for (let count = 0; count < 10 && offset < bytes.length; count++) {
26
+ const byte = bytes[offset++];
27
+ if (shift < 53)
28
+ value += (byte & 0x7f) * 2 ** shift;
29
+ if ((byte & 0x80) === 0)
30
+ return [value, offset];
31
+ shift += 7;
32
+ }
33
+ return fail("malformed protobuf varint");
34
+ }
35
+ function visitFields(bytes, visitor) {
36
+ let offset = 0;
37
+ while (offset < bytes.length) {
38
+ checkDeadline();
39
+ let tag;
40
+ [tag, offset] = readVarint(bytes, offset);
41
+ const number = Math.floor(tag / 8);
42
+ const wire = tag & 7;
43
+ if (number < 1)
44
+ fail("malformed protobuf field number");
45
+ if (wire === 0) {
46
+ let value;
47
+ [value, offset] = readVarint(bytes, offset);
48
+ visitor(number, wire, value);
49
+ }
50
+ else if (wire === 2) {
51
+ let length;
52
+ [length, offset] = readVarint(bytes, offset);
53
+ if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
54
+ fail("malformed protobuf length");
55
+ visitor(number, wire, bytes.subarray(offset, offset + length));
56
+ offset += length;
57
+ }
58
+ else if (wire === 1) {
59
+ offset += 8;
60
+ }
61
+ else if (wire === 5) {
62
+ offset += 4;
63
+ }
64
+ else {
65
+ fail(`unsupported protobuf wire type ${wire}`);
66
+ }
67
+ if (offset > bytes.length)
68
+ fail("truncated protobuf field");
69
+ }
70
+ }
71
+ function countLengthDelimitedFields(bytes, target, maximum) {
72
+ let count = 0;
73
+ let offset = 0;
74
+ while (offset < bytes.length) {
75
+ checkDeadline();
76
+ let tag;
77
+ [tag, offset] = readVarint(bytes, offset);
78
+ const number = Math.floor(tag / 8);
79
+ const wire = tag & 7;
80
+ if (number < 1)
81
+ fail("malformed protobuf field number");
82
+ if (wire === 0) {
83
+ [, offset] = readVarint(bytes, offset);
84
+ }
85
+ else if (wire === 2) {
86
+ let length;
87
+ [length, offset] = readVarint(bytes, offset);
88
+ if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
89
+ fail("malformed protobuf length");
90
+ if (number === target && ++count > maximum)
91
+ return count;
92
+ offset += length;
93
+ }
94
+ else if (wire === 1)
95
+ offset += 8;
96
+ else if (wire === 5)
97
+ offset += 4;
98
+ else
99
+ fail(`unsupported protobuf wire type ${wire}`);
100
+ if (offset > bytes.length)
101
+ fail("truncated protobuf field");
102
+ }
103
+ return count;
104
+ }
105
+ function strings(message, number) {
106
+ const result = [];
107
+ visitFields(message, (fieldNumber, wire, value) => {
108
+ if (fieldNumber === number && wire === 2)
109
+ result.push(textDecoder.decode(value));
110
+ });
111
+ return result;
112
+ }
113
+ function messages(message, number) {
114
+ const result = [];
115
+ visitFields(message, (fieldNumber, wire, value) => {
116
+ if (fieldNumber === number && wire === 2)
117
+ result.push(value);
118
+ });
119
+ return result;
120
+ }
121
+ function numberField(message, number) {
122
+ let result = 0;
123
+ let found = false;
124
+ visitFields(message, (fieldNumber, wire, value) => {
125
+ if (!found && fieldNumber === number && wire === 0) {
126
+ result = value;
127
+ found = true;
128
+ }
129
+ });
130
+ return result;
131
+ }
132
+ function packedNumbers(message, number) {
133
+ const result = [];
134
+ visitFields(message, (fieldNumber, wire, value) => {
135
+ if (fieldNumber !== number)
136
+ return;
137
+ if (wire === 0)
138
+ result.push(value);
139
+ if (wire === 2) {
140
+ const bytes = value;
141
+ let offset = 0;
142
+ while (offset < bytes.length) {
143
+ let packedValue;
144
+ [packedValue, offset] = readVarint(bytes, offset);
145
+ result.push(packedValue);
146
+ }
147
+ }
148
+ });
149
+ return result;
150
+ }
151
+ function parseRange(values) {
152
+ if (values.length !== 3 && values.length !== 4)
153
+ fail("range must contain three or four integers");
154
+ const [startLine, startCol] = values;
155
+ const endLine = values.length === 3 ? startLine : values[2];
156
+ const endCol = values.length === 3 ? values[2] : values[3];
157
+ if ([startLine, startCol, endLine, endCol].some((value) => !Number.isSafeInteger(value)))
158
+ fail("range contains an unsafe integer");
159
+ if (endLine < startLine || (endLine === startLine && endCol < startCol))
160
+ fail("range is reversed");
161
+ return { startLine, startCol, endLine, endCol };
162
+ }
163
+ function parseOccurrence(message) {
164
+ const singleLine = messages(message, 8)[0];
165
+ const multiLine = messages(message, 9)[0];
166
+ const symbol = strings(message, 2)[0] ?? "";
167
+ let range;
168
+ if (singleLine) {
169
+ const line = numberField(singleLine, 1);
170
+ range = parseRange([line, numberField(singleLine, 2), numberField(singleLine, 3)]);
171
+ }
172
+ else if (multiLine) {
173
+ range = parseRange([
174
+ numberField(multiLine, 1),
175
+ numberField(multiLine, 2),
176
+ numberField(multiLine, 3),
177
+ numberField(multiLine, 4),
178
+ ]);
179
+ }
180
+ else {
181
+ range = parseRange(packedNumbers(message, 1));
182
+ }
183
+ if (!symbol)
184
+ fail("occurrence is missing its symbol");
185
+ return { range, symbol, roles: numberField(message, 3) };
186
+ }
187
+ function parseSymbolInfo(message) {
188
+ const symbol = strings(message, 1)[0] ?? "";
189
+ if (!symbol)
190
+ fail("symbol information is missing its symbol");
191
+ return {
192
+ symbol,
193
+ displayName: strings(message, 6)[0] ?? "",
194
+ kind: numberField(message, 5),
195
+ relationships: messages(message, 4).map((relationship) => ({
196
+ symbol: strings(relationship, 1)[0] ?? "",
197
+ isReference: numberField(relationship, 2) !== 0,
198
+ isImplementation: numberField(relationship, 3) !== 0,
199
+ isTypeDefinition: numberField(relationship, 4) !== 0,
200
+ isDefinition: numberField(relationship, 5) !== 0,
201
+ })),
202
+ };
203
+ }
204
+ function safeRelativePath(repoPath, value) {
205
+ if (!value || value.includes("\0") || path.isAbsolute(value))
206
+ fail(`hostile document path ${JSON.stringify(value)}`);
207
+ const normalized = value.replaceAll("\\", "/");
208
+ if (normalized.split("/").some((part) => part === ".." || part === ""))
209
+ fail(`hostile document path ${JSON.stringify(value)}`);
210
+ const absolute = path.resolve(repoPath, normalized);
211
+ const relative = path.relative(repoPath, absolute);
212
+ if (relative.startsWith("..") || path.isAbsolute(relative))
213
+ fail(`document escapes repository: ${value}`);
214
+ let stat;
215
+ try {
216
+ stat = fs.lstatSync(absolute);
217
+ }
218
+ catch {
219
+ return fail(`document is missing: ${normalized}`);
220
+ }
221
+ if (!stat.isFile() || stat.isSymbolicLink())
222
+ fail(`document is not a regular repository file: ${normalized}`);
223
+ return normalized;
224
+ }
225
+ function symbolName(symbol, displayName) {
226
+ if (displayName.trim())
227
+ return displayName.trim();
228
+ if (symbol.startsWith("local "))
229
+ return symbol;
230
+ const descriptor = symbol.trim().split(/\s+/).at(-1) ?? symbol;
231
+ const withoutSuffix = descriptor.replace(/\([^)]*\)\.$|[#/.:!]$/, "");
232
+ const cleaned = (withoutSuffix.split(/[/#]/).at(-1) ?? descriptor).replaceAll("`", "");
233
+ return cleaned || symbol;
234
+ }
235
+ function symbolKind(_kind) {
236
+ // Do not translate a versioned producer enum into a potentially false native
237
+ // semantic kind; the original SCIP symbol remains in provenance.
238
+ return "SCIP_Symbol";
239
+ }
240
+ function parseMetadata(index) {
241
+ const metadata = messages(index, 1)[0];
242
+ if (!metadata)
243
+ return [];
244
+ const toolInfo = messages(metadata, 2)[0];
245
+ if (!toolInfo)
246
+ return [];
247
+ const name = strings(toolInfo, 1)[0];
248
+ const version = strings(toolInfo, 2)[0];
249
+ const suffix = version ? `@${version}` : "";
250
+ return name ? [`${name}${suffix}`] : [];
251
+ }
252
+ export function readScipIndex(repoPath, options) {
253
+ const started = performance.now();
254
+ const limits = { ...SCIP_DEFAULT_LIMITS, ...options };
255
+ for (const [name, value] of Object.entries(limits)) {
256
+ if (name === "path")
257
+ continue;
258
+ if (!Number.isSafeInteger(value) || value < 1)
259
+ fail(`${name} must be a positive integer`);
260
+ }
261
+ activeTimeoutMs = limits.timeoutMs;
262
+ activeDeadline = started + limits.timeoutMs;
263
+ const resolvedRepo = path.resolve(repoPath);
264
+ const inputPath = path.resolve(resolvedRepo, options.path);
265
+ const inputRelative = path.relative(resolvedRepo, inputPath);
266
+ if (inputRelative.startsWith("..") || path.isAbsolute(inputRelative))
267
+ fail("input path escapes repository");
268
+ let inputStat;
269
+ try {
270
+ inputStat = fs.lstatSync(inputPath);
271
+ }
272
+ catch {
273
+ return fail(`input is missing: ${inputRelative}`);
274
+ }
275
+ if (!inputStat.isFile() || inputStat.isSymbolicLink())
276
+ fail("input must be a regular repository file");
277
+ // A ceiling nobody can raise is just a failure, so name the flag and a value
278
+ // that would admit this file.
279
+ if (inputStat.size > limits.maxBytes)
280
+ fail(`input is ${inputStat.size.toLocaleString()} bytes, over the ` +
281
+ `${limits.maxBytes.toLocaleString()} byte limit. Raise it with ` +
282
+ `--scip-max-bytes ${inputStat.size}, or narrow the indexer that produced this file.`);
283
+ const bytes = fs.readFileSync(inputPath);
284
+ const index = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
285
+ const documents = [];
286
+ let parsedFacts = 0;
287
+ const documentCount = countLengthDelimitedFields(index, 2, limits.maxFiles);
288
+ if (documentCount > limits.maxFiles)
289
+ fail(`index covers ${documentCount.toLocaleString()} files, over the ` +
290
+ `${limits.maxFiles.toLocaleString()} file limit. Raise it with ` +
291
+ `--scip-max-files ${documentCount}, or narrow the indexer that produced this file.`);
292
+ for (const message of messages(index, 2)) {
293
+ const filePath = safeRelativePath(resolvedRepo, strings(message, 1)[0] ?? "");
294
+ const sourceStat = fs.statSync(path.join(resolvedRepo, filePath));
295
+ if (sourceStat.mtimeMs > inputStat.mtimeMs + 1)
296
+ fail(`input is stale for ${filePath}`);
297
+ const occurrenceCount = countLengthDelimitedFields(message, 2, limits.maxFacts - parsedFacts);
298
+ parsedFacts += occurrenceCount;
299
+ if (parsedFacts > limits.maxFacts)
300
+ fail(`index exceeds the ${limits.maxFacts.toLocaleString()} fact limit. ` +
301
+ `Raise it with --scip-max-facts, or narrow the indexer that produced this file.`);
302
+ const symbolCount = countLengthDelimitedFields(message, 3, limits.maxFacts - parsedFacts);
303
+ parsedFacts += symbolCount;
304
+ if (parsedFacts > limits.maxFacts)
305
+ fail(`index exceeds the ${limits.maxFacts.toLocaleString()} fact limit. ` +
306
+ `Raise it with --scip-max-facts, or narrow the indexer that produced this file.`);
307
+ const occurrenceMessages = messages(message, 2);
308
+ const symbolMessages = messages(message, 3);
309
+ for (const symbolMessage of symbolMessages) {
310
+ const remainingRelationships = Math.floor((limits.maxFacts - parsedFacts) / 4);
311
+ parsedFacts += countLengthDelimitedFields(symbolMessage, 4, remainingRelationships) * 4;
312
+ if (parsedFacts > limits.maxFacts)
313
+ fail(`index exceeds the ${limits.maxFacts.toLocaleString()} fact limit. ` +
314
+ `Raise it with --scip-max-facts, or narrow the indexer that produced this file.`);
315
+ }
316
+ documents.push({
317
+ filePath,
318
+ language: strings(message, 4)[0] ?? "unknown",
319
+ occurrences: occurrenceMessages.map(parseOccurrence),
320
+ symbols: symbolMessages.map(parseSymbolInfo),
321
+ });
322
+ if (performance.now() - started > limits.timeoutMs)
323
+ fail(`import exceeded ${limits.timeoutMs} ms limit`);
324
+ }
325
+ const scopedSymbol = (document, symbol) => symbol.startsWith("local ") ? `${document.filePath}\0${symbol}` : `\0${symbol}`;
326
+ const definitions = new Map();
327
+ for (const document of documents) {
328
+ const infos = new Map(document.symbols.map((info) => [info.symbol, info]));
329
+ for (const occurrence of document.occurrences) {
330
+ const key = scopedSymbol(document, occurrence.symbol);
331
+ if ((occurrence.roles & 1) !== 0 && !definitions.has(key)) {
332
+ definitions.set(key, {
333
+ document,
334
+ occurrence,
335
+ info: infos.get(occurrence.symbol),
336
+ });
337
+ }
338
+ }
339
+ }
340
+ const symbols = [...definitions.values()].map((definition) => ({
341
+ symbol: definition.occurrence.symbol,
342
+ name: symbolName(definition.occurrence.symbol, definition.info?.displayName ?? ""),
343
+ kind: symbolKind(definition.info?.kind ?? 0),
344
+ filePath: definition.document.filePath,
345
+ startLine: definition.occurrence.range.startLine + 1,
346
+ endLine: definition.occurrence.range.endLine + 1,
347
+ startCol: definition.occurrence.range.startCol + 1,
348
+ endCol: definition.occurrence.range.endCol + 1,
349
+ }));
350
+ const references = [];
351
+ for (const document of documents) {
352
+ for (const occurrence of document.occurrences) {
353
+ if ((occurrence.roles & 1) !== 0)
354
+ continue;
355
+ const target = definitions.get(scopedSymbol(document, occurrence.symbol));
356
+ if (!target)
357
+ continue;
358
+ references.push({
359
+ callerSymbol: null,
360
+ callerFile: document.filePath,
361
+ calleeSymbol: symbolName(occurrence.symbol, target.info?.displayName ?? ""),
362
+ calleeFile: target.document.filePath,
363
+ line: occurrence.range.startLine + 1,
364
+ column: occurrence.range.startCol + 1,
365
+ kind: "scip_reference",
366
+ });
367
+ }
368
+ for (const info of document.symbols) {
369
+ const source = definitions.get(scopedSymbol(document, info.symbol));
370
+ if (!source)
371
+ continue;
372
+ for (const relationship of info.relationships) {
373
+ const target = definitions.get(scopedSymbol(document, relationship.symbol));
374
+ if (!target)
375
+ continue;
376
+ const kinds = [];
377
+ if (relationship.isReference)
378
+ kinds.push("scip_relationship_reference");
379
+ if (relationship.isImplementation)
380
+ kinds.push("scip_implementation");
381
+ if (relationship.isTypeDefinition)
382
+ kinds.push("scip_type_definition");
383
+ if (relationship.isDefinition)
384
+ kinds.push("scip_definition");
385
+ for (const kind of kinds)
386
+ references.push({
387
+ callerSymbol: symbolName(info.symbol, info.displayName),
388
+ callerFile: source.document.filePath,
389
+ calleeSymbol: symbolName(relationship.symbol, target.info?.displayName ?? ""),
390
+ calleeFile: target.document.filePath,
391
+ line: source.occurrence.range.startLine + 1,
392
+ column: source.occurrence.range.startCol + 1,
393
+ kind,
394
+ });
395
+ }
396
+ }
397
+ }
398
+ const factCount = symbols.length + references.length;
399
+ if (factCount > limits.maxFacts)
400
+ fail(`index exceeds ${limits.maxFacts} fact limit`);
401
+ if (performance.now() - started > limits.timeoutMs)
402
+ fail(`import exceeded ${limits.timeoutMs} ms limit`);
403
+ // Byte order: imported facts are persisted and compared across runs, so their
404
+ // order has to be a function of the bytes rather than of host collation.
405
+ symbols.sort((a, b) => compareBytes(a.filePath, b.filePath) || a.startLine - b.startLine);
406
+ references.sort((a, b) => compareBytes(a.callerFile, b.callerFile) ||
407
+ a.line - b.line ||
408
+ compareBytes(a.calleeSymbol, b.calleeSymbol));
409
+ const result = {
410
+ inputPath: inputRelative.replaceAll("\\", "/"),
411
+ bytes: inputStat.size,
412
+ files: documents.length,
413
+ facts: factCount,
414
+ languages: [...new Set(documents.map((document) => document.language))].sort(compareBytes),
415
+ indexers: parseMetadata(index).sort(compareBytes),
416
+ symbols,
417
+ references,
418
+ elapsedMs: Math.round((performance.now() - started) * 1000) / 1000,
419
+ };
420
+ activeDeadline = Number.POSITIVE_INFINITY;
421
+ activeTimeoutMs = 0;
422
+ return result;
423
+ }
@@ -0,0 +1,85 @@
1
+ import path from "node:path";
2
+ import { isIndexablePath } from "./prune.js";
3
+ const SOURCE_EXTENSIONS = new Set([
4
+ ".ts",
5
+ ".tsx",
6
+ ".mts",
7
+ ".cts",
8
+ ".js",
9
+ ".jsx",
10
+ ".mjs",
11
+ ".cjs",
12
+ ".py",
13
+ ".cls",
14
+ ".trigger",
15
+ ".page",
16
+ ".component",
17
+ ".app",
18
+ ".cmp",
19
+ ".design",
20
+ ".docs",
21
+ ".evt",
22
+ ".intf",
23
+ ".svg",
24
+ ".sql",
25
+ ".pkb",
26
+ ".pks",
27
+ ".prisma",
28
+ ".java",
29
+ ".cs",
30
+ ".go",
31
+ ".rs",
32
+ ".clp",
33
+ ".ws",
34
+ ".xml",
35
+ ".tf",
36
+ ".tfvars",
37
+ ".hcl",
38
+ ".dockerfile",
39
+ ".lsif",
40
+ // Documentation participates as a link-only graph: doc-to-doc and doc-to-code
41
+ // edges, no symbols. Without it, "which docs describe this subsystem" and
42
+ // "which docs point at code that no longer exists" are unanswerable.
43
+ ".md",
44
+ ".mdx",
45
+ // CI pipeline definitions participate as link-only template references.
46
+ ".yml",
47
+ ".yaml",
48
+ // The .NET build graph: project-to-project references across services.
49
+ ".csproj",
50
+ ".sln",
51
+ ".props",
52
+ ".targets",
53
+ // Razor views/components: the C# members in their @code/@functions blocks.
54
+ ".cshtml",
55
+ ".razor",
56
+ ]);
57
+ function isSalesforceBundleMarkup(normalized) {
58
+ return (/^force-app\/main\/default\/lwc\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:html|css)$/.test(normalized) ||
59
+ /^force-app\/main\/default\/aura\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:css)$/.test(normalized));
60
+ }
61
+ function isSalesforceExperienceJson(normalized) {
62
+ return (normalized === "lwr.config.json" ||
63
+ /^force-app\/main\/default\/experiences\/[A-Za-z][A-Za-z0-9_]*\/(?:routes|views|themes)\/[^/]+\.json$/.test(normalized));
64
+ }
65
+ /**
66
+ * Canonical, path-only candidate policy shared by full-repository collection,
67
+ * file watching, repair selection, and Git-hook refresh. Content-sensitive
68
+ * formats (currently dbt manifest.json) are accepted as candidates here and
69
+ * validated by the parser before symbols are persisted.
70
+ */
71
+ export function isIndexableSourcePath(relativePath) {
72
+ if (!isIndexablePath(relativePath))
73
+ return false;
74
+ const normalized = relativePath.replaceAll("\\", "/");
75
+ const base = path.posix.basename(normalized);
76
+ if (base === "Dockerfile")
77
+ return true;
78
+ if (normalized.endsWith(".json")) {
79
+ return base === "manifest.json" || isSalesforceExperienceJson(normalized);
80
+ }
81
+ if (normalized.endsWith(".html") || normalized.endsWith(".css")) {
82
+ return isSalesforceBundleMarkup(normalized);
83
+ }
84
+ return SOURCE_EXTENSIONS.has(path.posix.extname(normalized));
85
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Production SQLite adapter for the Node runtime.
3
+ *
4
+ * knodin's engine was originally written against the compact `bun:sqlite`
5
+ * surface. Node 24 ships `node:sqlite` without a runtime flag, so this adapter
6
+ * preserves the engine's existing typed `query<Row, Params>()` contract while
7
+ * using Node's native `DatabaseSync` implementation underneath.
8
+ */
9
+ import { DatabaseSync } from "node:sqlite";
10
+ /**
11
+ * knodin's call sites pass bind params either spread (`stmt.all(a, b)`) or as a
12
+ * single array (`db.run(sql, [a, b])`). node:sqlite wants them spread — normalize a
13
+ * lone array argument back into a spread list. SQLite has no array column type,
14
+ * so a single array can only ever mean "the parameter list", never one value.
15
+ */
16
+ function normalize(params) {
17
+ if (params.length === 1 && Array.isArray(params[0]))
18
+ return params[0];
19
+ return params;
20
+ }
21
+ class Statement {
22
+ // node:sqlite's parameter union is intentionally narrower than the engine's
23
+ // generic call-site contract. Runtime validation still belongs to SQLite.
24
+ // biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
25
+ stmt;
26
+ // biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
27
+ constructor(stmt) {
28
+ this.stmt = stmt;
29
+ }
30
+ all(...params) {
31
+ return this.stmt.all(...normalize(params));
32
+ }
33
+ get(...params) {
34
+ return (this.stmt.get(...normalize(params)) ?? null);
35
+ }
36
+ run(...params) {
37
+ return this.stmt.run(...normalize(params));
38
+ }
39
+ finalize() {
40
+ // node:sqlite statements are released on GC; there is nothing to finalize.
41
+ }
42
+ }
43
+ export class Database {
44
+ db;
45
+ constructor(databasePath, options = {}) {
46
+ this.db = new DatabaseSync(databasePath, { readOnly: options.readonly === true });
47
+ }
48
+ query(sql) {
49
+ return new Statement(this.db.prepare(sql));
50
+ }
51
+ prepare(sql) {
52
+ return new Statement(this.db.prepare(sql));
53
+ }
54
+ run(sql, ...params) {
55
+ if (params.length === 0) {
56
+ // DDL / PRAGMA / BEGIN / COMMIT / multi-statement schema — `exec` avoids
57
+ // prepare() edge cases and handles more-than-one statement in one string.
58
+ this.db.exec(sql);
59
+ return { changes: 0, lastInsertRowid: 0 };
60
+ }
61
+ // biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
62
+ const statement = this.db.prepare(sql);
63
+ return statement.run(...normalize(params));
64
+ }
65
+ exec(sql) {
66
+ this.db.exec(sql);
67
+ }
68
+ close() {
69
+ this.db.close();
70
+ }
71
+ }