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,590 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { gitExecutable } from "./git-executable.js";
5
+ import { compressOutput, readOutputArtifact } from "./output-compression.js";
6
+ const CODE_EXTENSIONS = [
7
+ ".tsx",
8
+ ".jsx",
9
+ ".java",
10
+ ".cs",
11
+ ".go",
12
+ ".rs",
13
+ ".py",
14
+ ".ts",
15
+ ".js",
16
+ ".mjs",
17
+ ".cjs",
18
+ ".kt",
19
+ ".kts",
20
+ ".swift",
21
+ ".rb",
22
+ ".php",
23
+ ".scala",
24
+ ".c",
25
+ ".cc",
26
+ ".cpp",
27
+ ".h",
28
+ ".hpp",
29
+ ];
30
+ const MAX_ANALYSIS_BYTES = 4 * 1024 * 1024;
31
+ const MAX_SOURCE_FILE_BYTES = 1024 * 1024;
32
+ const MAX_MANIFEST_BYTES = 256 * 1024;
33
+ const MAX_REFERENCE_PATH_CHARS = 4_096;
34
+ const MAX_PATH_SUFFIXES = 64;
35
+ const DEFAULT_CONTEXT_BYTES = 16_384;
36
+ const MAX_CONTEXT_BYTES = 128 * 1024;
37
+ const DEFAULT_DIAGNOSTICS = 10;
38
+ const MAX_DIAGNOSTICS = 50;
39
+ const DEFAULT_RELATIONS = 8;
40
+ const MAX_RELATIONS = 25;
41
+ function integer(name, value, fallback, minimum, maximum) {
42
+ const resolved = value ?? fallback;
43
+ if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum)
44
+ throw new Error(`knodin diagnose: ${name} must be an integer from ${minimum} to ${maximum}`);
45
+ return resolved;
46
+ }
47
+ function evidenceLine(value) {
48
+ const compact = value.trim().replaceAll("\t", " ");
49
+ return compact.length <= 500 ? compact : `${compact.slice(0, 499)}…`;
50
+ }
51
+ function isPathCharacter(value) {
52
+ return !" \t\r\n\"'`()[]{}<>|=,".includes(value);
53
+ }
54
+ function skipCharacters(value, cursor, characters) {
55
+ let next = cursor;
56
+ while (next < value.length && characters.includes(value[next] ?? ""))
57
+ next++;
58
+ return next;
59
+ }
60
+ function readUnsignedInteger(value, cursor) {
61
+ let next = cursor;
62
+ while (next < value.length) {
63
+ const code = value.codePointAt(next) ?? -1;
64
+ if (code < 48 || code > 57)
65
+ break;
66
+ next++;
67
+ }
68
+ return {
69
+ value: next === cursor ? null : Number(value.slice(cursor, next)),
70
+ next,
71
+ };
72
+ }
73
+ function locationStart(suffix) {
74
+ let cursor = skipCharacters(suffix, 0, "\"'),");
75
+ cursor = skipCharacters(suffix, cursor, " ");
76
+ if (suffix.startsWith("line ", cursor))
77
+ cursor += 5;
78
+ else if (suffix.startsWith(":line ", cursor))
79
+ cursor += 6;
80
+ else
81
+ cursor = skipCharacters(suffix, cursor, ":([]");
82
+ return skipCharacters(suffix, cursor, " [");
83
+ }
84
+ function parseLocation(value, extensionEnd) {
85
+ const suffix = value.slice(extensionEnd, extensionEnd + 48).toLowerCase();
86
+ const line = readUnsignedInteger(suffix, locationStart(suffix));
87
+ if (line.value === null)
88
+ return { line: null, column: null };
89
+ const columnStart = skipCharacters(suffix, line.next, ":, [");
90
+ const column = readUnsignedInteger(suffix, columnStart);
91
+ return {
92
+ line: line.value,
93
+ column: column.value,
94
+ };
95
+ }
96
+ function referenceAt(line, lower, cursor) {
97
+ const extension = CODE_EXTENSIONS.find((candidate) => lower.startsWith(candidate, cursor));
98
+ if (!extension)
99
+ return null;
100
+ const extensionEnd = cursor + extension.length;
101
+ let start = cursor - 1;
102
+ while (start >= 0 && isPathCharacter(line[start] ?? ""))
103
+ start--;
104
+ let candidate = line.slice(start + 1, extensionEnd);
105
+ if (candidate.startsWith("file://"))
106
+ candidate = candidate.slice(7);
107
+ if (!candidate || candidate === extension || candidate.length > MAX_REFERENCE_PATH_CHARS)
108
+ return null;
109
+ const location = parseLocation(line, extensionEnd);
110
+ return {
111
+ path: candidate,
112
+ line: location.line,
113
+ column: location.column,
114
+ evidence: evidenceLine(line),
115
+ };
116
+ }
117
+ /** Extract bounded code-path references without treating free-form error prose as paths. */
118
+ export function extractDiagnosticReferences(text, limit = MAX_DIAGNOSTICS) {
119
+ const references = [];
120
+ const seen = new Set();
121
+ for (const sourceLine of text.split(/\r?\n/)) {
122
+ const line = sourceLine.slice(0, 32_768);
123
+ const lower = line.toLowerCase();
124
+ for (let cursor = 0; cursor < lower.length; cursor++) {
125
+ const reference = referenceAt(line, lower, cursor);
126
+ if (!reference)
127
+ continue;
128
+ const key = `${reference.path}:${reference.line ?? ""}:${reference.column ?? ""}`;
129
+ if (seen.has(key))
130
+ continue;
131
+ seen.add(key);
132
+ references.push(reference);
133
+ if (references.length >= limit)
134
+ return references;
135
+ }
136
+ }
137
+ return references;
138
+ }
139
+ function trackedPath(repo, candidate) {
140
+ const result = spawnSync(gitExecutable(), ["-C", repo, "ls-files", "--error-unmatch", "--", candidate], {
141
+ encoding: "utf8",
142
+ timeout: 3_000,
143
+ maxBuffer: 64 * 1024,
144
+ });
145
+ return result.status === 0;
146
+ }
147
+ function repositorySuffixes(candidate) {
148
+ const normalized = candidate.replaceAll("\\", "/").replace(/^[A-Za-z]:/, "");
149
+ const parts = normalized.split("/").filter(Boolean);
150
+ if (parts.includes(".."))
151
+ return [];
152
+ const suffixes = [];
153
+ for (let index = Math.max(0, parts.length - MAX_PATH_SUFFIXES); index < parts.length; index++) {
154
+ const suffix = parts.slice(index).join("/");
155
+ if (suffix)
156
+ suffixes.push(suffix);
157
+ }
158
+ return suffixes;
159
+ }
160
+ function withinRepository(repo, candidate) {
161
+ const realRepo = fs.realpathSync(repo);
162
+ const realCandidate = fs.realpathSync(path.join(repo, candidate));
163
+ const relative = path.relative(realRepo, realCandidate);
164
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
165
+ }
166
+ function validateTrackedCandidate(repo, candidate) {
167
+ try {
168
+ if (!trackedPath(repo, candidate))
169
+ return { reason: "not-indexed" };
170
+ return withinRepository(repo, candidate)
171
+ ? { file: candidate }
172
+ : { reason: "outside-repository" };
173
+ }
174
+ catch {
175
+ return { reason: "outside-repository" };
176
+ }
177
+ }
178
+ function basenameCandidates(search, basename) {
179
+ return [
180
+ ...new Set(search.results
181
+ .map(({ filePath }) => filePath)
182
+ .filter((filePath) => path.posix.basename(filePath) === basename)),
183
+ ].sort((left, right) => left.localeCompare(right));
184
+ }
185
+ async function resolveReferencePath(graph, repo, reference) {
186
+ const suffixes = repositorySuffixes(reference.path);
187
+ if (suffixes.length === 0)
188
+ return { reason: "outside-repository" };
189
+ for (const suffix of suffixes) {
190
+ if (!trackedPath(repo, suffix))
191
+ continue;
192
+ return validateTrackedCandidate(repo, suffix);
193
+ }
194
+ const basename = path.posix.basename(suffixes[0] ?? "");
195
+ const stem = basename.slice(0, Math.max(1, basename.lastIndexOf(".")));
196
+ const search = await graph.search(stem, repo, 100, {
197
+ includeSource: false,
198
+ federate: false,
199
+ });
200
+ const candidates = basenameCandidates(search, basename);
201
+ if (candidates.length === 1 && !search.hasMore)
202
+ return validateTrackedCandidate(repo, candidates[0] ?? "");
203
+ if (candidates.length > 1 || (search.hasMore && candidates.length > 0))
204
+ return { reason: "ambiguous", candidates };
205
+ return { reason: "not-indexed" };
206
+ }
207
+ function owningSymbol(rows, line) {
208
+ const symbols = rows.filter((row) => row.symbol && row.file && row.line !== undefined && row.endLine !== undefined);
209
+ if (symbols.length === 0)
210
+ return null;
211
+ if (line === null)
212
+ return [...symbols].sort((left, right) => (left.line ?? 0) - (right.line ?? 0))[0] ?? null;
213
+ return ([...symbols]
214
+ .filter((row) => (row.line ?? 0) <= line && (row.endLine ?? 0) >= line)
215
+ .sort((left, right) => (left.endLine ?? 0) - (left.line ?? 0) - ((right.endLine ?? 0) - (right.line ?? 0)) ||
216
+ (right.line ?? 0) - (left.line ?? 0))[0] ?? null);
217
+ }
218
+ function relationRows(result, limit) {
219
+ return result.results
220
+ .filter((row) => Boolean(row.symbol))
221
+ .slice(0, limit)
222
+ .map((row) => ({
223
+ identity: row.identity ?? null,
224
+ symbol: row.symbol ?? "",
225
+ file: row.file ?? null,
226
+ kind: row.kind ?? null,
227
+ line: row.line ?? null,
228
+ confidence: row.impactEdge?.confidence ?? null,
229
+ }));
230
+ }
231
+ function assignmentValue(line) {
232
+ const separator = line.indexOf("=");
233
+ if (separator < 0)
234
+ return null;
235
+ const value = line
236
+ .slice(separator + 1)
237
+ .trim()
238
+ .replace(/^["']|["']$/g, "");
239
+ return value || null;
240
+ }
241
+ function tomlName(content, sections) {
242
+ let active = false;
243
+ for (const sourceLine of content.split(/\r?\n/)) {
244
+ const line = sourceLine.trim();
245
+ if (line.startsWith("[") && line.endsWith("]")) {
246
+ active = sections.includes(line.slice(1, -1).trim());
247
+ continue;
248
+ }
249
+ const separator = line.indexOf("=");
250
+ if (active && separator >= 0 && line.slice(0, separator).trim() === "name")
251
+ return assignmentValue(line);
252
+ }
253
+ return null;
254
+ }
255
+ function xmlElement(content, element) {
256
+ const open = `<${element}>`;
257
+ const close = `</${element}>`;
258
+ const start = content.indexOf(open);
259
+ if (start < 0)
260
+ return null;
261
+ const end = content.indexOf(close, start + open.length);
262
+ if (end < 0)
263
+ return null;
264
+ return content.slice(start + open.length, end).trim() || null;
265
+ }
266
+ function packageName(kind, manifest, content) {
267
+ if (kind === "npm") {
268
+ try {
269
+ const parsed = JSON.parse(content);
270
+ return typeof parsed.name === "string" && parsed.name ? parsed.name : null;
271
+ }
272
+ catch {
273
+ return null;
274
+ }
275
+ }
276
+ if (kind === "python")
277
+ return tomlName(content, ["project", "tool.poetry"]);
278
+ if (kind === "cargo")
279
+ return tomlName(content, ["package"]);
280
+ if (kind === "maven") {
281
+ const parentEnd = content.indexOf("</parent>");
282
+ return xmlElement(parentEnd >= 0 ? content.slice(parentEnd + "</parent>".length) : content, "artifactId");
283
+ }
284
+ if (kind === "go") {
285
+ const module = content
286
+ .split(/\r?\n/)
287
+ .map((line) => line.trim())
288
+ .find((line) => line.startsWith("module "));
289
+ return module?.slice("module ".length).trim() || null;
290
+ }
291
+ return path.basename(manifest, path.extname(manifest));
292
+ }
293
+ function dotnetManifest(directory) {
294
+ let handle;
295
+ try {
296
+ handle = fs.opendirSync(directory);
297
+ for (let seen = 0; seen < 1_024; seen++) {
298
+ const entry = handle.readSync();
299
+ if (!entry)
300
+ break;
301
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".csproj"))
302
+ return entry.name;
303
+ }
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ finally {
309
+ handle?.closeSync();
310
+ }
311
+ return null;
312
+ }
313
+ function preferredPackageKind(file) {
314
+ const extension = path.extname(file).toLowerCase();
315
+ if (extension === ".py")
316
+ return "python";
317
+ if (extension === ".rs")
318
+ return "cargo";
319
+ if (extension === ".go")
320
+ return "go";
321
+ if ([".java", ".kt", ".kts"].includes(extension))
322
+ return "maven";
323
+ if (extension === ".cs")
324
+ return "dotnet";
325
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(extension))
326
+ return "npm";
327
+ return null;
328
+ }
329
+ function packageCandidate(repo, directory, name, kind) {
330
+ const manifest = path.posix.join(directory === "." ? "" : directory, name);
331
+ try {
332
+ if (!trackedPath(repo, manifest) || !withinRepository(repo, manifest))
333
+ return null;
334
+ const stat = fs.statSync(path.join(repo, manifest));
335
+ if (!stat.isFile() || stat.size > MAX_MANIFEST_BYTES)
336
+ return null;
337
+ const content = fs.readFileSync(path.join(repo, manifest), "utf8");
338
+ return { name: packageName(kind, manifest, content), manifest, kind };
339
+ }
340
+ catch {
341
+ return null;
342
+ }
343
+ }
344
+ function owningPackage(repo, file) {
345
+ const preferredKind = preferredPackageKind(file);
346
+ const manifests = [
347
+ ["package.json", "npm"],
348
+ ["pyproject.toml", "python"],
349
+ ["pom.xml", "maven"],
350
+ ["Cargo.toml", "cargo"],
351
+ ["go.mod", "go"],
352
+ ];
353
+ manifests.sort((left, right) => Number(right[1] === preferredKind) - Number(left[1] === preferredKind));
354
+ let directory = path.dirname(file);
355
+ while (directory !== ".." && !path.isAbsolute(directory)) {
356
+ const dotnet = preferredKind === "dotnet" ? dotnetManifest(path.join(repo, directory)) : null;
357
+ const candidates = [...manifests];
358
+ if (dotnet)
359
+ candidates.unshift([dotnet, "dotnet"]);
360
+ for (const [name, kind] of candidates) {
361
+ const found = packageCandidate(repo, directory, name, kind);
362
+ if (found)
363
+ return found;
364
+ }
365
+ const parent = path.dirname(directory);
366
+ if (parent === directory)
367
+ break;
368
+ directory = parent;
369
+ }
370
+ return null;
371
+ }
372
+ function recentChanges(repo, file, limit) {
373
+ const result = spawnSync(gitExecutable(), ["-C", repo, "log", `-${limit}`, "--format=%H%x09%aI%x09%an%x09%s", "--", file], { encoding: "utf8", timeout: 5_000, maxBuffer: 128 * 1024 });
374
+ if (result.status !== 0)
375
+ return [];
376
+ return result.stdout
377
+ .trim()
378
+ .split("\n")
379
+ .filter(Boolean)
380
+ .map((record) => {
381
+ const [commit = "", at = "", author = "", ...subject] = record.split("\t");
382
+ return { commit, at, author, subject: subject.join("\t") };
383
+ });
384
+ }
385
+ function truncateUtf8(value, byteBudget) {
386
+ if (Buffer.byteLength(value) <= byteBudget)
387
+ return value;
388
+ let output = "";
389
+ let bytes = 0;
390
+ for (const character of value) {
391
+ const size = Buffer.byteLength(character);
392
+ if (bytes + size > byteBudget)
393
+ break;
394
+ output += character;
395
+ bytes += size;
396
+ }
397
+ return output;
398
+ }
399
+ function sourceSnippet(repo, file, line, contextLines, remainingBytes) {
400
+ if (remainingBytes <= 0)
401
+ return { snippet: null, truncated: true };
402
+ const absolute = path.join(repo, file);
403
+ let lines;
404
+ try {
405
+ const stat = fs.statSync(absolute);
406
+ if (!stat.isFile() || stat.size > MAX_SOURCE_FILE_BYTES)
407
+ return { snippet: null, truncated: true };
408
+ lines = fs.readFileSync(absolute, "utf8").split(/\r?\n/);
409
+ }
410
+ catch {
411
+ return { snippet: null, truncated: true };
412
+ }
413
+ const startLine = Math.max(1, line - contextLines);
414
+ const endLine = Math.min(lines.length, line + contextLines);
415
+ const content = lines
416
+ .slice(startLine - 1, endLine)
417
+ .map((value, index) => `${startLine + index}: ${value.slice(0, 2_000)}`)
418
+ .join("\n");
419
+ const bounded = truncateUtf8(content, remainingBytes);
420
+ if (!bounded)
421
+ return { snippet: null, truncated: true };
422
+ return {
423
+ snippet: { file, startLine, endLine, content: bounded },
424
+ truncated: bounded !== content,
425
+ };
426
+ }
427
+ function diagnosisSource(repo, request) {
428
+ const sources = [request.artifactId !== undefined, request.text !== undefined].filter(Boolean).length;
429
+ if (sources !== 1)
430
+ throw new Error("knodin diagnose requires exactly one of artifactId or text");
431
+ if (request.artifactId) {
432
+ const probe = readOutputArtifact(repo, request.artifactId, {
433
+ byteBudget: MAX_ANALYSIS_BYTES,
434
+ });
435
+ const full = probe.complete || probe.range.totalLines <= probe.range.endLine
436
+ ? probe
437
+ : readOutputArtifact(repo, request.artifactId, {
438
+ endLine: probe.range.totalLines,
439
+ byteBudget: MAX_ANALYSIS_BYTES,
440
+ });
441
+ return {
442
+ kind: "artifact",
443
+ artifactId: request.artifactId,
444
+ content: full.content,
445
+ bytes: full.bytes,
446
+ complete: full.complete,
447
+ };
448
+ }
449
+ const compressed = compressOutput(repo, {
450
+ text: request.text,
451
+ lineBudget: 10_000,
452
+ byteBudget: MAX_ANALYSIS_BYTES,
453
+ maxInputBytes: MAX_ANALYSIS_BYTES,
454
+ retain: false,
455
+ redactSecrets: true,
456
+ });
457
+ return {
458
+ kind: "text",
459
+ artifactId: null,
460
+ content: compressed.content,
461
+ bytes: compressed.output.bytes,
462
+ complete: compressed.complete,
463
+ };
464
+ }
465
+ async function relationsForOwner(graph, repo, file, owner, limit) {
466
+ if (!owner?.symbol)
467
+ return { tests: [], upstream: [], downstream: [] };
468
+ const selector = owner.identity ? { identity: owner.identity, file } : { file };
469
+ const query = (pattern, options) => graph.query(pattern, owner.symbol ?? "", repo, undefined, limit, pattern === "impact" ? 2 : undefined, undefined, selector, options);
470
+ const [tests, upstream, downstream] = await Promise.all([
471
+ query("tests_for"),
472
+ query("impact", { direction: "upstream", includeTests: true }),
473
+ query("impact", { direction: "downstream", includeTests: true }),
474
+ ]);
475
+ return {
476
+ tests: relationRows(tests, limit),
477
+ upstream: relationRows(upstream, limit),
478
+ downstream: relationRows(downstream, limit),
479
+ };
480
+ }
481
+ function ownerEvidence(owner) {
482
+ if (!owner?.symbol)
483
+ return null;
484
+ return {
485
+ identity: owner.identity ?? null,
486
+ symbol: owner.symbol,
487
+ kind: owner.kind ?? "unknown",
488
+ line: owner.line ?? null,
489
+ endLine: owner.endLine ?? null,
490
+ signature: owner.signature ?? null,
491
+ evidenceQuality: owner.evidenceQuality ?? null,
492
+ };
493
+ }
494
+ async function diagnoseResolvedReference(graph, repo, reference, file, options) {
495
+ const summary = await graph.query("file_summary", file, repo, undefined, 200);
496
+ const owner = owningSymbol(summary.results, reference.line);
497
+ const relationships = await relationsForOwner(graph, repo, file, owner, options.relationLimit);
498
+ const evidenceLineNumber = reference.line ?? owner?.line ?? 1;
499
+ return {
500
+ diagnostic: {
501
+ reference,
502
+ file,
503
+ package: owningPackage(repo, file),
504
+ owner: ownerEvidence(owner),
505
+ ...relationships,
506
+ recentChanges: options.recentCommitLimit > 0 ? recentChanges(repo, file, options.recentCommitLimit) : [],
507
+ },
508
+ source: sourceSnippet(repo, file, evidenceLineNumber, options.contextLines, options.remainingContextBytes),
509
+ };
510
+ }
511
+ function diagnosisLimitations(inputComplete, referenceCount, contextTruncated, freshnessState) {
512
+ const limitations = [
513
+ "Static graph relationships and recent commits are diagnostic candidates, not runtime-causality proof.",
514
+ ];
515
+ if (!inputComplete)
516
+ limitations.push(`Only the first ${MAX_ANALYSIS_BYTES} analyzed bytes were available.`);
517
+ if (referenceCount === 0)
518
+ limitations.push("No repository-contained code-path reference was detected in the analyzed output.");
519
+ if (contextTruncated)
520
+ limitations.push("Source snippets were omitted or shortened to honor the context byte budget.");
521
+ if (freshnessState !== "fresh")
522
+ limitations.push(`Graph freshness is ${freshnessState}; refresh before strong claims.`);
523
+ return limitations;
524
+ }
525
+ function diagnosisStatus(resolvedCount, unresolvedCount, inputComplete, contextTruncated, freshnessState) {
526
+ if (resolvedCount === 0)
527
+ return "unresolved";
528
+ if (unresolvedCount > 0 || !inputComplete || contextTruncated || freshnessState !== "fresh")
529
+ return "partial";
530
+ return "resolved";
531
+ }
532
+ export async function diagnoseFailure(graph, repo, request) {
533
+ const maxDiagnostics = integer("maxDiagnostics", request.maxDiagnostics, DEFAULT_DIAGNOSTICS, 1, MAX_DIAGNOSTICS);
534
+ const contextLines = integer("contextLines", request.contextLines, 2, 0, 10);
535
+ const contextByteBudget = integer("contextByteBudget", request.contextByteBudget, DEFAULT_CONTEXT_BYTES, 256, MAX_CONTEXT_BYTES);
536
+ const relationLimit = integer("relationLimit", request.relationLimit, DEFAULT_RELATIONS, 1, MAX_RELATIONS);
537
+ const recentCommitLimit = integer("recentCommitLimit", request.recentCommitLimit, 3, 0, 10);
538
+ const input = diagnosisSource(repo, request);
539
+ const references = extractDiagnosticReferences(input.content, maxDiagnostics);
540
+ const diagnostics = [];
541
+ const unresolved = [];
542
+ const snippets = [];
543
+ let contextBytes = 0;
544
+ let contextTruncated = false;
545
+ for (const reference of references) {
546
+ const resolved = await resolveReferencePath(graph, repo, reference);
547
+ if (!resolved.file) {
548
+ unresolved.push({
549
+ reference,
550
+ reason: resolved.reason ?? "not-indexed",
551
+ ...(resolved.candidates ? { candidates: resolved.candidates } : {}),
552
+ });
553
+ continue;
554
+ }
555
+ const resolvedDiagnosis = await diagnoseResolvedReference(graph, repo, reference, resolved.file, {
556
+ relationLimit,
557
+ recentCommitLimit,
558
+ contextLines,
559
+ remainingContextBytes: contextByteBudget - contextBytes,
560
+ });
561
+ if (resolvedDiagnosis.source.snippet) {
562
+ snippets.push(resolvedDiagnosis.source.snippet);
563
+ contextBytes += Buffer.byteLength(resolvedDiagnosis.source.snippet.content);
564
+ }
565
+ contextTruncated ||= resolvedDiagnosis.source.truncated;
566
+ diagnostics.push(resolvedDiagnosis.diagnostic);
567
+ }
568
+ const health = await graph.status(repo, { audit: "cached" });
569
+ const status = diagnosisStatus(diagnostics.length, unresolved.length, input.complete, contextTruncated, health.freshness.state);
570
+ return {
571
+ schemaVersion: 1,
572
+ status,
573
+ input: {
574
+ kind: input.kind,
575
+ artifactId: input.artifactId,
576
+ bytes: input.bytes,
577
+ complete: input.complete,
578
+ },
579
+ diagnostics,
580
+ unresolved,
581
+ contextBundle: {
582
+ byteBudget: contextByteBudget,
583
+ usedBytes: contextBytes,
584
+ truncated: contextTruncated,
585
+ snippets,
586
+ },
587
+ freshness: health.freshness,
588
+ limitations: diagnosisLimitations(input.complete, references.length, contextTruncated, health.freshness.state),
589
+ };
590
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deprecated compatibility exports for the `knodin fleet init` alias.
3
+ *
4
+ * New implementation code must import from repository-management.ts.
5
+ * Remove this adapter after two minor releases.
6
+ */
7
+ export { discoverGitRepositoryPaths as discoverGitWorktrees, formatRepositoryHuman as formatFleetHuman, initializeRepositories as bootstrapFleet, parseFleetInitArgs, } from "./repository-management.js";
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function executableCandidates() {
4
+ if (process.platform === "win32") {
5
+ const roots = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"]].filter((value) => Boolean(value));
6
+ return roots.flatMap((root) => [
7
+ path.join(root, "Git", "cmd", "git.exe"),
8
+ path.join(root, "Git", "bin", "git.exe"),
9
+ ]);
10
+ }
11
+ return ["/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git"];
12
+ }
13
+ /**
14
+ * Resolve Git only from fixed installation locations. This deliberately avoids
15
+ * executing an attacker-controlled binary from a writable PATH entry.
16
+ */
17
+ export function gitExecutable() {
18
+ const executable = executableCandidates().find((candidate) => {
19
+ try {
20
+ fs.accessSync(candidate, fs.constants.X_OK);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ });
27
+ if (!executable) {
28
+ throw new Error("Git was not found in a trusted installation directory; install Git in a standard system location");
29
+ }
30
+ return executable;
31
+ }