knodin 0.7.6 → 0.8.2

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 (130) hide show
  1. package/README.md +19 -7
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +2164 -108
  4. package/dist/bin/launcher.js +25 -3
  5. package/dist/src/agent-integration.js +304 -0
  6. package/dist/src/artifact-refresh.js +82 -0
  7. package/dist/src/cli-args.js +292 -0
  8. package/dist/src/cli-model.js +384 -0
  9. package/dist/src/codeflow-replay.js +81 -0
  10. package/dist/src/compact-structural.js +96 -0
  11. package/dist/src/compare.js +39 -0
  12. package/dist/src/competitive-cold-mcp.js +40 -0
  13. package/dist/src/competitive-constraints.js +21 -0
  14. package/dist/src/competitive-manifest.js +411 -0
  15. package/dist/src/competitive-measurement.js +183 -0
  16. package/dist/src/competitive-runner.js +487 -0
  17. package/dist/src/competitive-sandbox.js +108 -0
  18. package/dist/src/context-export.js +423 -0
  19. package/dist/src/context.js +102 -0
  20. package/dist/src/deterministic-random.js +34 -0
  21. package/dist/src/diagnostics-write-helper.js +473 -0
  22. package/dist/src/diagnostics.js +1476 -0
  23. package/dist/src/docs-sections.js +141 -0
  24. package/dist/src/doctor.js +382 -0
  25. package/dist/src/engine/ann-hnsw.js +261 -0
  26. package/dist/src/engine/embeddings.js +193 -0
  27. package/dist/src/engine/file-walker.js +49 -0
  28. package/dist/src/engine/git-history.js +289 -0
  29. package/dist/src/engine/index.js +14238 -0
  30. package/dist/src/engine/perf.js +115 -0
  31. package/dist/src/engine/prune.js +112 -0
  32. package/dist/src/engine/sarif-import.js +341 -0
  33. package/dist/src/engine/scip-import.js +423 -0
  34. package/dist/src/engine/source-policy.js +85 -0
  35. package/dist/src/engine/sqlite.js +71 -0
  36. package/dist/src/engine/state-paths.js +175 -0
  37. package/dist/src/engine/symbol-delete.js +58 -0
  38. package/dist/src/execution-profile.js +208 -0
  39. package/dist/src/failure-diagnosis.js +655 -0
  40. package/dist/src/fleet.js +7 -0
  41. package/dist/src/git-executable.js +31 -0
  42. package/dist/src/graph-layout.js +173 -0
  43. package/dist/src/graph-query-health.js +115 -0
  44. package/dist/src/hook-manager-integration.js +156 -0
  45. package/dist/src/index-activity.js +126 -0
  46. package/dist/src/init-progress-worker.js +106 -2
  47. package/dist/src/init-progress.js +155 -0
  48. package/dist/src/init.js +1295 -0
  49. package/dist/src/lifecycle-health.js +282 -0
  50. package/dist/src/lsp-readonly.js +217 -0
  51. package/dist/src/mcp-graph-worker.js +69 -0
  52. package/dist/src/mcp-reliability.js +154 -0
  53. package/dist/src/mcp-worker-supervisor.js +350 -0
  54. package/dist/src/mirror.js +290 -0
  55. package/dist/src/node-runtime.js +157 -0
  56. package/dist/src/output-compression.js +630 -0
  57. package/dist/src/output-telemetry.js +368 -0
  58. package/dist/src/pr-triage.js +638 -0
  59. package/dist/src/progressive-evidence.js +477 -0
  60. package/dist/src/pure-compression-cli.js +102 -0
  61. package/dist/src/relationship-adapters.js +377 -0
  62. package/dist/src/release-attestation.js +533 -0
  63. package/dist/src/release-preflight.js +513 -0
  64. package/dist/src/repair-lease.js +85 -0
  65. package/dist/src/repair-progress-worker.js +120 -2
  66. package/dist/src/repair-progress.js +262 -0
  67. package/dist/src/repository-init-process.js +177 -0
  68. package/dist/src/repository-management.js +1261 -0
  69. package/dist/src/response-budget.js +196 -0
  70. package/dist/src/server.js +217 -0
  71. package/dist/src/structural-fast-path.js +344 -0
  72. package/dist/src/structural-snapshot.js +37 -0
  73. package/dist/src/system-config.js +638 -0
  74. package/dist/src/terminal-help.js +83 -0
  75. package/dist/src/tools/knodin-tools.js +1640 -0
  76. package/dist/src/update-ceremony.js +162 -0
  77. package/dist/src/update-policy.js +944 -0
  78. package/dist/src/update-trust.js +504 -0
  79. package/dist/src/version.js +13 -0
  80. package/dist/src/visualization.js +515 -0
  81. package/dist/src/wait-for-fresh.js +98 -0
  82. package/dist/src/worktree-lifecycle.js +234 -0
  83. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  84. package/docs/CLI.md +20 -1
  85. package/docs/COMPARISON.md +403 -0
  86. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  87. package/docs/CONTAINED-EXECUTION.md +77 -0
  88. package/docs/DIAGNOSTICS.md +80 -0
  89. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  90. package/docs/HANDOFF.md +180 -0
  91. package/docs/INSTALLATION.md +21 -18
  92. package/docs/MCP.md +59 -8
  93. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  94. package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
  95. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  96. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  97. package/docs/SCIP-IMPORT.md +62 -0
  98. package/docs/SIGNED-UPDATES.md +151 -0
  99. package/docs/TELEMETRY.md +46 -0
  100. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  101. package/docs/assets/knodin-favicon.svg +4 -0
  102. package/docs/releases/0.3.0.md +46 -0
  103. package/docs/releases/0.4.0.md +68 -0
  104. package/docs/releases/0.4.1.md +28 -0
  105. package/docs/releases/0.4.2.md +27 -0
  106. package/docs/releases/0.4.3.md +23 -0
  107. package/docs/releases/0.5.0.md +29 -0
  108. package/docs/releases/0.5.1.md +17 -0
  109. package/docs/releases/0.6.0.md +18 -0
  110. package/docs/releases/0.7.0.md +24 -0
  111. package/docs/releases/0.7.1.md +21 -0
  112. package/docs/releases/0.7.2.md +21 -0
  113. package/docs/releases/0.7.3.md +23 -0
  114. package/docs/releases/0.7.4.md +17 -0
  115. package/docs/releases/0.7.5.md +20 -0
  116. package/docs/releases/0.8.0.md +74 -0
  117. package/docs/releases/0.8.2.md +34 -0
  118. package/package.json +127 -4
  119. package/roadmap/competitive-roadmap.md +3801 -0
  120. package/schemas/release-attestation-v1.schema.json +210 -0
  121. package/schemas/support-bundle-v2.schema.json +212 -0
  122. package/dist/chunks/chunk-DMQAGX77.js +0 -654
  123. package/dist/chunks/chunk-F4Z3Z766.js +0 -4
  124. package/dist/chunks/chunk-SIJAQVSX.js +0 -3
  125. package/dist/chunks/chunk-X6M4HUUE.js +0 -2
  126. package/dist/chunks/chunk-YPRMY2LP.js +0 -8
  127. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
  128. package/dist/chunks/server-7EDF4CBY.js +0 -14
  129. package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
  130. package/docs/releases/0.7.6.md +0 -25
