knodin 0.7.4 → 0.7.6

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 (104) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/cli.js +108 -1859
  3. package/dist/bin/launcher.js +3 -10
  4. package/dist/chunks/chunk-DMQAGX77.js +654 -0
  5. package/dist/chunks/chunk-F4Z3Z766.js +4 -0
  6. package/dist/chunks/chunk-SIJAQVSX.js +3 -0
  7. package/dist/chunks/chunk-X6M4HUUE.js +2 -0
  8. package/dist/chunks/chunk-YPRMY2LP.js +8 -0
  9. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
  10. package/dist/chunks/server-7EDF4CBY.js +14 -0
  11. package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
  12. package/dist/src/init-progress-worker.js +2 -106
  13. package/dist/src/repair-progress-worker.js +2 -120
  14. package/docs/INSTALLATION.md +17 -1
  15. package/docs/releases/0.7.6.md +25 -0
  16. package/package.json +1 -110
  17. package/dist/src/agent-integration.js +0 -238
  18. package/dist/src/artifact-refresh.js +0 -81
  19. package/dist/src/cli-args.js +0 -274
  20. package/dist/src/cli-model.js +0 -358
  21. package/dist/src/codeflow-replay.js +0 -80
  22. package/dist/src/compact-structural.js +0 -96
  23. package/dist/src/competitive-cold-mcp.js +0 -40
  24. package/dist/src/competitive-constraints.js +0 -20
  25. package/dist/src/competitive-manifest.js +0 -411
  26. package/dist/src/competitive-measurement.js +0 -183
  27. package/dist/src/competitive-runner.js +0 -487
  28. package/dist/src/competitive-sandbox.js +0 -108
  29. package/dist/src/context-export.js +0 -422
  30. package/dist/src/context.js +0 -102
  31. package/dist/src/diagnostics.js +0 -449
  32. package/dist/src/docs-sections.js +0 -141
  33. package/dist/src/doctor.js +0 -380
  34. package/dist/src/engine/ann-hnsw.js +0 -271
  35. package/dist/src/engine/embeddings.js +0 -193
  36. package/dist/src/engine/file-walker.js +0 -43
  37. package/dist/src/engine/git-history.js +0 -289
  38. package/dist/src/engine/index.js +0 -13377
  39. package/dist/src/engine/perf.js +0 -115
  40. package/dist/src/engine/prune.js +0 -112
  41. package/dist/src/engine/scip-import.js +0 -408
  42. package/dist/src/engine/source-policy.js +0 -69
  43. package/dist/src/engine/sqlite.js +0 -71
  44. package/dist/src/engine/symbol-delete.js +0 -58
  45. package/dist/src/execution-profile.js +0 -203
  46. package/dist/src/failure-diagnosis.js +0 -649
  47. package/dist/src/fleet.js +0 -7
  48. package/dist/src/git-executable.js +0 -31
  49. package/dist/src/graph-query-health.js +0 -115
  50. package/dist/src/hook-manager-integration.js +0 -156
  51. package/dist/src/index-activity.js +0 -125
  52. package/dist/src/init-progress.js +0 -155
  53. package/dist/src/init.js +0 -1254
  54. package/dist/src/lifecycle-health.js +0 -250
  55. package/dist/src/lsp-readonly.js +0 -217
  56. package/dist/src/output-compression.js +0 -629
  57. package/dist/src/output-telemetry.js +0 -363
  58. package/dist/src/pr-triage.js +0 -638
  59. package/dist/src/progressive-evidence.js +0 -473
  60. package/dist/src/pure-compression-cli.js +0 -101
  61. package/dist/src/relationship-adapters.js +0 -370
  62. package/dist/src/release-attestation.js +0 -533
  63. package/dist/src/release-preflight.js +0 -510
  64. package/dist/src/repair-progress.js +0 -262
  65. package/dist/src/repository-init-process.js +0 -173
  66. package/dist/src/repository-management.js +0 -1231
  67. package/dist/src/response-budget.js +0 -194
  68. package/dist/src/server.js +0 -73
  69. package/dist/src/structural-fast-path.js +0 -338
  70. package/dist/src/structural-snapshot.js +0 -33
  71. package/dist/src/system-config.js +0 -615
  72. package/dist/src/terminal-help.js +0 -83
  73. package/dist/src/tools/knodin-tools.js +0 -1529
  74. package/dist/src/update-ceremony.js +0 -158
  75. package/dist/src/update-policy.js +0 -944
  76. package/dist/src/update-trust.js +0 -503
  77. package/dist/src/version.js +0 -13
  78. package/dist/src/visualization.js +0 -162
  79. package/dist/src/wait-for-fresh.js +0 -98
  80. package/dist/src/worktree-lifecycle.js +0 -231
  81. package/docs/CONTAINED-EXECUTION.md +0 -77
  82. package/docs/DIAGNOSTICS.md +0 -45
  83. package/docs/GIT-HISTORY-REVIEW.md +0 -39
  84. package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
  85. package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
  86. package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
  87. package/docs/SCIP-IMPORT.md +0 -57
  88. package/docs/SIGNED-UPDATES.md +0 -151
  89. package/docs/TELEMETRY.md +0 -46
  90. package/docs/assets/knodin-favicon.svg +0 -4
  91. package/docs/releases/0.3.0.md +0 -46
  92. package/docs/releases/0.4.0.md +0 -68
  93. package/docs/releases/0.4.1.md +0 -28
  94. package/docs/releases/0.4.2.md +0 -27
  95. package/docs/releases/0.4.3.md +0 -23
  96. package/docs/releases/0.5.0.md +0 -29
  97. package/docs/releases/0.5.1.md +0 -17
  98. package/docs/releases/0.6.0.md +0 -18
  99. package/docs/releases/0.7.0.md +0 -24
  100. package/docs/releases/0.7.1.md +0 -21
  101. package/docs/releases/0.7.2.md +0 -21
  102. package/docs/releases/0.7.3.md +0 -23
  103. package/docs/releases/0.7.4.md +0 -17
  104. package/schemas/release-attestation-v1.schema.json +0 -210
