knodin 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,262 @@
1
+ const DEFAULT_PROGRESS_INTERVAL_MS = 30_000;
2
+ const TTY_THROTTLE_MS = 100;
3
+ const CLEAR_TTY_LINE = "\r\x1b[2K";
4
+ const TERMINAL_PHASES = new Set(["completed", "cancelled", "failed"]);
5
+ const RESPONSE_BUDGET_FLAGS = new Set(["--bytes", "--tokens", "--items"]);
6
+ function parseDuration(value) {
7
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(value.trim());
8
+ if (!match) {
9
+ throw new Error("knodin repair: --progress-interval must be a positive duration such as 750ms, 30s, or 2m");
10
+ }
11
+ const amount = Number(match[1]);
12
+ const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : 60_000;
13
+ const milliseconds = amount * multiplier;
14
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
15
+ throw new Error("knodin repair: --progress-interval must be a positive duration");
16
+ }
17
+ return milliseconds;
18
+ }
19
+ export function parseRepairCliArgs(args) {
20
+ let progress = "auto";
21
+ let progressExplicit = false;
22
+ let progressIntervalMs = DEFAULT_PROGRESS_INTERVAL_MS;
23
+ let json = false;
24
+ let jsonl = false;
25
+ let plan = false;
26
+ const seen = new Set();
27
+ const markSeen = (flag) => {
28
+ if (seen.has(flag))
29
+ throw new Error(`knodin repair: repeated ${flag} argument`);
30
+ seen.add(flag);
31
+ };
32
+ const consumeValue = (flag, index) => {
33
+ const value = args[index + 1];
34
+ if (!value || value.startsWith("--"))
35
+ throw new Error(`knodin repair: ${flag} requires a value`);
36
+ return value;
37
+ };
38
+ for (let index = 0; index < args.length; index++) {
39
+ const argument = args[index];
40
+ if (argument === "--plan") {
41
+ markSeen(argument);
42
+ plan = true;
43
+ continue;
44
+ }
45
+ if (argument === "--json" || argument === "--jsonl") {
46
+ markSeen(argument);
47
+ if (argument === "--json")
48
+ json = true;
49
+ else
50
+ jsonl = true;
51
+ continue;
52
+ }
53
+ if (argument === "--progress" || argument.startsWith("--progress=")) {
54
+ markSeen("--progress");
55
+ const value = argument === "--progress"
56
+ ? consumeValue("--progress", index++)
57
+ : argument.slice("--progress=".length);
58
+ if (!value)
59
+ throw new Error("knodin repair: --progress requires a value");
60
+ if (!["auto", "tty", "plain", "none"].includes(value)) {
61
+ throw new Error("knodin repair: --progress must be auto, tty, plain, or none");
62
+ }
63
+ progress = value;
64
+ progressExplicit = true;
65
+ continue;
66
+ }
67
+ if (argument === "--progress-interval" || argument.startsWith("--progress-interval=")) {
68
+ markSeen("--progress-interval");
69
+ const value = argument === "--progress-interval"
70
+ ? consumeValue("--progress-interval", index++)
71
+ : argument.slice("--progress-interval=".length);
72
+ if (!value)
73
+ throw new Error("knodin repair: --progress-interval requires a value");
74
+ progressIntervalMs = parseDuration(value);
75
+ continue;
76
+ }
77
+ if (RESPONSE_BUDGET_FLAGS.has(argument)) {
78
+ markSeen(argument);
79
+ consumeValue(argument, index++);
80
+ continue;
81
+ }
82
+ if (argument.startsWith("--")) {
83
+ throw new Error(`knodin repair: unknown argument ${argument}`);
84
+ }
85
+ throw new Error(`knodin repair: unexpected positional argument ${argument}`);
86
+ }
87
+ if (json && jsonl)
88
+ throw new Error("knodin repair: --json and --jsonl are mutually exclusive");
89
+ return {
90
+ progress,
91
+ progressExplicit,
92
+ progressIntervalMs,
93
+ output: jsonl ? "jsonl" : json ? "json" : "human",
94
+ plan,
95
+ };
96
+ }
97
+ export function createRepairPlan(health) {
98
+ const staleRecords = health.missing.records.filter((record) => /^(indexed file no longer exists|indexed file is no longer eligible): /.test(record));
99
+ const damagedSnapshots = health.missing.records.filter((record) => record.startsWith("indexed snapshot differs from disk: "));
100
+ const incompletePostProcessing = health.missing.records.filter((record) => record.includes("post-processing is incomplete"));
101
+ const fileReconciliations = new Set([
102
+ ...health.missing.files,
103
+ ...staleRecords.map((record) => record.slice(record.indexOf(": ") + 2)),
104
+ ...damagedSnapshots.map((record) => record.slice(record.indexOf(": ") + 2)),
105
+ ]);
106
+ const orphanGroups = Object.values(health.orphaned).filter((count) => count > 0).length;
107
+ return {
108
+ schemaVersion: 1,
109
+ mutating: false,
110
+ healthFindings: {
111
+ missingFiles: health.missing.files.length,
112
+ damagedSnapshots: damagedSnapshots.length,
113
+ staleOrIneligibleRecords: staleRecords.length,
114
+ orphanedEmbeddings: health.orphaned.embeddings,
115
+ orphanedReferences: health.orphaned.references,
116
+ orphanedDependencies: health.orphaned.dependencies,
117
+ incompletePostProcessing: incompletePostProcessing.length,
118
+ total: health.missing.files.length +
119
+ health.missing.records.length +
120
+ Object.values(health.orphaned).reduce((sum, count) => sum + count, 0),
121
+ },
122
+ operations: {
123
+ fileReconciliations: fileReconciliations.size,
124
+ removals: staleRecords.length,
125
+ identityPasses: fileReconciliations.size > 0 ? 1 : 0,
126
+ embeddingCandidates: fileReconciliations.size,
127
+ orphanCleanupGroups: orphanGroups,
128
+ verification: 1,
129
+ total: fileReconciliations.size +
130
+ staleRecords.length +
131
+ (fileReconciliations.size > 0 ? 1 : 0) +
132
+ fileReconciliations.size +
133
+ orphanGroups +
134
+ 1,
135
+ },
136
+ explanation: "Health findings count observed problems; operations count bounded repair actions, so the totals need not match one-to-one.",
137
+ health,
138
+ };
139
+ }
140
+ export function resolveRepairProgressMode(options, environment, stderrIsTTY) {
141
+ if (options.output === "jsonl")
142
+ return "jsonl";
143
+ if (options.output === "json" && !options.progressExplicit)
144
+ return "none";
145
+ if (options.progress !== "auto")
146
+ return options.progress;
147
+ const ci = environment.CI;
148
+ const runsInCi = ci !== undefined && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
149
+ return stderrIsTTY && !runsInCi && environment.TERM?.toLowerCase() !== "dumb" ? "tty" : "plain";
150
+ }
151
+ export function formatRepairProgress(event) {
152
+ const total = event.phaseTotal === undefined
153
+ ? `${event.phaseCompleted}`
154
+ : `${event.phaseCompleted}/${event.phaseTotal}`;
155
+ const location = [event.currentFamily, event.currentPath].filter(Boolean).join(" ");
156
+ const suffix = location ? ` — ${location}` : "";
157
+ return `[repair:${event.phase}] ${total} ${event.message}${suffix}`;
158
+ }
159
+ export function createRepairProgressRenderer(options) {
160
+ const now = options.now ?? Date.now;
161
+ const schedule = options.setTimeout ??
162
+ ((callback, intervalMs) => setTimeout(callback, intervalMs));
163
+ const unschedule = options.clearTimeout ?? ((handle) => clearTimeout(handle));
164
+ let heartbeatHandle;
165
+ let started = false;
166
+ let stopped = false;
167
+ let ttyHasLine = false;
168
+ let lastWriteAt = Number.NEGATIVE_INFINITY;
169
+ let lastEvent;
170
+ let lastPhase;
171
+ let lastPercentBucket = -1;
172
+ const scheduleHeartbeat = () => {
173
+ if (options.mode !== "plain" && options.mode !== "tty")
174
+ return;
175
+ if (heartbeatHandle !== undefined)
176
+ unschedule(heartbeatHandle);
177
+ const heartbeatDelay = options.mode === "tty" ? Math.max(options.intervalMs, TTY_THROTTLE_MS) : options.intervalMs;
178
+ heartbeatHandle = schedule(() => {
179
+ heartbeatHandle = undefined;
180
+ if (stopped)
181
+ return;
182
+ if (lastEvent) {
183
+ if (options.mode === "plain")
184
+ writePlain(`${formatRepairProgress(lastEvent)} (heartbeat)`);
185
+ else
186
+ writeTty(`${formatRepairProgress(lastEvent)} (heartbeat)`);
187
+ }
188
+ scheduleHeartbeat();
189
+ }, heartbeatDelay);
190
+ if (typeof heartbeatHandle === "object" &&
191
+ heartbeatHandle !== null &&
192
+ "unref" in heartbeatHandle) {
193
+ heartbeatHandle.unref();
194
+ }
195
+ };
196
+ const writePlain = (line) => {
197
+ options.stderr.write(`${line}\n`);
198
+ lastWriteAt = now();
199
+ scheduleHeartbeat();
200
+ };
201
+ const writeTty = (line) => {
202
+ options.stderr.write(`${CLEAR_TTY_LINE}${line}`);
203
+ ttyHasLine = true;
204
+ lastWriteAt = now();
205
+ scheduleHeartbeat();
206
+ };
207
+ return {
208
+ start() {
209
+ if (started || stopped)
210
+ return;
211
+ started = true;
212
+ if (options.mode === "tty")
213
+ writeTty("Starting repair…");
214
+ else if (options.mode === "plain")
215
+ writePlain("[repair] Starting repair");
216
+ },
217
+ onProgress(event) {
218
+ if (stopped)
219
+ return;
220
+ if (!started)
221
+ this.start();
222
+ lastEvent = event;
223
+ if (options.mode === "none")
224
+ return;
225
+ if (options.mode === "jsonl") {
226
+ options.stdout.write(serializeRepairJsonlRecord("progress", event));
227
+ return;
228
+ }
229
+ if (options.mode === "tty") {
230
+ if (now() - lastWriteAt < TTY_THROTTLE_MS && !TERMINAL_PHASES.has(event.phase))
231
+ return;
232
+ writeTty(formatRepairProgress(event));
233
+ return;
234
+ }
235
+ const phaseChanged = event.phase !== lastPhase;
236
+ const percentage = event.phaseTotal && event.phaseTotal > 0
237
+ ? Math.floor((event.phaseCompleted / event.phaseTotal) * 100)
238
+ : undefined;
239
+ const percentBucket = percentage === undefined ? -1 : Math.floor(percentage / 5);
240
+ if (phaseChanged || percentBucket > lastPercentBucket) {
241
+ writePlain(formatRepairProgress(event));
242
+ if (phaseChanged)
243
+ lastPercentBucket = percentBucket;
244
+ else
245
+ lastPercentBucket = Math.max(lastPercentBucket, percentBucket);
246
+ lastPhase = event.phase;
247
+ }
248
+ },
249
+ stop() {
250
+ if (stopped)
251
+ return;
252
+ stopped = true;
253
+ if (heartbeatHandle !== undefined)
254
+ unschedule(heartbeatHandle);
255
+ if (options.mode === "tty" && ttyHasLine)
256
+ options.stderr.write("\n");
257
+ },
258
+ };
259
+ }
260
+ export function serializeRepairJsonlRecord(type, value) {
261
+ return `${JSON.stringify(type === "progress" ? { type, event: value } : { type, result: value })}\n`;
262
+ }
@@ -0,0 +1,173 @@
1
+ import { spawn } from "node:child_process";
2
+ import process from "node:process";
3
+ export const DEFAULT_REPOSITORY_INIT_MEMORY_LIMIT_BYTES = 768 * 1024 * 1024;
4
+ const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
5
+ const TERMINATION_GRACE_MS = 1_000;
6
+ const RSS_POLL_MS = 250;
7
+ function boundedAppend(current, chunk) {
8
+ if (current.length >= MAX_DIAGNOSTIC_BYTES)
9
+ return current;
10
+ return `${current}${chunk.toString("utf-8")}`.slice(0, MAX_DIAGNOSTIC_BYTES);
11
+ }
12
+ function terminateProcessTree(pid) {
13
+ if (!Number.isSafeInteger(pid) || pid <= 1)
14
+ return;
15
+ try {
16
+ process.kill(process.platform === "win32" ? pid : -pid, "SIGTERM");
17
+ }
18
+ catch {
19
+ // The worker may have exited between observation and termination.
20
+ }
21
+ }
22
+ function forceTerminateProcessTree(pid) {
23
+ if (!Number.isSafeInteger(pid) || pid <= 1)
24
+ return;
25
+ try {
26
+ process.kill(process.platform === "win32" ? pid : -pid, "SIGKILL");
27
+ }
28
+ catch {
29
+ // Already terminated.
30
+ }
31
+ }
32
+ function isWorkerMessage(value) {
33
+ if (!value || typeof value !== "object")
34
+ return false;
35
+ const record = value;
36
+ const result = record.result;
37
+ return ((record.type === "rss" &&
38
+ Number.isSafeInteger(record.rssBytes) &&
39
+ Number(record.rssBytes) >= 0) ||
40
+ (record.type === "result" &&
41
+ Boolean(result) &&
42
+ typeof result?.repository === "string" &&
43
+ typeof result.message === "string" &&
44
+ ["initialized", "updated", "already-current", "skipped", "failed"].includes(String(result.status))));
45
+ }
46
+ /**
47
+ * Initialize one repository in a disposable process. The parent observes RSS
48
+ * heartbeats, kills the whole process group on a ceiling breach, and converts
49
+ * every worker failure into a repository-scoped result so the portfolio can
50
+ * continue.
51
+ */
52
+ export async function runRepositoryInitializationProcess(options) {
53
+ const memoryLimitBytes = options.memoryLimitBytes ?? DEFAULT_REPOSITORY_INIT_MEMORY_LIMIT_BYTES;
54
+ if (!Number.isSafeInteger(memoryLimitBytes) || memoryLimitBytes < 128 * 1024 * 1024)
55
+ throw new Error("repository init memory limit must be an integer of at least 128 MiB");
56
+ if (options.command.length === 0 || !options.command[0])
57
+ throw new Error("repository init worker command is empty");
58
+ return await new Promise((resolve) => {
59
+ let stderr = "";
60
+ let peakRssBytes = 0;
61
+ let result;
62
+ let exceeded = false;
63
+ let timedOut = false;
64
+ let settled = false;
65
+ let rssPollRunning = false;
66
+ const child = spawn(options.command[0], [...options.command.slice(1), "__repository-init-worker", options.repository], {
67
+ cwd: options.repository,
68
+ detached: process.platform !== "win32",
69
+ env: {
70
+ ...process.env,
71
+ RECKON_INTERNAL_INIT_MEMORY_LIMIT_BYTES: String(memoryLimitBytes),
72
+ },
73
+ stdio: ["ignore", "ignore", "pipe", "ipc"],
74
+ });
75
+ child.stderr?.on("data", (chunk) => {
76
+ stderr = boundedAppend(stderr, chunk);
77
+ });
78
+ const observeRss = (rssBytes) => {
79
+ peakRssBytes = Math.max(peakRssBytes, rssBytes);
80
+ if (rssBytes <= memoryLimitBytes || exceeded)
81
+ return;
82
+ exceeded = true;
83
+ const pid = child.pid ?? 0;
84
+ terminateProcessTree(pid);
85
+ setTimeout(() => forceTerminateProcessTree(pid), TERMINATION_GRACE_MS).unref();
86
+ };
87
+ child.on("message", (message) => {
88
+ if (!isWorkerMessage(message))
89
+ return;
90
+ if (message.type === "result") {
91
+ result = message.result;
92
+ return;
93
+ }
94
+ observeRss(message.rssBytes);
95
+ });
96
+ const rssPoll = setInterval(() => {
97
+ const pid = child.pid;
98
+ if (process.platform === "win32" || !pid || rssPollRunning || settled)
99
+ return;
100
+ rssPollRunning = true;
101
+ const probe = spawn("/bin/ps", ["-o", "rss=", "-p", String(pid)], {
102
+ stdio: ["ignore", "pipe", "ignore"],
103
+ });
104
+ let output = "";
105
+ probe.stdout?.on("data", (chunk) => {
106
+ output = boundedAppend(output, chunk);
107
+ });
108
+ probe.on("close", () => {
109
+ rssPollRunning = false;
110
+ if (settled)
111
+ return;
112
+ const rssKiB = Number.parseInt(output.trim(), 10);
113
+ if (Number.isSafeInteger(rssKiB) && rssKiB >= 0)
114
+ observeRss(rssKiB * 1024);
115
+ });
116
+ probe.on("error", () => {
117
+ rssPollRunning = false;
118
+ });
119
+ }, RSS_POLL_MS);
120
+ rssPoll.unref();
121
+ child.on("error", (error) => {
122
+ stderr = boundedAppend(stderr, Buffer.from(error.message));
123
+ });
124
+ const timeout = setTimeout(() => {
125
+ timedOut = true;
126
+ const pid = child.pid ?? 0;
127
+ terminateProcessTree(pid);
128
+ setTimeout(() => forceTerminateProcessTree(pid), TERMINATION_GRACE_MS).unref();
129
+ }, options.timeoutMs ?? 30 * 60_000);
130
+ timeout.unref();
131
+ child.on("close", (code, signal) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ clearTimeout(timeout);
136
+ clearInterval(rssPoll);
137
+ if (exceeded) {
138
+ resolve({
139
+ repository: options.repository,
140
+ status: "failed",
141
+ message: `memory ceiling exceeded (${peakRssBytes} > ${memoryLimitBytes} bytes); worker terminated`,
142
+ peakRssBytes,
143
+ memoryLimitBytes,
144
+ });
145
+ return;
146
+ }
147
+ if (timedOut) {
148
+ resolve({
149
+ repository: options.repository,
150
+ status: "failed",
151
+ message: "isolated initialization timed out; worker process tree terminated",
152
+ peakRssBytes,
153
+ memoryLimitBytes,
154
+ });
155
+ return;
156
+ }
157
+ if (result) {
158
+ resolve({ ...result, repository: options.repository, peakRssBytes, memoryLimitBytes });
159
+ return;
160
+ }
161
+ const diagnostic = stderr.trim().replace(/\s+/g, " ");
162
+ const outcome = signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`;
163
+ const detail = diagnostic ? `: ${diagnostic}` : "";
164
+ resolve({
165
+ repository: options.repository,
166
+ status: "failed",
167
+ message: `isolated initialization failed (${outcome})${detail}`,
168
+ peakRssBytes,
169
+ memoryLimitBytes,
170
+ });
171
+ });
172
+ });
173
+ }