knodin 0.5.1 → 0.7.3
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.md +190 -535
- package/dist/bin/cli.js +173 -17
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +4 -16
- package/dist/src/artifact-refresh.js +1 -1
- package/dist/src/cli-args.js +8 -1
- package/dist/src/cli-model.js +35 -1
- package/dist/src/codeflow-replay.js +80 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-manifest.js +106 -25
- package/dist/src/competitive-runner.js +37 -3
- package/dist/src/competitive-sandbox.js +4 -4
- package/dist/src/context-export.js +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/docs-sections.js +5 -5
- package/dist/src/engine/ann-hnsw.js +5 -5
- package/dist/src/engine/embeddings.js +4 -4
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +450 -103
- package/dist/src/engine/prune.js +2 -2
- package/dist/src/engine/scip-import.js +408 -0
- package/dist/src/engine/symbol-delete.js +1 -1
- package/dist/src/execution-profile.js +203 -0
- package/dist/src/failure-diagnosis.js +69 -10
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/index-activity.js +1 -1
- package/dist/src/init-progress-worker.js +1 -1
- package/dist/src/init.js +361 -92
- package/dist/src/lifecycle-health.js +51 -14
- package/dist/src/lsp-readonly.js +1 -1
- package/dist/src/output-compression.js +1 -1
- package/dist/src/output-telemetry.js +8 -4
- package/dist/src/pr-triage.js +2 -2
- package/dist/src/progressive-evidence.js +473 -0
- package/dist/src/pure-compression-cli.js +101 -0
- package/dist/src/release-preflight.js +510 -0
- package/dist/src/repair-progress-worker.js +1 -1
- package/dist/src/repository-init-process.js +1 -1
- package/dist/src/repository-management.js +145 -3
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +23 -3
- package/dist/src/structural-fast-path.js +303 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/system-config.js +7 -7
- package/dist/src/tools/knodin-tools.js +106 -15
- package/dist/src/update-ceremony.js +158 -0
- package/dist/src/update-policy.js +4 -4
- package/dist/src/wait-for-fresh.js +2 -2
- package/dist/src/worktree-lifecycle.js +2 -2
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +7 -4
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
- package/docs/TELEMETRY.md +6 -2
- package/docs/releases/0.6.0.md +18 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/package.json +34 -3
- package/dist/src/tools/reckon-tools.js +0 -5
package/dist/src/engine/prune.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
/**
|
|
3
3
|
* Single source of truth for "which directories does knodin ignore" (R14).
|
|
4
4
|
*
|
|
5
|
-
* Before this module, the same intent — skip node_modules, .git, .
|
|
5
|
+
* Before this module, the same intent — skip node_modules, .git, .knodin,
|
|
6
6
|
* build output, caches, VCS internals — was encoded THREE separate times:
|
|
7
7
|
* 1. src/__tests__/setup.ts's `PRUNE_DIRS` regex (glob-shim exclude filter)
|
|
8
8
|
* 2. the indexer's own directory filter (`EXCLUDED_DIR_SEGMENTS` / `isIndexablePath`)
|
|
@@ -25,7 +25,7 @@ import path from "node:path";
|
|
|
25
25
|
export const PRUNE_DIR_NAMES = new Set([
|
|
26
26
|
"node_modules",
|
|
27
27
|
".git",
|
|
28
|
-
".
|
|
28
|
+
".knodin",
|
|
29
29
|
// Local state maintained by other code-intelligence tools. It is neither
|
|
30
30
|
// repository source nor stable project configuration, and indexing it makes
|
|
31
31
|
// a healthy checkout look perpetually stale as those tools update databases
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const SCIP_DEFAULT_LIMITS = {
|
|
4
|
+
maxBytes: 64 * 1024 * 1024,
|
|
5
|
+
maxFiles: 10_000,
|
|
6
|
+
maxFacts: 250_000,
|
|
7
|
+
timeoutMs: 30_000,
|
|
8
|
+
};
|
|
9
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
10
|
+
let activeDeadline = Number.POSITIVE_INFINITY;
|
|
11
|
+
let activeTimeoutMs = 0;
|
|
12
|
+
function fail(message) {
|
|
13
|
+
activeDeadline = Number.POSITIVE_INFINITY;
|
|
14
|
+
activeTimeoutMs = 0;
|
|
15
|
+
throw new Error(`knodin SCIP import: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function checkDeadline() {
|
|
18
|
+
if (performance.now() > activeDeadline)
|
|
19
|
+
fail(`import exceeded ${activeTimeoutMs} ms limit`);
|
|
20
|
+
}
|
|
21
|
+
function readVarint(bytes, offset) {
|
|
22
|
+
let value = 0;
|
|
23
|
+
let shift = 0;
|
|
24
|
+
for (let count = 0; count < 10 && offset < bytes.length; count++) {
|
|
25
|
+
const byte = bytes[offset++];
|
|
26
|
+
if (shift < 53)
|
|
27
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
28
|
+
if ((byte & 0x80) === 0)
|
|
29
|
+
return [value, offset];
|
|
30
|
+
shift += 7;
|
|
31
|
+
}
|
|
32
|
+
return fail("malformed protobuf varint");
|
|
33
|
+
}
|
|
34
|
+
function visitFields(bytes, visitor) {
|
|
35
|
+
let offset = 0;
|
|
36
|
+
while (offset < bytes.length) {
|
|
37
|
+
checkDeadline();
|
|
38
|
+
let tag;
|
|
39
|
+
[tag, offset] = readVarint(bytes, offset);
|
|
40
|
+
const number = Math.floor(tag / 8);
|
|
41
|
+
const wire = tag & 7;
|
|
42
|
+
if (number < 1)
|
|
43
|
+
fail("malformed protobuf field number");
|
|
44
|
+
if (wire === 0) {
|
|
45
|
+
let value;
|
|
46
|
+
[value, offset] = readVarint(bytes, offset);
|
|
47
|
+
visitor(number, wire, value);
|
|
48
|
+
}
|
|
49
|
+
else if (wire === 2) {
|
|
50
|
+
let length;
|
|
51
|
+
[length, offset] = readVarint(bytes, offset);
|
|
52
|
+
if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
|
|
53
|
+
fail("malformed protobuf length");
|
|
54
|
+
visitor(number, wire, bytes.subarray(offset, offset + length));
|
|
55
|
+
offset += length;
|
|
56
|
+
}
|
|
57
|
+
else if (wire === 1) {
|
|
58
|
+
offset += 8;
|
|
59
|
+
}
|
|
60
|
+
else if (wire === 5) {
|
|
61
|
+
offset += 4;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
fail(`unsupported protobuf wire type ${wire}`);
|
|
65
|
+
}
|
|
66
|
+
if (offset > bytes.length)
|
|
67
|
+
fail("truncated protobuf field");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function countLengthDelimitedFields(bytes, target, maximum) {
|
|
71
|
+
let count = 0;
|
|
72
|
+
let offset = 0;
|
|
73
|
+
while (offset < bytes.length) {
|
|
74
|
+
checkDeadline();
|
|
75
|
+
let tag;
|
|
76
|
+
[tag, offset] = readVarint(bytes, offset);
|
|
77
|
+
const number = Math.floor(tag / 8);
|
|
78
|
+
const wire = tag & 7;
|
|
79
|
+
if (number < 1)
|
|
80
|
+
fail("malformed protobuf field number");
|
|
81
|
+
if (wire === 0) {
|
|
82
|
+
[, offset] = readVarint(bytes, offset);
|
|
83
|
+
}
|
|
84
|
+
else if (wire === 2) {
|
|
85
|
+
let length;
|
|
86
|
+
[length, offset] = readVarint(bytes, offset);
|
|
87
|
+
if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
|
|
88
|
+
fail("malformed protobuf length");
|
|
89
|
+
if (number === target && ++count > maximum)
|
|
90
|
+
return count;
|
|
91
|
+
offset += length;
|
|
92
|
+
}
|
|
93
|
+
else if (wire === 1)
|
|
94
|
+
offset += 8;
|
|
95
|
+
else if (wire === 5)
|
|
96
|
+
offset += 4;
|
|
97
|
+
else
|
|
98
|
+
fail(`unsupported protobuf wire type ${wire}`);
|
|
99
|
+
if (offset > bytes.length)
|
|
100
|
+
fail("truncated protobuf field");
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
function strings(message, number) {
|
|
105
|
+
const result = [];
|
|
106
|
+
visitFields(message, (fieldNumber, wire, value) => {
|
|
107
|
+
if (fieldNumber === number && wire === 2)
|
|
108
|
+
result.push(textDecoder.decode(value));
|
|
109
|
+
});
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function messages(message, number) {
|
|
113
|
+
const result = [];
|
|
114
|
+
visitFields(message, (fieldNumber, wire, value) => {
|
|
115
|
+
if (fieldNumber === number && wire === 2)
|
|
116
|
+
result.push(value);
|
|
117
|
+
});
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function numberField(message, number) {
|
|
121
|
+
let result = 0;
|
|
122
|
+
let found = false;
|
|
123
|
+
visitFields(message, (fieldNumber, wire, value) => {
|
|
124
|
+
if (!found && fieldNumber === number && wire === 0) {
|
|
125
|
+
result = value;
|
|
126
|
+
found = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function packedNumbers(message, number) {
|
|
132
|
+
const result = [];
|
|
133
|
+
visitFields(message, (fieldNumber, wire, value) => {
|
|
134
|
+
if (fieldNumber !== number)
|
|
135
|
+
return;
|
|
136
|
+
if (wire === 0)
|
|
137
|
+
result.push(value);
|
|
138
|
+
if (wire === 2) {
|
|
139
|
+
const bytes = value;
|
|
140
|
+
let offset = 0;
|
|
141
|
+
while (offset < bytes.length) {
|
|
142
|
+
let packedValue;
|
|
143
|
+
[packedValue, offset] = readVarint(bytes, offset);
|
|
144
|
+
result.push(packedValue);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
function parseRange(values) {
|
|
151
|
+
if (values.length !== 3 && values.length !== 4)
|
|
152
|
+
fail("range must contain three or four integers");
|
|
153
|
+
const [startLine, startCol] = values;
|
|
154
|
+
const endLine = values.length === 3 ? startLine : values[2];
|
|
155
|
+
const endCol = values.length === 3 ? values[2] : values[3];
|
|
156
|
+
if ([startLine, startCol, endLine, endCol].some((value) => !Number.isSafeInteger(value)))
|
|
157
|
+
fail("range contains an unsafe integer");
|
|
158
|
+
if (endLine < startLine || (endLine === startLine && endCol < startCol))
|
|
159
|
+
fail("range is reversed");
|
|
160
|
+
return { startLine, startCol, endLine, endCol };
|
|
161
|
+
}
|
|
162
|
+
function parseOccurrence(message) {
|
|
163
|
+
const singleLine = messages(message, 8)[0];
|
|
164
|
+
const multiLine = messages(message, 9)[0];
|
|
165
|
+
const symbol = strings(message, 2)[0] ?? "";
|
|
166
|
+
let range;
|
|
167
|
+
if (singleLine) {
|
|
168
|
+
const line = numberField(singleLine, 1);
|
|
169
|
+
range = parseRange([line, numberField(singleLine, 2), numberField(singleLine, 3)]);
|
|
170
|
+
}
|
|
171
|
+
else if (multiLine) {
|
|
172
|
+
range = parseRange([
|
|
173
|
+
numberField(multiLine, 1),
|
|
174
|
+
numberField(multiLine, 2),
|
|
175
|
+
numberField(multiLine, 3),
|
|
176
|
+
numberField(multiLine, 4),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
range = parseRange(packedNumbers(message, 1));
|
|
181
|
+
}
|
|
182
|
+
if (!symbol)
|
|
183
|
+
fail("occurrence is missing its symbol");
|
|
184
|
+
return { range, symbol, roles: numberField(message, 3) };
|
|
185
|
+
}
|
|
186
|
+
function parseSymbolInfo(message) {
|
|
187
|
+
const symbol = strings(message, 1)[0] ?? "";
|
|
188
|
+
if (!symbol)
|
|
189
|
+
fail("symbol information is missing its symbol");
|
|
190
|
+
return {
|
|
191
|
+
symbol,
|
|
192
|
+
displayName: strings(message, 6)[0] ?? "",
|
|
193
|
+
kind: numberField(message, 5),
|
|
194
|
+
relationships: messages(message, 4).map((relationship) => ({
|
|
195
|
+
symbol: strings(relationship, 1)[0] ?? "",
|
|
196
|
+
isReference: numberField(relationship, 2) !== 0,
|
|
197
|
+
isImplementation: numberField(relationship, 3) !== 0,
|
|
198
|
+
isTypeDefinition: numberField(relationship, 4) !== 0,
|
|
199
|
+
isDefinition: numberField(relationship, 5) !== 0,
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function safeRelativePath(repoPath, value) {
|
|
204
|
+
if (!value || value.includes("\0") || path.isAbsolute(value))
|
|
205
|
+
fail(`hostile document path ${JSON.stringify(value)}`);
|
|
206
|
+
const normalized = value.replaceAll("\\", "/");
|
|
207
|
+
if (normalized.split("/").some((part) => part === ".." || part === ""))
|
|
208
|
+
fail(`hostile document path ${JSON.stringify(value)}`);
|
|
209
|
+
const absolute = path.resolve(repoPath, normalized);
|
|
210
|
+
const relative = path.relative(repoPath, absolute);
|
|
211
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
212
|
+
fail(`document escapes repository: ${value}`);
|
|
213
|
+
let stat;
|
|
214
|
+
try {
|
|
215
|
+
stat = fs.lstatSync(absolute);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return fail(`document is missing: ${normalized}`);
|
|
219
|
+
}
|
|
220
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
221
|
+
fail(`document is not a regular repository file: ${normalized}`);
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
function symbolName(symbol, displayName) {
|
|
225
|
+
if (displayName.trim())
|
|
226
|
+
return displayName.trim();
|
|
227
|
+
if (symbol.startsWith("local "))
|
|
228
|
+
return symbol;
|
|
229
|
+
const descriptor = symbol.trim().split(/\s+/).at(-1) ?? symbol;
|
|
230
|
+
const withoutSuffix = descriptor.replace(/\([^)]*\)\.$|[#/.:!]$/, "");
|
|
231
|
+
const cleaned = (withoutSuffix.split(/[/#]/).at(-1) ?? descriptor).replaceAll("`", "");
|
|
232
|
+
return cleaned || symbol;
|
|
233
|
+
}
|
|
234
|
+
function symbolKind(_kind) {
|
|
235
|
+
// Do not translate a versioned producer enum into a potentially false native
|
|
236
|
+
// semantic kind; the original SCIP symbol remains in provenance.
|
|
237
|
+
return "SCIP_Symbol";
|
|
238
|
+
}
|
|
239
|
+
function parseMetadata(index) {
|
|
240
|
+
const metadata = messages(index, 1)[0];
|
|
241
|
+
if (!metadata)
|
|
242
|
+
return [];
|
|
243
|
+
const toolInfo = messages(metadata, 2)[0];
|
|
244
|
+
if (!toolInfo)
|
|
245
|
+
return [];
|
|
246
|
+
const name = strings(toolInfo, 1)[0];
|
|
247
|
+
const version = strings(toolInfo, 2)[0];
|
|
248
|
+
return name ? [`${name}${version ? `@${version}` : ""}`] : [];
|
|
249
|
+
}
|
|
250
|
+
export function readScipIndex(repoPath, options) {
|
|
251
|
+
const started = performance.now();
|
|
252
|
+
const limits = { ...SCIP_DEFAULT_LIMITS, ...options };
|
|
253
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
254
|
+
if (name === "path")
|
|
255
|
+
continue;
|
|
256
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
257
|
+
fail(`${name} must be a positive integer`);
|
|
258
|
+
}
|
|
259
|
+
activeTimeoutMs = limits.timeoutMs;
|
|
260
|
+
activeDeadline = started + limits.timeoutMs;
|
|
261
|
+
const resolvedRepo = path.resolve(repoPath);
|
|
262
|
+
const inputPath = path.resolve(resolvedRepo, options.path);
|
|
263
|
+
const inputRelative = path.relative(resolvedRepo, inputPath);
|
|
264
|
+
if (inputRelative.startsWith("..") || path.isAbsolute(inputRelative))
|
|
265
|
+
fail("input path escapes repository");
|
|
266
|
+
let inputStat;
|
|
267
|
+
try {
|
|
268
|
+
inputStat = fs.lstatSync(inputPath);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return fail(`input is missing: ${inputRelative}`);
|
|
272
|
+
}
|
|
273
|
+
if (!inputStat.isFile() || inputStat.isSymbolicLink())
|
|
274
|
+
fail("input must be a regular repository file");
|
|
275
|
+
if (inputStat.size > limits.maxBytes)
|
|
276
|
+
fail(`input exceeds ${limits.maxBytes} byte limit`);
|
|
277
|
+
const bytes = fs.readFileSync(inputPath);
|
|
278
|
+
const index = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
279
|
+
const documents = [];
|
|
280
|
+
let parsedFacts = 0;
|
|
281
|
+
const documentCount = countLengthDelimitedFields(index, 2, limits.maxFiles);
|
|
282
|
+
if (documentCount > limits.maxFiles)
|
|
283
|
+
fail(`index exceeds ${limits.maxFiles} file limit`);
|
|
284
|
+
for (const message of messages(index, 2)) {
|
|
285
|
+
const filePath = safeRelativePath(resolvedRepo, strings(message, 1)[0] ?? "");
|
|
286
|
+
const sourceStat = fs.statSync(path.join(resolvedRepo, filePath));
|
|
287
|
+
if (sourceStat.mtimeMs > inputStat.mtimeMs + 1)
|
|
288
|
+
fail(`input is stale for ${filePath}`);
|
|
289
|
+
const occurrenceCount = countLengthDelimitedFields(message, 2, limits.maxFacts - parsedFacts);
|
|
290
|
+
parsedFacts += occurrenceCount;
|
|
291
|
+
if (parsedFacts > limits.maxFacts)
|
|
292
|
+
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
293
|
+
const symbolCount = countLengthDelimitedFields(message, 3, limits.maxFacts - parsedFacts);
|
|
294
|
+
parsedFacts += symbolCount;
|
|
295
|
+
if (parsedFacts > limits.maxFacts)
|
|
296
|
+
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
297
|
+
const occurrenceMessages = messages(message, 2);
|
|
298
|
+
const symbolMessages = messages(message, 3);
|
|
299
|
+
for (const symbolMessage of symbolMessages) {
|
|
300
|
+
const remainingRelationships = Math.floor((limits.maxFacts - parsedFacts) / 4);
|
|
301
|
+
parsedFacts += countLengthDelimitedFields(symbolMessage, 4, remainingRelationships) * 4;
|
|
302
|
+
if (parsedFacts > limits.maxFacts)
|
|
303
|
+
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
304
|
+
}
|
|
305
|
+
documents.push({
|
|
306
|
+
filePath,
|
|
307
|
+
language: strings(message, 4)[0] ?? "unknown",
|
|
308
|
+
occurrences: occurrenceMessages.map(parseOccurrence),
|
|
309
|
+
symbols: symbolMessages.map(parseSymbolInfo),
|
|
310
|
+
});
|
|
311
|
+
if (performance.now() - started > limits.timeoutMs)
|
|
312
|
+
fail(`import exceeded ${limits.timeoutMs} ms limit`);
|
|
313
|
+
}
|
|
314
|
+
const scopedSymbol = (document, symbol) => symbol.startsWith("local ") ? `${document.filePath}\0${symbol}` : `\0${symbol}`;
|
|
315
|
+
const definitions = new Map();
|
|
316
|
+
for (const document of documents) {
|
|
317
|
+
const infos = new Map(document.symbols.map((info) => [info.symbol, info]));
|
|
318
|
+
for (const occurrence of document.occurrences) {
|
|
319
|
+
const key = scopedSymbol(document, occurrence.symbol);
|
|
320
|
+
if ((occurrence.roles & 1) !== 0 && !definitions.has(key)) {
|
|
321
|
+
definitions.set(key, {
|
|
322
|
+
document,
|
|
323
|
+
occurrence,
|
|
324
|
+
info: infos.get(occurrence.symbol),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const symbols = [...definitions.values()].map((definition) => ({
|
|
330
|
+
symbol: definition.occurrence.symbol,
|
|
331
|
+
name: symbolName(definition.occurrence.symbol, definition.info?.displayName ?? ""),
|
|
332
|
+
kind: symbolKind(definition.info?.kind ?? 0),
|
|
333
|
+
filePath: definition.document.filePath,
|
|
334
|
+
startLine: definition.occurrence.range.startLine + 1,
|
|
335
|
+
endLine: definition.occurrence.range.endLine + 1,
|
|
336
|
+
startCol: definition.occurrence.range.startCol + 1,
|
|
337
|
+
endCol: definition.occurrence.range.endCol + 1,
|
|
338
|
+
}));
|
|
339
|
+
const references = [];
|
|
340
|
+
for (const document of documents) {
|
|
341
|
+
for (const occurrence of document.occurrences) {
|
|
342
|
+
if ((occurrence.roles & 1) !== 0)
|
|
343
|
+
continue;
|
|
344
|
+
const target = definitions.get(scopedSymbol(document, occurrence.symbol));
|
|
345
|
+
if (!target)
|
|
346
|
+
continue;
|
|
347
|
+
references.push({
|
|
348
|
+
callerSymbol: null,
|
|
349
|
+
callerFile: document.filePath,
|
|
350
|
+
calleeSymbol: symbolName(occurrence.symbol, target.info?.displayName ?? ""),
|
|
351
|
+
calleeFile: target.document.filePath,
|
|
352
|
+
line: occurrence.range.startLine + 1,
|
|
353
|
+
column: occurrence.range.startCol + 1,
|
|
354
|
+
kind: "scip_reference",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
for (const info of document.symbols) {
|
|
358
|
+
const source = definitions.get(scopedSymbol(document, info.symbol));
|
|
359
|
+
if (!source)
|
|
360
|
+
continue;
|
|
361
|
+
for (const relationship of info.relationships) {
|
|
362
|
+
const target = definitions.get(scopedSymbol(document, relationship.symbol));
|
|
363
|
+
if (!target)
|
|
364
|
+
continue;
|
|
365
|
+
const kinds = [];
|
|
366
|
+
if (relationship.isReference)
|
|
367
|
+
kinds.push("scip_relationship_reference");
|
|
368
|
+
if (relationship.isImplementation)
|
|
369
|
+
kinds.push("scip_implementation");
|
|
370
|
+
if (relationship.isTypeDefinition)
|
|
371
|
+
kinds.push("scip_type_definition");
|
|
372
|
+
if (relationship.isDefinition)
|
|
373
|
+
kinds.push("scip_definition");
|
|
374
|
+
for (const kind of kinds)
|
|
375
|
+
references.push({
|
|
376
|
+
callerSymbol: symbolName(info.symbol, info.displayName),
|
|
377
|
+
callerFile: source.document.filePath,
|
|
378
|
+
calleeSymbol: symbolName(relationship.symbol, target.info?.displayName ?? ""),
|
|
379
|
+
calleeFile: target.document.filePath,
|
|
380
|
+
line: source.occurrence.range.startLine + 1,
|
|
381
|
+
column: source.occurrence.range.startCol + 1,
|
|
382
|
+
kind,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const factCount = symbols.length + references.length;
|
|
388
|
+
if (factCount > limits.maxFacts)
|
|
389
|
+
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
390
|
+
if (performance.now() - started > limits.timeoutMs)
|
|
391
|
+
fail(`import exceeded ${limits.timeoutMs} ms limit`);
|
|
392
|
+
const result = {
|
|
393
|
+
inputPath: inputRelative.replaceAll("\\", "/"),
|
|
394
|
+
bytes: inputStat.size,
|
|
395
|
+
files: documents.length,
|
|
396
|
+
facts: factCount,
|
|
397
|
+
languages: [...new Set(documents.map((document) => document.language))].sort(),
|
|
398
|
+
indexers: parseMetadata(index).sort(),
|
|
399
|
+
symbols: symbols.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine),
|
|
400
|
+
references: references.sort((a, b) => a.callerFile.localeCompare(b.callerFile) ||
|
|
401
|
+
a.line - b.line ||
|
|
402
|
+
a.calleeSymbol.localeCompare(b.calleeSymbol)),
|
|
403
|
+
elapsedMs: Math.round((performance.now() - started) * 1000) / 1000,
|
|
404
|
+
};
|
|
405
|
+
activeDeadline = Number.POSITIVE_INFINITY;
|
|
406
|
+
activeTimeoutMs = 0;
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
@@ -45,7 +45,7 @@ export function deleteSymbolsForFile(db, filePath) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Delete every symbol whose file path matches a LIKE pattern, plus its
|
|
47
47
|
* embeddings. Used by the dbt path, whose symbols live under synthetic
|
|
48
|
-
* `.
|
|
48
|
+
* `.knodin/dbt/%` paths rather than one real file.
|
|
49
49
|
*/
|
|
50
50
|
export function deleteSymbolsMatchingPath(db, likePattern) {
|
|
51
51
|
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath LIKE ?)", [likePattern]);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { compressOutput } from "./output-compression.js";
|
|
6
|
+
function defaultGlobalConfigPath() {
|
|
7
|
+
const base = process.env.XDG_CONFIG_HOME
|
|
8
|
+
? path.resolve(process.env.XDG_CONFIG_HOME)
|
|
9
|
+
: path.join(os.homedir(), ".config");
|
|
10
|
+
return path.join(base, "knodin", "execution.json");
|
|
11
|
+
}
|
|
12
|
+
function readDocument(file) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sandboxLiteral(value) {
|
|
21
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
22
|
+
}
|
|
23
|
+
function macosPolicy(repo, _executable) {
|
|
24
|
+
const protectedRoots = ["/Users", "/Volumes", "/Network", "/private/tmp", "/private/var/folders"];
|
|
25
|
+
return [
|
|
26
|
+
"(version 1)",
|
|
27
|
+
"(allow default)",
|
|
28
|
+
"(deny network*)",
|
|
29
|
+
"(deny process-fork)",
|
|
30
|
+
`(deny file-read* ${protectedRoots.map((item) => `(subpath ${sandboxLiteral(item)})`).join(" ")})`,
|
|
31
|
+
"(deny file-write*)",
|
|
32
|
+
`(allow file-read* file-write* (subpath ${sandboxLiteral(repo)}) (literal "/dev/null"))`,
|
|
33
|
+
].join(" ");
|
|
34
|
+
}
|
|
35
|
+
function unavailable(profile, warning) {
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
status: "unavailable",
|
|
39
|
+
profile,
|
|
40
|
+
exit: { code: null, signal: null },
|
|
41
|
+
containment: {
|
|
42
|
+
level: "containment-unavailable",
|
|
43
|
+
platform: process.platform,
|
|
44
|
+
adapter: "unavailable",
|
|
45
|
+
filesystem: "unavailable",
|
|
46
|
+
network: "unavailable",
|
|
47
|
+
processes: "unavailable",
|
|
48
|
+
warning,
|
|
49
|
+
},
|
|
50
|
+
limits: { timeoutMs: 0, maxOutputBytes: 0, maxProcesses: 1 },
|
|
51
|
+
output: null,
|
|
52
|
+
audit: { commandTextRecorded: false, profileRecorded: true },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function validateProfile(profileName, value) {
|
|
56
|
+
if (!value || typeof value !== "object")
|
|
57
|
+
throw new Error(`execution profile ${profileName} is invalid`);
|
|
58
|
+
const profile = value;
|
|
59
|
+
if (!path.isAbsolute(profile.executable ?? ""))
|
|
60
|
+
throw new Error(`execution profile ${profileName}: executable must be absolute`);
|
|
61
|
+
if (!Array.isArray(profile.args) ||
|
|
62
|
+
profile.args.some((arg) => typeof arg !== "string" || arg.includes("\0")))
|
|
63
|
+
throw new Error(`execution profile ${profileName}: args must be a NUL-free string array`);
|
|
64
|
+
if (profile.network !== "deny")
|
|
65
|
+
throw new Error(`execution profile ${profileName}: network must be deny`);
|
|
66
|
+
if (!Number.isInteger(profile.timeoutMs) ||
|
|
67
|
+
(profile.timeoutMs ?? 0) < 1 ||
|
|
68
|
+
(profile.timeoutMs ?? 0) > 300_000)
|
|
69
|
+
throw new Error(`execution profile ${profileName}: timeoutMs must be 1..300000`);
|
|
70
|
+
if (!Number.isInteger(profile.maxOutputBytes) ||
|
|
71
|
+
(profile.maxOutputBytes ?? 0) < 256 ||
|
|
72
|
+
(profile.maxOutputBytes ?? 0) > 16 * 1024 * 1024)
|
|
73
|
+
throw new Error(`execution profile ${profileName}: maxOutputBytes must be 256..16777216`);
|
|
74
|
+
if (profile.maxProcesses !== 1)
|
|
75
|
+
throw new Error(`execution profile ${profileName}: maxProcesses must be 1 for hard fork denial`);
|
|
76
|
+
if (profile.containment !== undefined && profile.containment !== "fully-contained")
|
|
77
|
+
throw new Error(`execution profile ${profileName}: only fully-contained is supported`);
|
|
78
|
+
return profile;
|
|
79
|
+
}
|
|
80
|
+
function appendAudit(repo, record) {
|
|
81
|
+
const destination = path.join(repo, ".knodin", "execution-audit.jsonl");
|
|
82
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
83
|
+
fs.appendFileSync(destination, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
export async function runExecutionProfile(options) {
|
|
86
|
+
const repo = fs.realpathSync(options.repo);
|
|
87
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(options.profile))
|
|
88
|
+
throw new Error("execution profile identity must match [a-z][a-z0-9_-]{0,63}");
|
|
89
|
+
const globalDocument = readDocument(options.globalConfigPath ?? defaultGlobalConfigPath());
|
|
90
|
+
const repositoryDocument = readDocument(path.join(repo, ".knodin", "execution.json"));
|
|
91
|
+
if (globalDocument?.execution?.enabled !== true)
|
|
92
|
+
return unavailable(options.profile, "execution is disabled globally");
|
|
93
|
+
if (repositoryDocument?.execution?.enabled !== true)
|
|
94
|
+
return unavailable(options.profile, "execution is disabled for this repository");
|
|
95
|
+
const configured = repositoryDocument.execution.profiles?.[options.profile];
|
|
96
|
+
if (!configured)
|
|
97
|
+
throw new Error(`execution profile not found: ${options.profile}`);
|
|
98
|
+
const profile = validateProfile(options.profile, configured);
|
|
99
|
+
if (process.platform !== "darwin" || !fs.existsSync("/usr/bin/sandbox-exec"))
|
|
100
|
+
return unavailable(options.profile, "no certified containment adapter is available on this platform");
|
|
101
|
+
const executable = fs.realpathSync(profile.executable);
|
|
102
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(repo, ".knodin", "execution-tmp-"));
|
|
103
|
+
const environment = {
|
|
104
|
+
PATH: `${path.dirname(executable)}:/usr/bin:/bin`,
|
|
105
|
+
LANG: "C.UTF-8",
|
|
106
|
+
LC_ALL: "C.UTF-8",
|
|
107
|
+
TMPDIR: temporaryDirectory,
|
|
108
|
+
};
|
|
109
|
+
const policy = macosPolicy(repo, executable);
|
|
110
|
+
const proc = childProcess.spawn("/usr/bin/sandbox-exec", ["-p", policy, executable, ...profile.args], {
|
|
111
|
+
cwd: repo,
|
|
112
|
+
env: environment,
|
|
113
|
+
shell: false,
|
|
114
|
+
detached: true,
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
let stdout = Buffer.alloc(0);
|
|
118
|
+
let stderr = Buffer.alloc(0);
|
|
119
|
+
let reason = "exit";
|
|
120
|
+
let stopped = false;
|
|
121
|
+
const stop = (next) => {
|
|
122
|
+
if (stopped || reason !== "exit")
|
|
123
|
+
return;
|
|
124
|
+
reason = next;
|
|
125
|
+
if (proc.pid)
|
|
126
|
+
try {
|
|
127
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
};
|
|
131
|
+
const append = (stream, chunk) => {
|
|
132
|
+
const remaining = Math.max(0, profile.maxOutputBytes - stdout.length - stderr.length);
|
|
133
|
+
const accepted = chunk.subarray(0, remaining);
|
|
134
|
+
if (stream === "stdout")
|
|
135
|
+
stdout = Buffer.concat([stdout, accepted]);
|
|
136
|
+
else
|
|
137
|
+
stderr = Buffer.concat([stderr, accepted]);
|
|
138
|
+
if (accepted.length < chunk.length)
|
|
139
|
+
stop("output-limit");
|
|
140
|
+
};
|
|
141
|
+
proc.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
142
|
+
proc.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
143
|
+
const timer = setTimeout(() => stop("timeout"), profile.timeoutMs);
|
|
144
|
+
const closed = await new Promise((resolve, reject) => {
|
|
145
|
+
proc.once("error", reject);
|
|
146
|
+
proc.once("close", (code, signal) => resolve({ code, signal }));
|
|
147
|
+
});
|
|
148
|
+
stopped = true;
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
151
|
+
const combined = [
|
|
152
|
+
...(stdout.length ? [{ stream: "stdout", text: stdout.toString("utf8") }] : []),
|
|
153
|
+
...(stderr.length ? [{ stream: "stderr", text: stderr.toString("utf8") }] : []),
|
|
154
|
+
];
|
|
155
|
+
const output = compressOutput(repo, {
|
|
156
|
+
events: combined,
|
|
157
|
+
exitCode: closed.code,
|
|
158
|
+
signal: closed.signal,
|
|
159
|
+
lineBudget: 200,
|
|
160
|
+
byteBudget: Math.min(profile.maxOutputBytes, 16_384),
|
|
161
|
+
retain: true,
|
|
162
|
+
});
|
|
163
|
+
const finalReason = reason;
|
|
164
|
+
const status = finalReason === "timeout"
|
|
165
|
+
? "timed-out"
|
|
166
|
+
: finalReason === "output-limit"
|
|
167
|
+
? "output-limit"
|
|
168
|
+
: closed.code === 0
|
|
169
|
+
? "completed"
|
|
170
|
+
: "failed";
|
|
171
|
+
const result = {
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
status,
|
|
174
|
+
profile: options.profile,
|
|
175
|
+
exit: closed,
|
|
176
|
+
containment: {
|
|
177
|
+
level: "fully-contained",
|
|
178
|
+
platform: process.platform,
|
|
179
|
+
adapter: "macos-sandbox-exec",
|
|
180
|
+
filesystem: "repository-write-user-data-read-bounded",
|
|
181
|
+
network: "deny",
|
|
182
|
+
processes: "fork-denied",
|
|
183
|
+
warning: "macOS sandbox-exec is deprecated. Writes are repository-bound; system runtime paths remain readable while user homes, mounted volumes, network paths, and shared temporary roots are read-denied outside the repository.",
|
|
184
|
+
},
|
|
185
|
+
limits: {
|
|
186
|
+
timeoutMs: profile.timeoutMs,
|
|
187
|
+
maxOutputBytes: profile.maxOutputBytes,
|
|
188
|
+
maxProcesses: 1,
|
|
189
|
+
},
|
|
190
|
+
output,
|
|
191
|
+
audit: { commandTextRecorded: false, profileRecorded: true },
|
|
192
|
+
};
|
|
193
|
+
appendAudit(repo, {
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
profile: options.profile,
|
|
196
|
+
status,
|
|
197
|
+
exitCode: closed.code,
|
|
198
|
+
signal: closed.signal,
|
|
199
|
+
containment: result.containment.level,
|
|
200
|
+
outputArtifactId: output.artifact.id,
|
|
201
|
+
});
|
|
202
|
+
return result;
|
|
203
|
+
}
|