@@ -1,108 +0,0 @@
1
- import childProcess from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- function runGit(args, cwd, environment) {
6
- const result = childProcess.spawnSync("git", args, {
7
- cwd,
8
- env: environment,
9
- encoding: "utf8",
10
- });
11
- if (result.status !== 0) {
12
- throw new Error(`Unable to create competitive sandbox: git ${args.join(" ")} failed\n${result.stderr.trim()}`);
13
- }
14
- }
15
- function executableOnPath(name) {
16
- for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
17
- if (!directory)
18
- continue;
19
- const candidate = path.join(directory, name);
20
- try {
21
- fs.accessSync(candidate, fs.constants.X_OK);
22
- return fs.realpathSync(candidate);
23
- }
24
- catch {
25
- // Continue searching PATH.
26
- }
27
- }
28
- return undefined;
29
- }
30
- /**
31
- * Give a stateful competitor a disposable checkout and user-level state tree.
32
- *
33
- * The clone borrows immutable Git objects from the source repository, but owns
34
- * its refs, index, worktree, and generated graph data. Dependencies are exposed
35
- * read-only-by-convention through a node_modules symlink so benchmark adapters
36
- * can execute without a second package installation.
37
- */
38
- export function createCompetitiveSandbox(sourceRepository, sourceFixture, competitorId) {
39
- const root = fs.mkdtempSync(path.join(os.tmpdir(), `knodin-${competitorId}-sandbox-`));
40
- const repository = path.join(root, "repository");
41
- const fixture = path.join(root, "fixture");
42
- const home = path.join(root, "home");
43
- const temporary = path.join(root, "tmp");
44
- const cache = path.join(root, "cache");
45
- const config = path.join(root, "config");
46
- const data = path.join(root, "data");
47
- const state = path.join(root, "state");
48
- const binaries = path.join(root, "bin");
49
- for (const directory of [home, temporary, cache, config, data, state, binaries]) {
50
- fs.mkdirSync(directory, { recursive: true });
51
- }
52
- const realBun = executableOnPath("bun");
53
- const environment = {
54
- ...process.env,
55
- HOME: home,
56
- TMPDIR: temporary,
57
- XDG_CACHE_HOME: cache,
58
- XDG_CONFIG_HOME: config,
59
- XDG_DATA_HOME: data,
60
- XDG_STATE_HOME: state,
61
- NPM_CONFIG_CACHE: path.join(cache, "npm"),
62
- BUN_INSTALL_CACHE_DIR: path.join(cache, "bun"),
63
- PIP_CACHE_DIR: path.join(cache, "pip"),
64
- CLAUDE_CONFIG_DIR: path.join(config, "claude"),
65
- GIT_CONFIG_GLOBAL: path.join(config, "gitconfig"),
66
- GIT_CONFIG_NOSYSTEM: "1",
67
- PATH: `${binaries}${path.delimiter}${process.env.PATH ?? ""}`,
68
- KNODIN_BENCHMARK_SOURCE_REPOSITORY: sourceRepository,
69
- ...(realBun ? { KNODIN_BENCHMARK_REAL_BUN: realBun } : {}),
70
- };
71
- try {
72
- if (realBun) {
73
- const bunShim = path.join(binaries, "bun");
74
- fs.writeFileSync(bunShim, `#!/bin/sh
75
- if [ "$1" = "src/server.ts" ]; then
76
- shift
77
- exec node --import tsx "$PWD/src/server.ts" "$@"
78
- fi
79
- exec "$KNODIN_BENCHMARK_REAL_BUN" "$@"
80
- `, { mode: 0o755 });
81
- }
82
- runGit(["clone", "--quiet", "--shared", "--no-checkout", sourceRepository, repository], root, environment);
83
- const revision = childProcess
84
- .spawnSync("git", ["rev-parse", "HEAD"], {
85
- cwd: sourceRepository,
86
- encoding: "utf8",
87
- })
88
- .stdout.trim();
89
- runGit(["checkout", "--quiet", "--detach", revision], repository, environment);
90
- const dependencies = path.join(sourceRepository, "node_modules");
91
- if (fs.existsSync(dependencies)) {
92
- fs.symlinkSync(dependencies, path.join(repository, "node_modules"), "dir");
93
- }
94
- fs.cpSync(sourceFixture, fixture, { recursive: true });
95
- return {
96
- root,
97
- repository,
98
- fixture,
99
- output: path.join(root, "raw-result.json"),
100
- environment,
101
- dispose: () => fs.rmSync(root, { recursive: true, force: true }),
102
- };
103
- }
104
- catch (error) {
105
- fs.rmSync(root, { recursive: true, force: true });
106
- throw error;
107
- }
108
- }
@@ -1,422 +0,0 @@
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 { measurePerfPhaseSync } from "./engine/perf.js";
6
- const DEFAULT_EXCLUDES = [".git/**", ".knodin/**", "node_modules/**", "dist/**", "build/**"];
7
- const GLOB_CACHE_LIMIT = 256;
8
- const globCache = new Map();
9
- function normalizeRelative(value) {
10
- return value.replaceAll("\\", "/").replace(/^\.\//, "");
11
- }
12
- function globRegex(glob) {
13
- const normalized = normalizeRelative(glob);
14
- const cached = globCache.get(normalized);
15
- if (cached)
16
- return cached;
17
- let source = "^";
18
- for (let i = 0; i < normalized.length; i++) {
19
- const char = normalized[i];
20
- if (char === "*") {
21
- if (normalized[i + 1] === "*") {
22
- i++;
23
- if (normalized[i + 1] === "/") {
24
- i++;
25
- source += "(?:.*/)?";
26
- }
27
- else
28
- source += ".*";
29
- }
30
- else
31
- source += "[^/]*";
32
- }
33
- else if (char === "?")
34
- source += "[^/]";
35
- else
36
- source += char.replace(/[\\^$+?.()|{}[\]]/g, "\\$&");
37
- }
38
- const compiled = new RegExp(`${source}$`);
39
- if (globCache.size >= GLOB_CACHE_LIMIT) {
40
- const oldest = globCache.keys().next().value;
41
- if (oldest !== undefined)
42
- globCache.delete(oldest);
43
- }
44
- globCache.set(normalized, compiled);
45
- return compiled;
46
- }
47
- function matches(file, patterns) {
48
- return patterns.some((pattern) => globRegex(normalizeRelative(pattern)).test(file));
49
- }
50
- function compilePatterns(patterns) {
51
- return patterns.map((pattern) => globRegex(normalizeRelative(pattern)));
52
- }
53
- function matchesCompiled(file, patterns) {
54
- return patterns.some((pattern) => pattern.test(file));
55
- }
56
- function walk(root, current = root, result = []) {
57
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
58
- const absolute = path.join(current, entry.name);
59
- const relative = normalizeRelative(path.relative(root, absolute));
60
- if (matches(relative, DEFAULT_EXCLUDES) || matches(`${relative}/`, DEFAULT_EXCLUDES))
61
- continue;
62
- if (entry.isDirectory())
63
- walk(root, absolute, result);
64
- else if (entry.isFile())
65
- result.push(relative);
66
- }
67
- return result;
68
- }
69
- function policyFor(file, policies) {
70
- for (const { pattern, policy } of policies)
71
- if (pattern.test(file))
72
- return policy;
73
- return "full";
74
- }
75
- function structureOnly(source, lineNumbers = false) {
76
- return source
77
- .split(/\r?\n/)
78
- .map((line, index) => ({ line, index }))
79
- .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))
80
- .map(({ line, index }) => (lineNumbers ? `${index + 1}: ${line}` : line))
81
- .join("\n");
82
- }
83
- function summarize(source, lineNumbers = false) {
84
- const lines = source.split(/\r?\n/);
85
- const structure = structureOnly(source, lineNumbers);
86
- return [`[summary: ${lines.length} lines, ${Buffer.byteLength(source)} bytes]`, structure]
87
- .filter(Boolean)
88
- .join("\n");
89
- }
90
- function numbered(source) {
91
- return source
92
- .split(/\r?\n/)
93
- .map((line, index) => `${index + 1}: ${line}`)
94
- .join("\n");
95
- }
96
- function xmlEscape(value) {
97
- return value
98
- .replaceAll("&", "&amp;")
99
- .replaceAll("<", "&lt;")
100
- .replaceAll(">", "&gt;")
101
- .replaceAll('"', "&quot;");
102
- }
103
- function serialize(format, files, tree, git) {
104
- if (format === "json")
105
- return JSON.stringify({ tree, files, git }, null, 2);
106
- if (format === "xml") {
107
- const treeXml = tree
108
- ? `<tree>${tree.map((item) => `<path>${xmlEscape(item)}</path>`).join("")}</tree>`
109
- : "";
110
- const filesXml = files
111
- .map((file) => `<file path="${xmlEscape(file.path)}" policy="${file.policy}"><![CDATA[${file.content.replaceAll("]]>", "]]]]><![CDATA[>")}]]></file>`)
112
- .join("");
113
- const gitXml = git
114
- ? `<git>${git.diff === undefined ? "" : `<diff><![CDATA[${git.diff.replaceAll("]]>", "]]]]><![CDATA[>")}]]></diff>`}${git.log === undefined ? "" : `<log><![CDATA[${git.log.replaceAll("]]>", "]]]]><![CDATA[>")}]]></log>`}</git>`
115
- : "";
116
- return `<context>${treeXml}<files>${filesXml}</files>${gitXml}</context>`;
117
- }
118
- const fenced = (content, language = "") => {
119
- const longest = Math.max(0, ...[...content.matchAll(/`+/g)].map((match) => match[0].length));
120
- const fence = "`".repeat(Math.max(3, longest + 1));
121
- return [`${fence}${language}`, content, fence];
122
- };
123
- const chunks = ["# knodin context export"];
124
- if (tree)
125
- chunks.push("## Tree", ...fenced(tree.join("\n"), "text"));
126
- for (const file of files)
127
- chunks.push(`## ${file.path} (${file.policy})`, ...fenced(file.content));
128
- if (git?.diff !== undefined)
129
- chunks.push("## Git diff", ...fenced(git.diff, "diff"));
130
- if (git?.log !== undefined)
131
- chunks.push("## Git log", ...fenced(git.log, "text"));
132
- return chunks.join("\n");
133
- }
134
- /**
135
- * Exact serialized-byte contribution for one additional file. Serializing a
136
- * single candidate (plus a constant sentinel for non-first entries) keeps pack
137
- * selection linear in total source bytes instead of repeatedly serializing the
138
- * whole growing artifact.
139
- */
140
- function serializedFileByteDelta(format, file, hasFiles) {
141
- if (!hasFiles)
142
- return Buffer.byteLength(serialize(format, [file])) - Buffer.byteLength(serialize(format, []));
143
- const sentinel = { path: "", policy: "full", content: "" };
144
- return (Buffer.byteLength(serialize(format, [sentinel, file])) -
145
- Buffer.byteLength(serialize(format, [sentinel])));
146
- }
147
- function gitSections(repo, request) {
148
- if (!request)
149
- return undefined;
150
- const run = (args) => execFileSync("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
151
- const result = {};
152
- if (request.diffScope) {
153
- const revision = (value) => {
154
- const hasControl = [...value].some((character) => {
155
- const code = character.charCodeAt(0);
156
- return code < 32 || code === 127;
157
- });
158
- if (!value || value.startsWith("-") || hasControl)
159
- throw new Error(`knodin pack: invalid git revision: ${value}`);
160
- return value;
161
- };
162
- const args = request.diffScope === "unstaged"
163
- ? ["diff", "--"]
164
- : request.diffScope === "staged"
165
- ? ["diff", "--cached", "--"]
166
- : request.diffScope === "compare"
167
- ? ["diff", revision(request.from ?? "HEAD~1"), revision(request.to ?? "HEAD"), "--"]
168
- : ["diff", revision(request.from ?? "HEAD"), "--"];
169
- result.diff = run(args);
170
- }
171
- if (request.log !== undefined)
172
- result.log = run([
173
- "log",
174
- `-${Math.max(0, Math.min(100, Math.floor(request.log)))}`,
175
- "--oneline",
176
- ]);
177
- return result;
178
- }
179
- function safeOutput(repo, output) {
180
- const absolute = path.resolve(repo, output);
181
- if (absolute !== repo && !absolute.startsWith(`${repo}${path.sep}`))
182
- throw new Error("knodin pack: outputPath must stay inside the repository");
183
- if (fs.existsSync(absolute)) {
184
- const realTarget = fs.realpathSync(absolute);
185
- if (realTarget !== repo && !realTarget.startsWith(`${repo}${path.sep}`))
186
- throw new Error("knodin pack: outputPath may not follow a symlink outside the repository");
187
- }
188
- let parent = path.dirname(absolute);
189
- while (!fs.existsSync(parent) && parent !== repo)
190
- parent = path.dirname(parent);
191
- const realParent = fs.realpathSync(parent);
192
- if (realParent !== repo && !realParent.startsWith(`${repo}${path.sep}`))
193
- throw new Error("knodin pack: outputPath may not traverse a symlink outside the repository");
194
- return absolute;
195
- }
196
- export function exportContext(repoPath, request = {}) {
197
- const repo = fs.realpathSync(repoPath);
198
- const format = request.format ?? "markdown";
199
- if (!["markdown", "json", "xml"].includes(format))
200
- throw new Error("knodin pack: format must be markdown, json, or xml");
201
- if (request.byteBudget !== undefined &&
202
- (!Number.isFinite(request.byteBudget) || request.byteBudget < 256))
203
- throw new Error("knodin pack: byteBudget must be at least 256");
204
- if (request.tokenBudget !== undefined &&
205
- (!Number.isFinite(request.tokenBudget) || request.tokenBudget < 64))
206
- throw new Error("knodin pack: tokenBudget must be at least 64");
207
- if (request.git?.diffScope !== undefined &&
208
- !["unstaged", "staged", "all", "compare"].includes(request.git.diffScope))
209
- throw new Error("knodin pack: invalid git diff scope");
210
- if (request.git?.log !== undefined &&
211
- (!Number.isInteger(request.git.log) || request.git.log < 0 || request.git.log > 100))
212
- throw new Error("knodin pack: git log must be an integer from 0 to 100");
213
- if (Object.values(request.policies ?? {}).some((policy) => !["full", "summary", "structure-only"].includes(policy)))
214
- throw new Error("knodin pack: invalid file policy");
215
- const byteLimit = Math.floor(request.byteBudget ?? 65_536);
216
- const tokenLimit = Math.floor(request.tokenBudget ?? 16_384);
217
- const hardLimit = Math.min(byteLimit, tokenLimit * 4);
218
- const include = request.include?.length ? request.include : ["**"];
219
- const exclude = [...DEFAULT_EXCLUDES, ...(request.exclude ?? [])];
220
- const includePatterns = compilePatterns(include);
221
- const excludePatterns = compilePatterns(exclude);
222
- const policyRules = request.policies ?? {};
223
- const policies = Object.keys(policyRules)
224
- .sort()
225
- .map((pattern) => ({
226
- pattern: globRegex(normalizeRelative(pattern)),
227
- policy: policyRules[pattern],
228
- }));
229
- const omitted = new Set([...(request.alreadyPresent ?? []), ...(request.chatFiles ?? [])].map(normalizeRelative));
230
- if (request.outputPath)
231
- omitted.add(normalizeRelative(request.outputPath));
232
- const candidates = measurePerfPhaseSync("pack_walk", () => walk(repo))
233
- .filter((file) => matchesCompiled(file, includePatterns) &&
234
- !matchesCompiled(file, excludePatterns) &&
235
- !omitted.has(file))
236
- .sort();
237
- let tree = request.includeTree ? [...candidates] : undefined;
238
- let git = gitSections(repo, request.git);
239
- const files = [];
240
- const measuredSerialize = () => measurePerfPhaseSync("pack_serialize", () => serialize(format, files, tree, git));
241
- let artifact = measuredSerialize();
242
- if (Buffer.byteLength(artifact) > hardLimit) {
243
- git = undefined;
244
- artifact = measuredSerialize();
245
- }
246
- if (Buffer.byteLength(artifact) > hardLimit) {
247
- tree = undefined;
248
- artifact = measuredSerialize();
249
- }
250
- let serializedSize = Buffer.byteLength(artifact);
251
- for (const file of candidates) {
252
- const policy = policyFor(file, policies);
253
- let content = fs.readFileSync(path.join(repo, file), "utf8");
254
- if (policy === "summary")
255
- content = summarize(content, request.lineNumbers);
256
- else if (policy === "structure-only")
257
- content = structureOnly(content, request.lineNumbers);
258
- else if (request.lineNumbers)
259
- content = numbered(content);
260
- const packed = { path: file, policy, content };
261
- const delta = measurePerfPhaseSync("pack_serialize", () => serializedFileByteDelta(format, packed, files.length > 0));
262
- if (serializedSize + delta > hardLimit)
263
- continue;
264
- files.push(packed);
265
- serializedSize += delta;
266
- }
267
- artifact = measuredSerialize();
268
- const serializedBytes = Buffer.byteLength(artifact);
269
- const outputAbsolute = request.outputPath ? safeOutput(repo, request.outputPath) : undefined;
270
- const result = {
271
- format,
272
- artifact,
273
- files: files.map(({ path: filePath, policy }) => ({ path: filePath, policy })),
274
- telemetry: {
275
- byteLimit,
276
- tokenLimit,
277
- serializedBytes,
278
- responseBytes: 0,
279
- estimatedTokens: Math.ceil(serializedBytes / 4),
280
- tokenizer: "estimate:deterministic-utf8-bytes-divided-by-4",
281
- includedFiles: files.length,
282
- candidateFiles: candidates.length,
283
- truncated: files.length < candidates.length,
284
- },
285
- ...(outputAbsolute
286
- ? { outputPath: normalizeRelative(path.relative(repo, outputAbsolute)) }
287
- : {}),
288
- };
289
- const updateResponseBytes = () => {
290
- for (let index = 0; index < 4; index++) {
291
- const actual = Buffer.byteLength(JSON.stringify(result));
292
- if (actual === result.telemetry.responseBytes)
293
- break;
294
- result.telemetry.responseBytes = actual;
295
- }
296
- };
297
- updateResponseBytes();
298
- while (result.telemetry.responseBytes > hardLimit && files.length > 0) {
299
- files.pop();
300
- result.files.pop();
301
- result.artifact = measuredSerialize();
302
- result.telemetry.serializedBytes = Buffer.byteLength(result.artifact);
303
- result.telemetry.estimatedTokens = Math.ceil(result.telemetry.serializedBytes / 4);
304
- result.telemetry.includedFiles = files.length;
305
- result.telemetry.truncated = true;
306
- updateResponseBytes();
307
- }
308
- if (result.telemetry.responseBytes > hardLimit)
309
- throw new Error("knodin pack: budget is too small for the requested format and telemetry");
310
- if (outputAbsolute) {
311
- fs.mkdirSync(path.dirname(outputAbsolute), { recursive: true });
312
- fs.writeFileSync(outputAbsolute, result.artifact);
313
- }
314
- return result;
315
- }
316
- function safeArtifact(repoPath, artifactPath) {
317
- const repo = fs.realpathSync(repoPath);
318
- const absolute = path.resolve(repo, artifactPath);
319
- if (absolute !== repo && !absolute.startsWith(`${repo}${path.sep}`))
320
- throw new Error("knodin pack: artifactPath must stay inside the repository");
321
- const real = fs.realpathSync(absolute);
322
- if (real !== repo && !real.startsWith(`${repo}${path.sep}`))
323
- throw new Error("knodin pack: artifactPath may not follow a symlink outside the repository");
324
- const stat = fs.statSync(real);
325
- if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
326
- throw new Error("knodin pack: artifactPath must be a packed file no larger than 16 MiB");
327
- return real;
328
- }
329
- const ARTIFACT_CACHE_ENTRY_LIMIT = 2;
330
- const ARTIFACT_CACHE_FILE_LIMIT = 4 * 1024 * 1024;
331
- const artifactLineCache = new Map();
332
- function artifactLines(repoPath, artifactPath) {
333
- const real = safeArtifact(repoPath, artifactPath);
334
- const stat = fs.statSync(real);
335
- const cached = artifactLineCache.get(real);
336
- if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
337
- artifactLineCache.delete(real);
338
- artifactLineCache.set(real, cached);
339
- return cached.lines;
340
- }
341
- if (cached)
342
- artifactLineCache.delete(real);
343
- const lines = fs.readFileSync(real, "utf8").split(/\r?\n/);
344
- if (stat.size <= ARTIFACT_CACHE_FILE_LIMIT) {
345
- while (artifactLineCache.size >= ARTIFACT_CACHE_ENTRY_LIMIT) {
346
- const oldest = artifactLineCache.keys().next().value;
347
- if (oldest === undefined)
348
- break;
349
- artifactLineCache.delete(oldest);
350
- }
351
- artifactLineCache.set(real, { mtimeMs: stat.mtimeMs, size: stat.size, lines });
352
- }
353
- return lines;
354
- }
355
- function truncateUtf8(value, byteLimit) {
356
- if (Buffer.byteLength(value) <= byteLimit)
357
- return value;
358
- let low = 0;
359
- let high = value.length;
360
- while (low < high) {
361
- const midpoint = Math.ceil((low + high) / 2);
362
- if (Buffer.byteLength(value.slice(0, midpoint)) <= byteLimit)
363
- low = midpoint;
364
- else
365
- high = midpoint - 1;
366
- }
367
- return value.slice(0, low);
368
- }
369
- export function readPackedArtifact(repoPath, artifactPath, startLine = 1, endLine = startLine + 199, byteBudget = 16_384) {
370
- if (!Number.isFinite(byteBudget) || !Number.isInteger(byteBudget) || byteBudget < 256)
371
- throw new Error("knodin pack read: byteBudget must be an integer >= 256");
372
- if (!Number.isInteger(startLine) ||
373
- !Number.isInteger(endLine) ||
374
- startLine < 1 ||
375
- endLine < startLine ||
376
- endLine - startLine > 999)
377
- throw new Error("knodin pack read: invalid or unbounded line range");
378
- const lines = measurePerfPhaseSync("artifact_read", () => artifactLines(repoPath, artifactPath).slice(startLine - 1, endLine));
379
- let content = lines.join("\n");
380
- const limit = Math.max(256, Math.min(65_536, Math.floor(byteBudget)));
381
- content = truncateUtf8(content, limit);
382
- return {
383
- artifactPath: normalizeRelative(artifactPath),
384
- startLine,
385
- endLine: startLine + content.split(/\r?\n/).length - 1,
386
- content,
387
- truncated: lines.join("\n") !== content,
388
- };
389
- }
390
- export function grepPackedArtifact(repoPath, artifactPath, pattern, flags = "", limit = 100) {
391
- if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > 1000)
392
- throw new Error("knodin pack grep: limit must be an integer from 1 to 1000");
393
- if (pattern.length === 0 || pattern.length > 1000 || !/^[gimsuy]*$/.test(flags))
394
- throw new Error("knodin pack grep: invalid regex or flags");
395
- let regex;
396
- try {
397
- const safeFlags = flags.replace("g", "").includes("u")
398
- ? flags.replace("g", "")
399
- : `${flags.replace("g", "")}u`;
400
- regex = new RE2(pattern, safeFlags);
401
- }
402
- catch {
403
- throw new Error("knodin pack grep: invalid or unsupported linear-time regex");
404
- }
405
- const matches = [];
406
- measurePerfPhaseSync("artifact_grep", () => {
407
- for (const [index, text] of artifactLines(repoPath, artifactPath).entries()) {
408
- regex.lastIndex = 0;
409
- if (regex.test(text))
410
- matches.push({ line: index + 1, text });
411
- if (matches.length >= Math.max(1, Math.min(1000, Math.floor(limit))))
412
- break;
413
- }
414
- });
415
- return {
416
- artifactPath: normalizeRelative(artifactPath),
417
- pattern,
418
- flags,
419
- matches,
420
- truncated: matches.length >= limit,
421
- };
422
- }
@@ -1,102 +0,0 @@
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_][A-Za-z0-9_]*$/.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
- }