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.
Files changed (70) hide show
  1. package/README.md +190 -535
  2. package/dist/bin/cli.js +173 -17
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +4 -16
  5. package/dist/src/artifact-refresh.js +1 -1
  6. package/dist/src/cli-args.js +8 -1
  7. package/dist/src/cli-model.js +35 -1
  8. package/dist/src/codeflow-replay.js +80 -0
  9. package/dist/src/competitive-cold-mcp.js +40 -0
  10. package/dist/src/competitive-manifest.js +106 -25
  11. package/dist/src/competitive-runner.js +37 -3
  12. package/dist/src/competitive-sandbox.js +4 -4
  13. package/dist/src/context-export.js +1 -1
  14. package/dist/src/diagnostics.js +449 -0
  15. package/dist/src/docs-sections.js +5 -5
  16. package/dist/src/engine/ann-hnsw.js +5 -5
  17. package/dist/src/engine/embeddings.js +4 -4
  18. package/dist/src/engine/git-history.js +289 -0
  19. package/dist/src/engine/index.js +450 -103
  20. package/dist/src/engine/prune.js +2 -2
  21. package/dist/src/engine/scip-import.js +408 -0
  22. package/dist/src/engine/symbol-delete.js +1 -1
  23. package/dist/src/execution-profile.js +203 -0
  24. package/dist/src/failure-diagnosis.js +69 -10
  25. package/dist/src/hook-manager-integration.js +156 -0
  26. package/dist/src/index-activity.js +1 -1
  27. package/dist/src/init-progress-worker.js +1 -1
  28. package/dist/src/init.js +361 -92
  29. package/dist/src/lifecycle-health.js +51 -14
  30. package/dist/src/lsp-readonly.js +1 -1
  31. package/dist/src/output-compression.js +1 -1
  32. package/dist/src/output-telemetry.js +8 -4
  33. package/dist/src/pr-triage.js +2 -2
  34. package/dist/src/progressive-evidence.js +473 -0
  35. package/dist/src/pure-compression-cli.js +101 -0
  36. package/dist/src/release-preflight.js +510 -0
  37. package/dist/src/repair-progress-worker.js +1 -1
  38. package/dist/src/repository-init-process.js +1 -1
  39. package/dist/src/repository-management.js +145 -3
  40. package/dist/src/response-budget.js +11 -1
  41. package/dist/src/server.js +23 -3
  42. package/dist/src/structural-fast-path.js +303 -0
  43. package/dist/src/structural-snapshot.js +33 -0
  44. package/dist/src/system-config.js +7 -7
  45. package/dist/src/tools/knodin-tools.js +106 -15
  46. package/dist/src/update-ceremony.js +158 -0
  47. package/dist/src/update-policy.js +4 -4
  48. package/dist/src/wait-for-fresh.js +2 -2
  49. package/dist/src/worktree-lifecycle.js +2 -2
  50. package/docs/CLI.md +22 -0
  51. package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
  52. package/docs/CONTAINED-EXECUTION.md +77 -0
  53. package/docs/DIAGNOSTICS.md +45 -0
  54. package/docs/DOCTOR-AND-UPDATES.md +7 -4
  55. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  56. package/docs/INSTALLATION.md +1 -1
  57. package/docs/MCP.md +15 -0
  58. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  59. package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
  60. package/docs/SCIP-IMPORT.md +57 -0
  61. package/docs/SIGNED-UPDATES.md +5 -0
  62. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
  63. package/docs/TELEMETRY.md +6 -2
  64. package/docs/releases/0.6.0.md +18 -0
  65. package/docs/releases/0.7.0.md +24 -0
  66. package/docs/releases/0.7.1.md +21 -0
  67. package/docs/releases/0.7.2.md +21 -0
  68. package/docs/releases/0.7.3.md +23 -0
  69. package/package.json +34 -3
  70. 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 \`reckon.yaml\` stable identities for
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 \`.reckon/federation.json\` remains a compatibility input and is not
47
- silently discarded. Migrate it to \`reckon.yaml\` plus personal XDG checkout
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 \`.reckon-telemetry.jsonl\`; knodin never sends it over the network. Use \`telemetryAction: "report"\` or \`knodin telemetry report\` for a static repository-local HTML report.
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
- \`.reckon/federation.json\`, then migrate stable identities to \`reckon.yaml\`
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
- * `RECKON_ANN=1`; `search()`'s default remains the exact scan at every corpus
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 RECKON_ANN_EF (a
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.RECKON_ANN === "1";
260
+ return process.env.KNODIN_ANN === "1";
261
261
  }
262
- /** efSearch beam width, overridable via RECKON_ANN_EF (a benchmark knob). */
262
+ /** efSearch beam width, overridable via KNODIN_ANN_EF (a benchmark knob). */
263
263
  export function annEfSearch() {
264
- const raw = process.env.RECKON_ANN_EF;
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.RECKON_MODEL_CACHE?.trim();
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.RECKON_MODEL_HOST?.trim() || DEFAULT_MODEL_REMOTE_HOST;
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(`RECKON_MODEL_HOST must be an absolute HTTP(S) URL: ${configured}`);
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(`RECKON_MODEL_HOST must use HTTP(S): ${configured}`);
54
+ throw new Error(`KNODIN_MODEL_HOST must use HTTP(S): ${configured}`);
55
55
  }
56
56
  parsed.hash = "";
57
57
  parsed.search = "";