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
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import zlib from "node:zlib";
|
|
6
|
+
const CONFIG_PATH = ".knodin/diagnostics/config.json";
|
|
7
|
+
const JOURNAL_PATH = ".knodin/diagnostics/events.jsonl";
|
|
8
|
+
const JOURNAL_LOCK_PATH = ".knodin/diagnostics/events.lock";
|
|
9
|
+
const DEFAULT_RETENTION_DAYS = 14;
|
|
10
|
+
const MAX_RETENTION_DAYS = 365;
|
|
11
|
+
const MAX_RECORDS = 500;
|
|
12
|
+
const MAX_LOG_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_BUNDLE_BYTES = 20 * 1024 * 1024;
|
|
14
|
+
const MAX_JOURNAL_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
const OPERATIONS = new Set([
|
|
16
|
+
"init",
|
|
17
|
+
"configure",
|
|
18
|
+
"index",
|
|
19
|
+
"doctor",
|
|
20
|
+
"status",
|
|
21
|
+
"wait",
|
|
22
|
+
"repair",
|
|
23
|
+
"serve",
|
|
24
|
+
"context",
|
|
25
|
+
"explain",
|
|
26
|
+
"review",
|
|
27
|
+
"map",
|
|
28
|
+
"search",
|
|
29
|
+
"query",
|
|
30
|
+
"rename",
|
|
31
|
+
"wiki",
|
|
32
|
+
"visualize",
|
|
33
|
+
"pack",
|
|
34
|
+
"compress",
|
|
35
|
+
"prs",
|
|
36
|
+
"worktrees",
|
|
37
|
+
"telemetry",
|
|
38
|
+
"diagnostics",
|
|
39
|
+
"system",
|
|
40
|
+
"repos",
|
|
41
|
+
"repositories",
|
|
42
|
+
"update",
|
|
43
|
+
"docs",
|
|
44
|
+
"unknown",
|
|
45
|
+
]);
|
|
46
|
+
function containedPath(repoPath, requested, purpose) {
|
|
47
|
+
const repo = fs.realpathSync(repoPath);
|
|
48
|
+
const target = path.resolve(repo, requested);
|
|
49
|
+
if (target !== repo && !target.startsWith(`${repo}${path.sep}`))
|
|
50
|
+
throw new Error(`knodin diagnostics ${purpose}: path must stay inside the repository`);
|
|
51
|
+
let cursor = repo;
|
|
52
|
+
for (const segment of path.relative(repo, target).split(path.sep).filter(Boolean)) {
|
|
53
|
+
cursor = path.join(cursor, segment);
|
|
54
|
+
try {
|
|
55
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
56
|
+
throw new Error(`knodin diagnostics ${purpose}: refusing symlinked path`);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === "ENOENT")
|
|
60
|
+
break;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { repo, target };
|
|
65
|
+
}
|
|
66
|
+
function atomicPrivateWrite(target, data) {
|
|
67
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
68
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
69
|
+
fs.writeFileSync(temporary, data, { mode: 0o600, flag: "wx" });
|
|
70
|
+
fs.renameSync(temporary, target);
|
|
71
|
+
}
|
|
72
|
+
function withJournalLock(repo, run) {
|
|
73
|
+
const { target: lock } = containedPath(repo, JOURNAL_LOCK_PATH, "record");
|
|
74
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true, mode: 0o700 });
|
|
75
|
+
for (let attempt = 0;; attempt++) {
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(lock, { mode: 0o700 });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code !== "EEXIST")
|
|
82
|
+
throw error;
|
|
83
|
+
try {
|
|
84
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 30_000) {
|
|
85
|
+
fs.rmdirSync(lock);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (inspectionError) {
|
|
90
|
+
if (inspectionError.code === "ENOENT")
|
|
91
|
+
continue;
|
|
92
|
+
throw inspectionError;
|
|
93
|
+
}
|
|
94
|
+
if (attempt >= 100)
|
|
95
|
+
throw new Error("knodin diagnostics record: journal is busy");
|
|
96
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return run();
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
try {
|
|
104
|
+
fs.rmdirSync(lock);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Recording is best-effort; stale-lock recovery handles interrupted cleanup.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validRetention(days) {
|
|
112
|
+
if (!Number.isInteger(days) || days < 1 || days > MAX_RETENTION_DAYS)
|
|
113
|
+
throw new Error(`knodin diagnostics: retention days must be an integer from 1 to ${MAX_RETENTION_DAYS}`);
|
|
114
|
+
return days;
|
|
115
|
+
}
|
|
116
|
+
function readConfig(repoPath) {
|
|
117
|
+
const { target } = containedPath(repoPath, CONFIG_PATH, "status");
|
|
118
|
+
if (!fs.existsSync(target))
|
|
119
|
+
return null;
|
|
120
|
+
try {
|
|
121
|
+
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
122
|
+
if (value.schemaVersion !== 1 || value.enabled !== true)
|
|
123
|
+
return null;
|
|
124
|
+
validRetention(value.retentionDays);
|
|
125
|
+
if (!Number.isInteger(value.generation) || value.generation < 1)
|
|
126
|
+
value.generation = 1;
|
|
127
|
+
if (typeof value.sessionId !== "string" || !/^[a-f0-9]{32}$/.test(value.sessionId))
|
|
128
|
+
value.sessionId = crypto
|
|
129
|
+
.createHash("sha256")
|
|
130
|
+
.update(value.enabledAt)
|
|
131
|
+
.digest("hex")
|
|
132
|
+
.slice(0, 32);
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function safeLabel(value, fallback) {
|
|
140
|
+
return value && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,63}$/.test(value) ? value : fallback;
|
|
141
|
+
}
|
|
142
|
+
function errorCode(error) {
|
|
143
|
+
const code = error?.code;
|
|
144
|
+
return typeof code === "string" &&
|
|
145
|
+
/^(?:E[A-Z0-9]+|ERR_[A-Z0-9_]+|SQLITE_[A-Z0-9_]+|KNODIN_[A-Z0-9_]+)$/.test(code)
|
|
146
|
+
? code
|
|
147
|
+
: null;
|
|
148
|
+
}
|
|
149
|
+
function scrubText(raw, repo) {
|
|
150
|
+
let value = raw;
|
|
151
|
+
let redactions = 0;
|
|
152
|
+
const replace = (pattern, replacement) => {
|
|
153
|
+
value = value.replace(pattern, () => {
|
|
154
|
+
redactions++;
|
|
155
|
+
return replacement;
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
for (const exact of [repo, os.homedir()].filter(Boolean).sort((a, b) => b.length - a.length))
|
|
159
|
+
replace(new RegExp(exact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "<path>");
|
|
160
|
+
replace(/\b(?:ghp|github_pat|sk|xox[baprs])-[-A-Za-z0-9_]{10,}\b/g, "<secret>");
|
|
161
|
+
replace(/\b(?:password|passwd|token|secret|api[_-]?key|authorization)\s*[=:]\s*[^\s,;]+/gi, "$1=<secret>");
|
|
162
|
+
replace(/\b[A-Z]:\\(?:[^\s<>:"|?*]+\\)*[^\s<>:"|?*]*/g, "<path>");
|
|
163
|
+
replace(/(?:^|[\s('"`])\/(?:[^\s)'"`]+\/)*[^\s)'"`]*/g, "<path>");
|
|
164
|
+
replace(/\b(?:[A-Za-z0-9_.-]+\/)+(?:[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,12})\b/g, "<path>");
|
|
165
|
+
replace(/\b[A-Za-z0-9_.+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "<email>");
|
|
166
|
+
return { value: value.slice(0, 2_000), redactions };
|
|
167
|
+
}
|
|
168
|
+
function sanitizeStack(error, repo) {
|
|
169
|
+
return (error.stack ?? "")
|
|
170
|
+
.split(/\r?\n/)
|
|
171
|
+
.slice(1, 9)
|
|
172
|
+
.map((line) => `at ${line.includes(repo) ? "<repository>" : "<runtime>"}`);
|
|
173
|
+
}
|
|
174
|
+
function sanitizeUnknown(value, repo, state) {
|
|
175
|
+
if (typeof value === "string") {
|
|
176
|
+
const scrubbed = scrubText(value, repo);
|
|
177
|
+
state.redactions += scrubbed.redactions;
|
|
178
|
+
return scrubbed.value;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value))
|
|
181
|
+
return value.slice(0, 500).map((item) => sanitizeUnknown(item, repo, state));
|
|
182
|
+
if (value && typeof value === "object") {
|
|
183
|
+
const output = {};
|
|
184
|
+
for (const [key, item] of Object.entries(value).slice(0, 500)) {
|
|
185
|
+
const safeKey = /[\\/]|\.[A-Za-z0-9]{1,12}$/.test(key) ? "<path-key>" : key;
|
|
186
|
+
if (safeKey !== key)
|
|
187
|
+
state.redactions++;
|
|
188
|
+
if (/^(?:source|content|query|command|arguments?|env(?:ironment)?|remote)$/i.test(key)) {
|
|
189
|
+
output[safeKey] = "<omitted>";
|
|
190
|
+
state.redactions++;
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
output[safeKey] = sanitizeUnknown(item, repo, state);
|
|
194
|
+
}
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function readEvents(repoPath, retentionDays, since) {
|
|
200
|
+
const { target } = containedPath(repoPath, JOURNAL_PATH, "read");
|
|
201
|
+
if (!fs.existsSync(target))
|
|
202
|
+
return [];
|
|
203
|
+
if (fs.statSync(target).size > MAX_JOURNAL_BYTES)
|
|
204
|
+
throw new Error("knodin diagnostics read: journal exceeds the 2 MiB safety limit");
|
|
205
|
+
const cutoff = Math.max(Date.now() - validRetention(retentionDays) * 86_400_000, since?.getTime() ?? Number.NEGATIVE_INFINITY);
|
|
206
|
+
return fs
|
|
207
|
+
.readFileSync(target, "utf8")
|
|
208
|
+
.split(/\r?\n/)
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.flatMap((line) => {
|
|
211
|
+
try {
|
|
212
|
+
const record = JSON.parse(line);
|
|
213
|
+
return record.schemaVersion === 1 && Date.parse(record.at) >= cutoff ? [record] : [];
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
.slice(-MAX_RECORDS);
|
|
220
|
+
}
|
|
221
|
+
export function enableDiagnostics(repoPath, retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
222
|
+
const { repo, target } = containedPath(repoPath, CONFIG_PATH, "enable");
|
|
223
|
+
withJournalLock(repo, () => {
|
|
224
|
+
const previous = readConfig(repo);
|
|
225
|
+
const config = {
|
|
226
|
+
schemaVersion: 1,
|
|
227
|
+
enabled: true,
|
|
228
|
+
retentionDays: validRetention(retentionDays),
|
|
229
|
+
enabledAt: new Date().toISOString(),
|
|
230
|
+
generation: (previous?.generation ?? 0) + 1,
|
|
231
|
+
sessionId: crypto.randomBytes(16).toString("hex"),
|
|
232
|
+
};
|
|
233
|
+
atomicPrivateWrite(target, `${JSON.stringify(config, null, 2)}\n`);
|
|
234
|
+
});
|
|
235
|
+
return { ...diagnosticsStatus(repo), message: "Local diagnostics enabled; nothing is uploaded." };
|
|
236
|
+
}
|
|
237
|
+
export function disableDiagnostics(repoPath) {
|
|
238
|
+
const { repo, target } = containedPath(repoPath, CONFIG_PATH, "disable");
|
|
239
|
+
withJournalLock(repo, () => {
|
|
240
|
+
if (fs.existsSync(target))
|
|
241
|
+
fs.unlinkSync(target);
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
...diagnosticsStatus(repo),
|
|
245
|
+
message: "Local diagnostics disabled; retained events were not deleted.",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export function diagnosticsStatus(repoPath) {
|
|
249
|
+
const config = readConfig(repoPath);
|
|
250
|
+
const retentionDays = config?.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
251
|
+
let events = [];
|
|
252
|
+
let journal = {
|
|
253
|
+
status: "healthy",
|
|
254
|
+
issue: null,
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
events = readEvents(repoPath, retentionDays);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
journal = {
|
|
261
|
+
status: "unavailable",
|
|
262
|
+
issue: error instanceof Error ? error.message : "diagnostic journal is unavailable",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: 1,
|
|
267
|
+
enabled: config !== null,
|
|
268
|
+
localOnly: true,
|
|
269
|
+
uploaded: false,
|
|
270
|
+
retentionDays,
|
|
271
|
+
records: events.length,
|
|
272
|
+
oldestAt: events[0]?.at ?? null,
|
|
273
|
+
newestAt: events.at(-1)?.at ?? null,
|
|
274
|
+
journal,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export function recordDiagnosticFailure(repoPath, input) {
|
|
278
|
+
try {
|
|
279
|
+
const repo = fs.realpathSync(repoPath);
|
|
280
|
+
const observed = readConfig(repo);
|
|
281
|
+
if (!observed)
|
|
282
|
+
return { recorded: false, reason: "disabled" };
|
|
283
|
+
return withJournalLock(repo, () => {
|
|
284
|
+
const config = readConfig(repo);
|
|
285
|
+
if (!config)
|
|
286
|
+
return { recorded: false, reason: "disabled" };
|
|
287
|
+
if (config.sessionId !== observed.sessionId || config.generation !== observed.generation)
|
|
288
|
+
return { recorded: false, reason: "state-changed" };
|
|
289
|
+
const error = input.error instanceof Error ? input.error : new Error(String(input.error));
|
|
290
|
+
const message = scrubText(error.message, repo).value;
|
|
291
|
+
const event = {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
at: new Date().toISOString(),
|
|
294
|
+
correlationId: crypto.randomBytes(8).toString("hex"),
|
|
295
|
+
surface: input.surface,
|
|
296
|
+
operation: OPERATIONS.has(input.operation) ? input.operation : "unknown",
|
|
297
|
+
phase: safeLabel(input.phase, "operation"),
|
|
298
|
+
error: {
|
|
299
|
+
name: safeLabel(error.name, "Error"),
|
|
300
|
+
code: errorCode(input.error),
|
|
301
|
+
messageFingerprint: crypto
|
|
302
|
+
.createHash("sha256")
|
|
303
|
+
.update(message)
|
|
304
|
+
.digest("hex")
|
|
305
|
+
.slice(0, 16),
|
|
306
|
+
stack: sanitizeStack(error, repo),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const events = [...readEvents(repo, config.retentionDays), event].slice(-MAX_RECORDS);
|
|
310
|
+
const { target } = containedPath(repo, JOURNAL_PATH, "record");
|
|
311
|
+
atomicPrivateWrite(target, `${events.map((record) => JSON.stringify(record)).join("\n")}\n`);
|
|
312
|
+
return { recorded: true, correlationId: event.correlationId };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return { recorded: false, reason: "unavailable" };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export function clearDiagnostics(repoPath) {
|
|
320
|
+
const { repo, target } = containedPath(repoPath, JOURNAL_PATH, "clear");
|
|
321
|
+
return withJournalLock(repo, () => {
|
|
322
|
+
if (!fs.existsSync(target))
|
|
323
|
+
return { removed: false, records: 0, bytes: 0 };
|
|
324
|
+
let records = null;
|
|
325
|
+
try {
|
|
326
|
+
records = readEvents(repo, MAX_RETENTION_DAYS).length;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Deletion must remain the recovery path for an unreadable or oversized journal.
|
|
330
|
+
}
|
|
331
|
+
const bytes = fs.statSync(target).size;
|
|
332
|
+
fs.unlinkSync(target);
|
|
333
|
+
const config = readConfig(repo);
|
|
334
|
+
if (config) {
|
|
335
|
+
atomicPrivateWrite(containedPath(repo, CONFIG_PATH, "clear").target, `${JSON.stringify({ ...config, generation: config.generation + 1 }, null, 2)}\n`);
|
|
336
|
+
}
|
|
337
|
+
return { removed: true, records, bytes };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function defaultBundlePath() {
|
|
341
|
+
return `.knodin/diagnostics/knodin-diagnostics-${new Date().toISOString().replace(/[:.]/g, "-")}.json.gz`;
|
|
342
|
+
}
|
|
343
|
+
function lifecycleLog(repo, state) {
|
|
344
|
+
const target = path.join(repo, ".knodin", "indexer.log");
|
|
345
|
+
if (!fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink())
|
|
346
|
+
return [];
|
|
347
|
+
const size = fs.statSync(target).size;
|
|
348
|
+
const descriptor = fs.openSync(target, "r");
|
|
349
|
+
try {
|
|
350
|
+
const length = Math.min(size, MAX_LOG_BYTES);
|
|
351
|
+
const buffer = Buffer.alloc(length);
|
|
352
|
+
fs.readSync(descriptor, buffer, 0, length, Math.max(0, size - length));
|
|
353
|
+
return buffer
|
|
354
|
+
.toString("utf8")
|
|
355
|
+
.split(/\r?\n/)
|
|
356
|
+
.filter(Boolean)
|
|
357
|
+
.slice(-200)
|
|
358
|
+
.map((line) => sanitizeUnknown(line, repo, state));
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
fs.closeSync(descriptor);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export function collectDiagnostics(repoPath, options = {}) {
|
|
365
|
+
const { repo, target } = containedPath(repoPath, options.outputPath ?? defaultBundlePath(), "collect");
|
|
366
|
+
if (fs.existsSync(target))
|
|
367
|
+
throw new Error("knodin diagnostics collect: refusing to overwrite an existing bundle");
|
|
368
|
+
const sinceHours = options.sinceHours ?? 24;
|
|
369
|
+
if (!Number.isFinite(sinceHours) || sinceHours <= 0 || sinceHours > 24 * 365)
|
|
370
|
+
throw new Error("knodin diagnostics collect: --since must be from 1h through 8760h");
|
|
371
|
+
const state = { redactions: 0 };
|
|
372
|
+
const status = diagnosticsStatus(repo);
|
|
373
|
+
const bundle = {
|
|
374
|
+
manifest: {
|
|
375
|
+
schemaVersion: 1,
|
|
376
|
+
generatedAt: new Date().toISOString(),
|
|
377
|
+
privacy: "redacted-local-only",
|
|
378
|
+
localOnly: true,
|
|
379
|
+
uploaded: false,
|
|
380
|
+
redactions: 0,
|
|
381
|
+
omissions: [
|
|
382
|
+
"source",
|
|
383
|
+
"queries-and-arguments",
|
|
384
|
+
"environment-values",
|
|
385
|
+
"git-remotes-diffs-and-messages",
|
|
386
|
+
"raw-paths",
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
runtime: {
|
|
390
|
+
...(options.knodinVersion ? { knodinVersion: options.knodinVersion } : {}),
|
|
391
|
+
node: process.version,
|
|
392
|
+
platform: process.platform,
|
|
393
|
+
arch: process.arch,
|
|
394
|
+
},
|
|
395
|
+
diagnostics: status,
|
|
396
|
+
diagnosticEvents: readEvents(repo, status.retentionDays, new Date(Date.now() - sinceHours * 3_600_000)),
|
|
397
|
+
telemetry: sanitizeUnknown(options.telemetry ?? [], repo, state),
|
|
398
|
+
doctor: sanitizeUnknown(options.doctor ?? null, repo, state),
|
|
399
|
+
graph: sanitizeUnknown(options.graph ?? null, repo, state),
|
|
400
|
+
lifecycleLog: lifecycleLog(repo, state),
|
|
401
|
+
};
|
|
402
|
+
bundle.manifest.redactions = state.redactions;
|
|
403
|
+
atomicPrivateWrite(target, zlib.gzipSync(`${JSON.stringify(bundle, null, 2)}\n`, { level: 9 }));
|
|
404
|
+
return {
|
|
405
|
+
schemaVersion: 1,
|
|
406
|
+
outputPath: path.relative(repo, target),
|
|
407
|
+
format: "gzip-json",
|
|
408
|
+
localOnly: true,
|
|
409
|
+
uploaded: false,
|
|
410
|
+
bytes: fs.statSync(target).size,
|
|
411
|
+
records: bundle.diagnosticEvents.length,
|
|
412
|
+
redactions: bundle.manifest.redactions,
|
|
413
|
+
message: "Bundle created locally. Run `knodin diagnostics inspect <bundle>` before sharing it.",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
export function inspectDiagnosticsBundle(repoPath, bundlePath) {
|
|
417
|
+
const { target } = containedPath(repoPath, bundlePath, "inspect");
|
|
418
|
+
if (!fs.existsSync(target))
|
|
419
|
+
throw new Error("knodin diagnostics inspect: bundle does not exist");
|
|
420
|
+
if (fs.statSync(target).size > 10 * 1024 * 1024)
|
|
421
|
+
throw new Error("knodin diagnostics inspect: bundle exceeds the 10 MiB safety limit");
|
|
422
|
+
let parsed;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(zlib
|
|
425
|
+
.gunzipSync(fs.readFileSync(target), { maxOutputLength: MAX_BUNDLE_BYTES })
|
|
426
|
+
.toString("utf8"));
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
throw new Error("knodin diagnostics inspect: invalid gzip JSON bundle");
|
|
430
|
+
}
|
|
431
|
+
if (parsed.manifest?.schemaVersion !== 1 || parsed.manifest.privacy !== "redacted-local-only")
|
|
432
|
+
throw new Error("knodin diagnostics inspect: unsupported or unsafe bundle manifest");
|
|
433
|
+
if (!Array.isArray(parsed.diagnosticEvents) ||
|
|
434
|
+
!Array.isArray(parsed.telemetry) ||
|
|
435
|
+
!Array.isArray(parsed.lifecycleLog))
|
|
436
|
+
throw new Error("knodin diagnostics inspect: malformed bundle sections");
|
|
437
|
+
return {
|
|
438
|
+
manifest: parsed.manifest,
|
|
439
|
+
runtime: parsed.runtime,
|
|
440
|
+
diagnostics: parsed.diagnostics,
|
|
441
|
+
sections: ["diagnosticEvents", "telemetry", "doctor", "graph", "lifecycleLog"],
|
|
442
|
+
counts: {
|
|
443
|
+
diagnosticEvents: parsed.diagnosticEvents.length,
|
|
444
|
+
telemetry: parsed.telemetry.length,
|
|
445
|
+
lifecycleLogLines: parsed.lifecycleLog.length,
|
|
446
|
+
},
|
|
447
|
+
bundle: parsed,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
@@ -39,12 +39,12 @@ The \`query\` operation lets you ask precise, structured questions about your co
|
|
|
39
39
|
|
|
40
40
|
Federation is knodin's internal query-composition mechanism, not the primary
|
|
41
41
|
user-facing model. Use \`knodin repos\` for operational checkout management and
|
|
42
|
-
\`knodin system\` with commit-ready \`
|
|
42
|
+
\`knodin system\` with commit-ready \`knodin.yaml\` stable identities for
|
|
43
43
|
cooperating components. Sibling directory placement and shared search matches
|
|
44
44
|
never establish system membership.
|
|
45
45
|
|
|
46
|
-
Legacy \`.
|
|
47
|
-
silently discarded. Migrate it to \`
|
|
46
|
+
Legacy \`.knodin/federation.json\` remains a compatibility input and is not
|
|
47
|
+
silently discarded. Migrate it to \`knodin.yaml\` plus personal XDG checkout
|
|
48
48
|
paths. The \`federated_repos\` query remains available for inspecting the
|
|
49
49
|
engine's currently composed repository paths; it does not prove those
|
|
50
50
|
repositories form one system.`,
|
|
@@ -82,7 +82,7 @@ Go, Rust, PHP, Ruby, Kotlin, Swift, Perl, PowerShell, Bash, and MuleSoft/RAML ar
|
|
|
82
82
|
|
|
83
83
|
Run \`knodin status\` (or MCP \`operation: "status"\`) to inspect the local schema/model/version, file and symbol coverage, orphaned or missing records, and the last successful reconciliation. Its repair steps are actionable. Run \`knodin repair\` (or MCP \`operation: "repair"\`) to rebuild only missing/damaged rows and verify health; healthy indexed files are not deleted or rebuilt.
|
|
84
84
|
|
|
85
|
-
The MCP \`telemetry\` operation reads process-local metadata-only measurements: actual response bytes and \`gpt-tokenizer@3.4.0:o200k_base\` tokens, executed baselines where available, negative or positive savings, latency, RSS, schema cost, truncation, and detail mode. It never contains source or file paths and does not change existing operation response shapes. Telemetry is not persisted by default. Set MCP \`persistTelemetry: true\` to append metadata-only JSONL to the repo-root \`.
|
|
85
|
+
The MCP \`telemetry\` operation reads process-local metadata-only measurements: actual response bytes and \`gpt-tokenizer@3.4.0:o200k_base\` tokens, executed baselines where available, negative or positive savings, latency, RSS, schema cost, truncation, and detail mode. It never contains source or file paths and does not change existing operation response shapes. Telemetry is not persisted by default. Set MCP \`persistTelemetry: true\` to append metadata-only JSONL to the repo-root \`.knodin-telemetry.jsonl\`; knodin never sends it over the network. Use \`telemetryAction: "report"\` or \`knodin telemetry report\` for a static repository-local HTML report.
|
|
86
86
|
|
|
87
87
|
Common issues and how to resolve them when using knodin:
|
|
88
88
|
|
|
@@ -100,7 +100,7 @@ The \`prs\` triage operation requires the official GitHub CLI (\`gh\`) to be ins
|
|
|
100
100
|
|
|
101
101
|
### Legacy federation configuration
|
|
102
102
|
If legacy federation configuration fails, validate
|
|
103
|
-
\`.
|
|
103
|
+
\`.knodin/federation.json\`, then migrate stable identities to \`knodin.yaml\`
|
|
104
104
|
and local paths to XDG configuration. Do not infer membership from siblings.`,
|
|
105
105
|
};
|
|
106
106
|
const DOC_TOPIC_FILES = {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* The durable lesson, now a hard gate: no approximate search path may engage BY
|
|
12
12
|
* DEFAULT without a measured >=95% top-10 recall vs the exact scan on a real
|
|
13
13
|
* corpus, with the method stated. This module is therefore OPT-IN ONLY, behind
|
|
14
|
-
* `
|
|
14
|
+
* `KNODIN_ANN=1`; `search()`'s default remains the exact scan at every corpus
|
|
15
15
|
* size. See scripts/ann-bench.ts for the recall/latency harness.
|
|
16
16
|
*
|
|
17
17
|
* MEASURED, GATE NOT CLEARED (so it stays opt-in). scripts/ann-bench.ts on
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 88.0% top-10 is far better than R11(a)'s LSH (43.3% top-10, AND slower) — the
|
|
26
26
|
* three queries LSH scored 0% on ("session token", "webhook handler", "metrics
|
|
27
27
|
* collection") here score 80/80/90% top-10 — but it misses the hard 95% floor,
|
|
28
|
-
* so the exact scan REMAINS the default. Recall is tunable via
|
|
28
|
+
* so the exact scan REMAINS the default. Recall is tunable via KNODIN_ANN_EF (a
|
|
29
29
|
* larger ef trades latency for recall); default-engagement would require a fresh
|
|
30
30
|
* measurement clearing 95% at the corpus size where the path engages. Given the
|
|
31
31
|
* exact scan is 38ms at 18.6k and extrapolates sub-second to ~250k symbols,
|
|
@@ -257,11 +257,11 @@ export function buildHnswIndex(rows, params = {}) {
|
|
|
257
257
|
* at a corpus size where the path would actually engage (see module header).
|
|
258
258
|
*/
|
|
259
259
|
export function annEnabled() {
|
|
260
|
-
return process.env.
|
|
260
|
+
return process.env.KNODIN_ANN === "1";
|
|
261
261
|
}
|
|
262
|
-
/** efSearch beam width, overridable via
|
|
262
|
+
/** efSearch beam width, overridable via KNODIN_ANN_EF (a benchmark knob). */
|
|
263
263
|
export function annEfSearch() {
|
|
264
|
-
const raw = process.env.
|
|
264
|
+
const raw = process.env.KNODIN_ANN_EF;
|
|
265
265
|
if (raw) {
|
|
266
266
|
const n = Number.parseInt(raw, 10);
|
|
267
267
|
if (Number.isFinite(n) && n > 0)
|
|
@@ -24,7 +24,7 @@ const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
|
|
|
24
24
|
*/
|
|
25
25
|
export function getModelCacheDirectory(options = {}) {
|
|
26
26
|
const environment = options.env ?? process.env;
|
|
27
|
-
const override = environment.
|
|
27
|
+
const override = environment.KNODIN_MODEL_CACHE?.trim();
|
|
28
28
|
if (override)
|
|
29
29
|
return path.resolve(override);
|
|
30
30
|
const platform = options.platform ?? process.platform;
|
|
@@ -42,16 +42,16 @@ export function getModelCacheDirectory(options = {}) {
|
|
|
42
42
|
* point at an approved mirror without changing where model files are cached.
|
|
43
43
|
*/
|
|
44
44
|
export function getModelRemoteHost(environment = process.env) {
|
|
45
|
-
const configured = environment.
|
|
45
|
+
const configured = environment.KNODIN_MODEL_HOST?.trim() || DEFAULT_MODEL_REMOTE_HOST;
|
|
46
46
|
let parsed;
|
|
47
47
|
try {
|
|
48
48
|
parsed = new URL(configured);
|
|
49
49
|
}
|
|
50
50
|
catch {
|
|
51
|
-
throw new Error(`
|
|
51
|
+
throw new Error(`KNODIN_MODEL_HOST must be an absolute HTTP(S) URL: ${configured}`);
|
|
52
52
|
}
|
|
53
53
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
54
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`KNODIN_MODEL_HOST must use HTTP(S): ${configured}`);
|
|
55
55
|
}
|
|
56
56
|
parsed.hash = "";
|
|
57
57
|
parsed.search = "";
|