@@ -0,0 +1,423 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { RE2 } from "re2-wasm";
5
+ import { compareBytes } from "./compare.js";
6
+ import { measurePerfPhaseSync } from "./engine/perf.js";
7
+ const DEFAULT_EXCLUDES = [".git/**", ".knodin/**", "node_modules/**", "dist/**", "build/**"];
8
+ const GLOB_CACHE_LIMIT = 256;
9
+ const globCache = new Map();
10
+ function normalizeRelative(value) {
11
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
12
+ }
13
+ function globRegex(glob) {
14
+ const normalized = normalizeRelative(glob);
15
+ const cached = globCache.get(normalized);
16
+ if (cached)
17
+ return cached;
18
+ let source = "^";
19
+ for (let i = 0; i < normalized.length; i++) {
20
+ const char = normalized[i];
21
+ if (char === "*") {
22
+ if (normalized[i + 1] === "*") {
23
+ i++;
24
+ if (normalized[i + 1] === "/") {
25
+ i++;
26
+ source += "(?:.*/)?";
27
+ }
28
+ else
29
+ source += ".*";
30
+ }
31
+ else
32
+ source += "[^/]*";
33
+ }
34
+ else if (char === "?")
35
+ source += "[^/]";
36
+ else
37
+ source += char.replace(/[\\^$+?.()|{}[\]]/g, "\\$&");
38
+ }
39
+ const compiled = new RegExp(`${source}$`);
40
+ if (globCache.size >= GLOB_CACHE_LIMIT) {
41
+ const oldest = globCache.keys().next().value;
42
+ if (oldest !== undefined)
43
+ globCache.delete(oldest);
44
+ }
45
+ globCache.set(normalized, compiled);
46
+ return compiled;
47
+ }
48
+ function matches(file, patterns) {
49
+ return patterns.some((pattern) => globRegex(normalizeRelative(pattern)).test(file));
50
+ }
51
+ function compilePatterns(patterns) {
52
+ return patterns.map((pattern) => globRegex(normalizeRelative(pattern)));
53
+ }
54
+ function matchesCompiled(file, patterns) {
55
+ return patterns.some((pattern) => pattern.test(file));
56
+ }
57
+ function walk(root, current = root, result = []) {
58
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
59
+ const absolute = path.join(current, entry.name);
60
+ const relative = normalizeRelative(path.relative(root, absolute));
61
+ if (matches(relative, DEFAULT_EXCLUDES) || matches(`${relative}/`, DEFAULT_EXCLUDES))
62
+ continue;
63
+ if (entry.isDirectory())
64
+ walk(root, absolute, result);
65
+ else if (entry.isFile())
66
+ result.push(relative);
67
+ }
68
+ return result;
69
+ }
70
+ function policyFor(file, policies) {
71
+ for (const { pattern, policy } of policies)
72
+ if (pattern.test(file))
73
+ return policy;
74
+ return "full";
75
+ }
76
+ function structureOnly(source, lineNumbers = false) {
77
+ return source
78
+ .split(/\r?\n/)
79
+ .map((line, index) => ({ line, index }))
80
+ .filter(({ line }) => /^\s*(export\s+)?(async\s+)?(class|interface|type|enum|function|const|let|var|def|module|namespace)\b/.test(line) || /^\s*(import|export)\b/.test(line))
81
+ .map(({ line, index }) => (lineNumbers ? `${index + 1}: ${line}` : line))
82
+ .join("\n");
83
+ }
84
+ function summarize(source, lineNumbers = false) {
85
+ const lines = source.split(/\r?\n/);
86
+ const structure = structureOnly(source, lineNumbers);
87
+ return [`[summary: ${lines.length} lines, ${Buffer.byteLength(source)} bytes]`, structure]
88
+ .filter(Boolean)
89
+ .join("\n");
90
+ }
91
+ function numbered(source) {
92
+ return source
93
+ .split(/\r?\n/)
94
+ .map((line, index) => `${index + 1}: ${line}`)
95
+ .join("\n");
96
+ }
97
+ function xmlEscape(value) {
98
+ return value
99
+ .replaceAll("&", "&amp;")
100
+ .replaceAll("<", "&lt;")
101
+ .replaceAll(">", "&gt;")
102
+ .replaceAll('"', "&quot;");
103
+ }
104
+ function serialize(format, files, tree, git) {
105
+ if (format === "json")
106
+ return JSON.stringify({ tree, files, git }, null, 2);
107
+ if (format === "xml") {
108
+ const treeXml = tree
109
+ ? `<tree>${tree.map((item) => `<path>${xmlEscape(item)}</path>`).join("")}</tree>`
110
+ : "";
111
+ const filesXml = files
112
+ .map((file) => `<file path="${xmlEscape(file.path)}" policy="${file.policy}"><![CDATA[${file.content.replaceAll("]]>", "]]]]><![CDATA[>")}]]></file>`)
113
+ .join("");
114
+ const gitXml = git
115
+ ? `<git>${git.diff === undefined ? "" : `<diff><![CDATA[${git.diff.replaceAll("]]>", "]]]]><![CDATA[>")}]]></diff>`}${git.log === undefined ? "" : `<log><![CDATA[${git.log.replaceAll("]]>", "]]]]><![CDATA[>")}]]></log>`}</git>`
116
+ : "";
117
+ return `<context>${treeXml}<files>${filesXml}</files>${gitXml}</context>`;
118
+ }
119
+ const fenced = (content, language = "") => {
120
+ const longest = Math.max(0, ...[...content.matchAll(/`+/g)].map((match) => match[0].length));
121
+ const fence = "`".repeat(Math.max(3, longest + 1));
122
+ return [`${fence}${language}`, content, fence];
123
+ };
124
+ const chunks = ["# knodin context export"];
125
+ if (tree)
126
+ chunks.push("## Tree", ...fenced(tree.join("\n"), "text"));
127
+ for (const file of files)
128
+ chunks.push(`## ${file.path} (${file.policy})`, ...fenced(file.content));
129
+ if (git?.diff !== undefined)
130
+ chunks.push("## Git diff", ...fenced(git.diff, "diff"));
131
+ if (git?.log !== undefined)
132
+ chunks.push("## Git log", ...fenced(git.log, "text"));
133
+ return chunks.join("\n");
134
+ }
135
+ /**
136
+ * Exact serialized-byte contribution for one additional file. Serializing a
137
+ * single candidate (plus a constant sentinel for non-first entries) keeps pack
138
+ * selection linear in total source bytes instead of repeatedly serializing the
139
+ * whole growing artifact.
140
+ */
141
+ function serializedFileByteDelta(format, file, hasFiles) {
142
+ if (!hasFiles)
143
+ return Buffer.byteLength(serialize(format, [file])) - Buffer.byteLength(serialize(format, []));
144
+ const sentinel = { path: "", policy: "full", content: "" };
145
+ return (Buffer.byteLength(serialize(format, [sentinel, file])) -
146
+ Buffer.byteLength(serialize(format, [sentinel])));
147
+ }
148
+ function gitSections(repo, request) {
149
+ if (!request)
150
+ return undefined;
151
+ const run = (args) => execFileSync("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
152
+ const result = {};
153
+ if (request.diffScope) {
154
+ const revision = (value) => {
155
+ const hasControl = [...value].some((character) => {
156
+ const code = character.charCodeAt(0);
157
+ return code < 32 || code === 127;
158
+ });
159
+ if (!value || value.startsWith("-") || hasControl)
160
+ throw new Error(`knodin pack: invalid git revision: ${value}`);
161
+ return value;
162
+ };
163
+ const args = request.diffScope === "unstaged"
164
+ ? ["diff", "--"]
165
+ : request.diffScope === "staged"
166
+ ? ["diff", "--cached", "--"]
167
+ : request.diffScope === "compare"
168
+ ? ["diff", revision(request.from ?? "HEAD~1"), revision(request.to ?? "HEAD"), "--"]
169
+ : ["diff", revision(request.from ?? "HEAD"), "--"];
170
+ result.diff = run(args);
171
+ }
172
+ if (request.log !== undefined)
173
+ result.log = run([
174
+ "log",
175
+ `-${Math.max(0, Math.min(100, Math.floor(request.log)))}`,
176
+ "--oneline",
177
+ ]);
178
+ return result;
179
+ }
180
+ function safeOutput(repo, output) {
181
+ const absolute = path.resolve(repo, output);
182
+ if (absolute !== repo && !absolute.startsWith(`${repo}${path.sep}`))
183
+ throw new Error("knodin pack: outputPath must stay inside the repository");
184
+ if (fs.existsSync(absolute)) {
185
+ const realTarget = fs.realpathSync(absolute);
186
+ if (realTarget !== repo && !realTarget.startsWith(`${repo}${path.sep}`))
187
+ throw new Error("knodin pack: outputPath may not follow a symlink outside the repository");
188
+ }
189
+ let parent = path.dirname(absolute);
190
+ while (!fs.existsSync(parent) && parent !== repo)
191
+ parent = path.dirname(parent);
192
+ const realParent = fs.realpathSync(parent);
193
+ if (realParent !== repo && !realParent.startsWith(`${repo}${path.sep}`))
194
+ throw new Error("knodin pack: outputPath may not traverse a symlink outside the repository");
195
+ return absolute;
196
+ }
197
+ export function exportContext(repoPath, request = {}) {
198
+ const repo = fs.realpathSync(repoPath);
199
+ const format = request.format ?? "markdown";
200
+ if (!["markdown", "json", "xml"].includes(format))
201
+ throw new Error("knodin pack: format must be markdown, json, or xml");
202
+ if (request.byteBudget !== undefined &&
203
+ (!Number.isFinite(request.byteBudget) || request.byteBudget < 256))
204
+ throw new Error("knodin pack: byteBudget must be at least 256");
205
+ if (request.tokenBudget !== undefined &&
206
+ (!Number.isFinite(request.tokenBudget) || request.tokenBudget < 64))
207
+ throw new Error("knodin pack: tokenBudget must be at least 64");
208
+ if (request.git?.diffScope !== undefined &&
209
+ !["unstaged", "staged", "all", "compare"].includes(request.git.diffScope))
210
+ throw new Error("knodin pack: invalid git diff scope");
211
+ if (request.git?.log !== undefined &&
212
+ (!Number.isInteger(request.git.log) || request.git.log < 0 || request.git.log > 100))
213
+ throw new Error("knodin pack: git log must be an integer from 0 to 100");
214
+ if (Object.values(request.policies ?? {}).some((policy) => !["full", "summary", "structure-only"].includes(policy)))
215
+ throw new Error("knodin pack: invalid file policy");
216
+ const byteLimit = Math.floor(request.byteBudget ?? 65_536);
217
+ const tokenLimit = Math.floor(request.tokenBudget ?? 16_384);
218
+ const hardLimit = Math.min(byteLimit, tokenLimit * 4);
219
+ const include = request.include?.length ? request.include : ["**"];
220
+ const exclude = [...DEFAULT_EXCLUDES, ...(request.exclude ?? [])];
221
+ const includePatterns = compilePatterns(include);
222
+ const excludePatterns = compilePatterns(exclude);
223
+ const policyRules = request.policies ?? {};
224
+ const policies = Object.keys(policyRules)
225
+ .sort(compareBytes)
226
+ .map((pattern) => ({
227
+ pattern: globRegex(normalizeRelative(pattern)),
228
+ policy: policyRules[pattern],
229
+ }));
230
+ const omitted = new Set([...(request.alreadyPresent ?? []), ...(request.chatFiles ?? [])].map(normalizeRelative));
231
+ if (request.outputPath)
232
+ omitted.add(normalizeRelative(request.outputPath));
233
+ const candidates = measurePerfPhaseSync("pack_walk", () => walk(repo))
234
+ .filter((file) => matchesCompiled(file, includePatterns) &&
235
+ !matchesCompiled(file, excludePatterns) &&
236
+ !omitted.has(file))
237
+ .sort(compareBytes);
238
+ let tree = request.includeTree ? [...candidates] : undefined;
239
+ let git = gitSections(repo, request.git);
240
+ const files = [];
241
+ const measuredSerialize = () => measurePerfPhaseSync("pack_serialize", () => serialize(format, files, tree, git));
242
+ let artifact = measuredSerialize();
243
+ if (Buffer.byteLength(artifact) > hardLimit) {
244
+ git = undefined;
245
+ artifact = measuredSerialize();
246
+ }
247
+ if (Buffer.byteLength(artifact) > hardLimit) {
248
+ tree = undefined;
249
+ artifact = measuredSerialize();
250
+ }
251
+ let serializedSize = Buffer.byteLength(artifact);
252
+ for (const file of candidates) {
253
+ const policy = policyFor(file, policies);
254
+ let content = fs.readFileSync(path.join(repo, file), "utf8");
255
+ if (policy === "summary")
256
+ content = summarize(content, request.lineNumbers);
257
+ else if (policy === "structure-only")
258
+ content = structureOnly(content, request.lineNumbers);
259
+ else if (request.lineNumbers)
260
+ content = numbered(content);
261
+ const packed = { path: file, policy, content };
262
+ const delta = measurePerfPhaseSync("pack_serialize", () => serializedFileByteDelta(format, packed, files.length > 0));
263
+ if (serializedSize + delta > hardLimit)
264
+ continue;
265
+ files.push(packed);
266
+ serializedSize += delta;
267
+ }
268
+ artifact = measuredSerialize();
269
+ const serializedBytes = Buffer.byteLength(artifact);
270
+ const outputAbsolute = request.outputPath ? safeOutput(repo, request.outputPath) : undefined;
271
+ const result = {
272
+ format,
273
+ artifact,
274
+ files: files.map(({ path: filePath, policy }) => ({ path: filePath, policy })),
275
+ telemetry: {
276
+ byteLimit,
277
+ tokenLimit,
278
+ serializedBytes,
279
+ responseBytes: 0,
280
+ estimatedTokens: Math.ceil(serializedBytes / 4),
281
+ tokenizer: "estimate:deterministic-utf8-bytes-divided-by-4",
282
+ includedFiles: files.length,
283
+ candidateFiles: candidates.length,
284
+ truncated: files.length < candidates.length,
285
+ },
286
+ ...(outputAbsolute
287
+ ? { outputPath: normalizeRelative(path.relative(repo, outputAbsolute)) }
288
+ : {}),
289
+ };
290
+ const updateResponseBytes = () => {
291
+ for (let index = 0; index < 4; index++) {
292
+ const actual = Buffer.byteLength(JSON.stringify(result));
293
+ if (actual === result.telemetry.responseBytes)
294
+ break;
295
+ result.telemetry.responseBytes = actual;
296
+ }
297
+ };
298
+ updateResponseBytes();
299
+ while (result.telemetry.responseBytes > hardLimit && files.length > 0) {
300
+ files.pop();
301
+ result.files.pop();
302
+ result.artifact = measuredSerialize();
303
+ result.telemetry.serializedBytes = Buffer.byteLength(result.artifact);
304
+ result.telemetry.estimatedTokens = Math.ceil(result.telemetry.serializedBytes / 4);
305
+ result.telemetry.includedFiles = files.length;
306
+ result.telemetry.truncated = true;
307
+ updateResponseBytes();
308
+ }
309
+ if (result.telemetry.responseBytes > hardLimit)
310
+ throw new Error("knodin pack: budget is too small for the requested format and telemetry");
311
+ if (outputAbsolute) {
312
+ fs.mkdirSync(path.dirname(outputAbsolute), { recursive: true });
313
+ fs.writeFileSync(outputAbsolute, result.artifact);
314
+ }
315
+ return result;
316
+ }
317
+ function safeArtifact(repoPath, artifactPath) {
318
+ const repo = fs.realpathSync(repoPath);
319
+ const absolute = path.resolve(repo, artifactPath);
320
+ if (absolute !== repo && !absolute.startsWith(`${repo}${path.sep}`))
321
+ throw new Error("knodin pack: artifactPath must stay inside the repository");
322
+ const real = fs.realpathSync(absolute);
323
+ if (real !== repo && !real.startsWith(`${repo}${path.sep}`))
324
+ throw new Error("knodin pack: artifactPath may not follow a symlink outside the repository");
325
+ const stat = fs.statSync(real);
326
+ if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
327
+ throw new Error("knodin pack: artifactPath must be a packed file no larger than 16 MiB");
328
+ return real;
329
+ }
330
+ const ARTIFACT_CACHE_ENTRY_LIMIT = 2;
331
+ const ARTIFACT_CACHE_FILE_LIMIT = 4 * 1024 * 1024;
332
+ const artifactLineCache = new Map();
333
+ function artifactLines(repoPath, artifactPath) {
334
+ const real = safeArtifact(repoPath, artifactPath);
335
+ const stat = fs.statSync(real);
336
+ const cached = artifactLineCache.get(real);
337
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
338
+ artifactLineCache.delete(real);
339
+ artifactLineCache.set(real, cached);
340
+ return cached.lines;
341
+ }
342
+ if (cached)
343
+ artifactLineCache.delete(real);
344
+ const lines = fs.readFileSync(real, "utf8").split(/\r?\n/);
345
+ if (stat.size <= ARTIFACT_CACHE_FILE_LIMIT) {
346
+ while (artifactLineCache.size >= ARTIFACT_CACHE_ENTRY_LIMIT) {
347
+ const oldest = artifactLineCache.keys().next().value;
348
+ if (oldest === undefined)
349
+ break;
350
+ artifactLineCache.delete(oldest);
351
+ }
352
+ artifactLineCache.set(real, { mtimeMs: stat.mtimeMs, size: stat.size, lines });
353
+ }
354
+ return lines;
355
+ }
356
+ function truncateUtf8(value, byteLimit) {
357
+ if (Buffer.byteLength(value) <= byteLimit)
358
+ return value;
359
+ let low = 0;
360
+ let high = value.length;
361
+ while (low < high) {
362
+ const midpoint = Math.ceil((low + high) / 2);
363
+ if (Buffer.byteLength(value.slice(0, midpoint)) <= byteLimit)
364
+ low = midpoint;
365
+ else
366
+ high = midpoint - 1;
367
+ }
368
+ return value.slice(0, low);
369
+ }
370
+ export function readPackedArtifact(repoPath, artifactPath, startLine = 1, endLine = startLine + 199, byteBudget = 16_384) {
371
+ if (!Number.isFinite(byteBudget) || !Number.isInteger(byteBudget) || byteBudget < 256)
372
+ throw new Error("knodin pack read: byteBudget must be an integer >= 256");
373
+ if (!Number.isInteger(startLine) ||
374
+ !Number.isInteger(endLine) ||
375
+ startLine < 1 ||
376
+ endLine < startLine ||
377
+ endLine - startLine > 999)
378
+ throw new Error("knodin pack read: invalid or unbounded line range");
379
+ const lines = measurePerfPhaseSync("artifact_read", () => artifactLines(repoPath, artifactPath).slice(startLine - 1, endLine));
380
+ let content = lines.join("\n");
381
+ const limit = Math.max(256, Math.min(65_536, Math.floor(byteBudget)));
382
+ content = truncateUtf8(content, limit);
383
+ return {
384
+ artifactPath: normalizeRelative(artifactPath),
385
+ startLine,
386
+ endLine: startLine + content.split(/\r?\n/).length - 1,
387
+ content,
388
+ truncated: lines.join("\n") !== content,
389
+ };
390
+ }
391
+ export function grepPackedArtifact(repoPath, artifactPath, pattern, flags = "", limit = 100) {
392
+ if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > 1000)
393
+ throw new Error("knodin pack grep: limit must be an integer from 1 to 1000");
394
+ if (pattern.length === 0 || pattern.length > 1000 || !/^[gimsuy]*$/.test(flags))
395
+ throw new Error("knodin pack grep: invalid regex or flags");
396
+ let regex;
397
+ try {
398
+ const safeFlags = flags.replace("g", "").includes("u")
399
+ ? flags.replace("g", "")
400
+ : `${flags.replace("g", "")}u`;
401
+ regex = new RE2(pattern, safeFlags);
402
+ }
403
+ catch {
404
+ throw new Error("knodin pack grep: invalid or unsupported linear-time regex");
405
+ }
406
+ const matches = [];
407
+ measurePerfPhaseSync("artifact_grep", () => {
408
+ for (const [index, text] of artifactLines(repoPath, artifactPath).entries()) {
409
+ regex.lastIndex = 0;
410
+ if (regex.test(text))
411
+ matches.push({ line: index + 1, text });
412
+ if (matches.length >= Math.max(1, Math.min(1000, Math.floor(limit))))
413
+ break;
414
+ }
415
+ });
416
+ return {
417
+ artifactPath: normalizeRelative(artifactPath),
418
+ pattern,
419
+ flags,
420
+ matches,
421
+ truncated: matches.length >= limit,
422
+ };
423
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Logic for the `knodin context` operation — the "call this FIRST" ultra-
3
+ * compact orientation. Lives in its own neutral module, mirroring
4
+ * `pr-triage.ts`'s role for the `prs` operation: `bin/cli.ts` and
5
+ * `src/tools/knodin-tools.ts` both import from here independently, rather
6
+ * than the CLI reaching into the MCP-gateway module the way it previously did
7
+ * (code review finding — R23-R28 reuse/altitude pass).
8
+ */
9
+ import { measurePerfPhase } from "./engine/perf.js";
10
+ /** Keyword heuristic mapping a free-text `task` to a suggested next knodin
11
+ * operation for the `context` op. This is explicitly a hint, not a claim of
12
+ * understanding intent — a wrong guess must never stop the caller from reaching
13
+ * for any operation directly (R26). Order matters: review-shaped tasks win over
14
+ * map-shaped ones, and a bare single-symbol token routes to `explain`. */
15
+ export function suggestNextOperation(task) {
16
+ const t = task.toLowerCase();
17
+ if (/\b(bug|review|diff|pr|pull request|regression|risk|approve|merge)\b/.test(t)) {
18
+ return "review";
19
+ }
20
+ if (/\b(batch outline|file outline|outline files?|project overview|directory overview)\b/.test(t)) {
21
+ return "query";
22
+ }
23
+ if (/\b(how does|how do|architecture|architectural|structure|structured|design|subsystem|boundaries|overview)\b/.test(t)) {
24
+ return "map";
25
+ }
26
+ // A bare identifier-shaped token (a single symbol name) → explain it directly.
27
+ if (/^[A-Za-z_]\w*$/.test(task.trim())) {
28
+ return "explain";
29
+ }
30
+ // Otherwise no exact symbol is known → fuzzy search to find a starting point.
31
+ return "search";
32
+ }
33
+ function suggestedStructuralPattern(task) {
34
+ const normalized = task.toLowerCase();
35
+ if (/\bbatch outline\b/.test(normalized))
36
+ return "batch_outline";
37
+ if (/\b(project|directory) overview\b/.test(normalized))
38
+ return "project_overview";
39
+ if (/\bfile outline\b|\boutline (?:the )?file\b/.test(normalized))
40
+ return "file_summary";
41
+ return undefined;
42
+ }
43
+ /**
44
+ * R26 `context` operation — the "call this FIRST" ultra-compact orientation.
45
+ * Composes EXISTING public engine methods only (`map`, `query stats`,
46
+ * `query flows`, `review`) so it never touches `src/engine/index.ts` and stays
47
+ * parallel-safe against the query-dispatch work landing there. Takes an
48
+ * `engine: KnodinEngine` parameter rather than constructing its own, so the
49
+ * `knodin-tools.ts` module-level `createEngine()` singleton is never
50
+ * duplicated or side-effected by this module's existence.
51
+ */
52
+ export async function buildKnodinContext(eng, task, repo, base, changedFiles) {
53
+ return measurePerfPhase("context_composition", async () => {
54
+ // Establish one full graph snapshot first: stats requires that same standard
55
+ // map, so asking for a separate minimal map would run community analytics
56
+ // twice. Once initialized, the remaining read-only facets can safely share
57
+ // the generation and freshness lease.
58
+ const CONTEXT_TOP = 5;
59
+ const mapResult = await eng.map(repo, "standard");
60
+ const shouldReview = changedFiles === undefined || changedFiles.length > 0;
61
+ const [statsResult, flowsResult, review] = await Promise.all([
62
+ eng.query("stats", "", repo),
63
+ eng.query("flows", "", repo, undefined, CONTEXT_TOP),
64
+ shouldReview ? eng.review(base ?? "HEAD~1", repo, "minimal") : Promise.resolve(undefined),
65
+ ]);
66
+ // Risk score: eng.review() always re-derives the diff from `git diff <base>`;
67
+ // it has no seam for an explicit file list, so `changedFiles`, when supplied,
68
+ // is only a caller hint — an explicit empty array means "no changes, skip the
69
+ // diff". When there IS a diff but it touches no indexed symbols, review
70
+ // returns changedSymbolCount 0 and a meaningless riskScore 0; omit the field
71
+ // in that case rather than report a misleading 0 (R26 acceptance criterion).
72
+ let riskScore;
73
+ if (review && review.changedSymbolCount > 0)
74
+ riskScore = review.riskScore;
75
+ // Truncate aggressively — this is a ~100-300 token orientation, not a dump of
76
+ // map()'s full payload. Sort defensively so "top" holds even if an engine
77
+ // method's ordering changes; flows already arrive most-critical first (capped
78
+ // at CONTEXT_TOP via the query limit above).
79
+ return {
80
+ repoPath: repo,
81
+ task,
82
+ stats: statsResult.totals,
83
+ communities: [...mapResult.communities]
84
+ .sort((a, b) => b.size - a.size)
85
+ .slice(0, CONTEXT_TOP)
86
+ .map((c) => ({ name: c.name, size: c.size })),
87
+ hubs: [...mapResult.hubs]
88
+ .sort((a, b) => b.degree - a.degree)
89
+ .slice(0, CONTEXT_TOP)
90
+ .map((h) => ({ symbol: h.symbol, degree: h.degree })),
91
+ flows: flowsResult.results
92
+ .slice(0, CONTEXT_TOP)
93
+ .map((f) => ({ symbol: f.symbol, criticality: f.criticality })),
94
+ ...(riskScore !== undefined ? { riskScore } : {}),
95
+ suggestedOperation: suggestNextOperation(task),
96
+ ...(suggestedStructuralPattern(task)
97
+ ? { suggestedQueryPattern: suggestedStructuralPattern(task) }
98
+ : {}),
99
+ suggestionNote: "Heuristic from keywords in `task`; a hint only — any operation can be called directly regardless.",
100
+ };
101
+ });
102
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Deterministic pseudo-random number generation.
3
+ *
4
+ * knodin never calls `Math.random` in production code: index topology, graph
5
+ * layout, and every artifact they feed must be reproducible so that a rerun
6
+ * over unchanged evidence produces a byte-identical result. Callers that need
7
+ * randomness seed one of these generators instead.
8
+ */
9
+ /** Default seed (golden-ratio constant) shared by callers without their own. */
10
+ export const DEFAULT_PRNG_SEED = 0x9e3779b9;
11
+ /** Deterministic PRNG so derived structures are reproducible across runs. */
12
+ export function mulberry32(seed) {
13
+ let a = seed >>> 0;
14
+ return () => {
15
+ a |= 0;
16
+ a = (a + 0x6d2b79f5) | 0;
17
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
18
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
19
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
20
+ };
21
+ }
22
+ /**
23
+ * Folds a string into a 32-bit seed (FNV-1a). Lets a caller derive a stable
24
+ * seed from repository-scoped evidence such as a commit id, so the same
25
+ * evidence always produces the same output.
26
+ */
27
+ export function seedFromString(value) {
28
+ let hash = 0x811c9dc5;
29
+ for (let index = 0; index < value.length; index++) {
30
+ hash ^= value.charCodeAt(index);
31
+ hash = Math.imul(hash, 0x01000193);
32
+ }
33
+ return hash >>> 0;
34
+ }