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.
- package/README.md +19 -7
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +2164 -108
- package/dist/bin/launcher.js +25 -3
- package/dist/src/agent-integration.js +304 -0
- package/dist/src/artifact-refresh.js +82 -0
- package/dist/src/cli-args.js +292 -0
- package/dist/src/cli-model.js +384 -0
- package/dist/src/codeflow-replay.js +81 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-constraints.js +21 -0
- package/dist/src/competitive-manifest.js +411 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +487 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +423 -0
- package/dist/src/context.js +102 -0
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1476 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +382 -0
- package/dist/src/engine/ann-hnsw.js +261 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +49 -0
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +14238 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +423 -0
- package/dist/src/engine/source-policy.js +85 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/execution-profile.js +208 -0
- package/dist/src/failure-diagnosis.js +655 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/index-activity.js +126 -0
- package/dist/src/init-progress-worker.js +106 -2
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +1295 -0
- package/dist/src/lifecycle-health.js +282 -0
- package/dist/src/lsp-readonly.js +217 -0
- package/dist/src/mcp-graph-worker.js +69 -0
- package/dist/src/mcp-reliability.js +154 -0
- package/dist/src/mcp-worker-supervisor.js +350 -0
- package/dist/src/mirror.js +290 -0
- package/dist/src/node-runtime.js +157 -0
- package/dist/src/output-compression.js +630 -0
- package/dist/src/output-telemetry.js +368 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/progressive-evidence.js +477 -0
- package/dist/src/pure-compression-cli.js +102 -0
- package/dist/src/relationship-adapters.js +377 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/release-preflight.js +513 -0
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repair-progress-worker.js +120 -2
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +177 -0
- package/dist/src/repository-management.js +1261 -0
- package/dist/src/response-budget.js +196 -0
- package/dist/src/server.js +217 -0
- package/dist/src/structural-fast-path.js +344 -0
- package/dist/src/structural-snapshot.js +37 -0
- package/dist/src/system-config.js +638 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1640 -0
- package/dist/src/update-ceremony.js +162 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +504 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +515 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +234 -0
- package/docs/BEHAVIORAL-CONTRACT.md +72 -0
- package/docs/CLI.md +20 -1
- package/docs/COMPARISON.md +403 -0
- package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +80 -0
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -18
- package/docs/MCP.md +59 -8
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +62 -0
- package/docs/SIGNED-UPDATES.md +151 -0
- package/docs/TELEMETRY.md +46 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/assets/knodin-favicon.svg +4 -0
- package/docs/releases/0.3.0.md +46 -0
- package/docs/releases/0.4.0.md +68 -0
- package/docs/releases/0.4.1.md +28 -0
- package/docs/releases/0.4.2.md +27 -0
- package/docs/releases/0.4.3.md +23 -0
- package/docs/releases/0.5.0.md +29 -0
- package/docs/releases/0.5.1.md +17 -0
- package/docs/releases/0.6.0.md +18 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/docs/releases/0.7.4.md +17 -0
- package/docs/releases/0.7.5.md +20 -0
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +127 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/schemas/support-bundle-v2.schema.json +212 -0
- package/dist/chunks/chunk-DMQAGX77.js +0 -654
- package/dist/chunks/chunk-F4Z3Z766.js +0 -4
- package/dist/chunks/chunk-SIJAQVSX.js +0 -3
- package/dist/chunks/chunk-X6M4HUUE.js +0 -2
- package/dist/chunks/chunk-YPRMY2LP.js +0 -8
- package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
- package/dist/chunks/server-7EDF4CBY.js +0 -14
- package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
- package/docs/releases/0.7.6.md +0 -25
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { compareBytes } from "./compare.js";
|
|
5
|
+
import { resolveStateDir } from "./engine/state-paths.js";
|
|
6
|
+
const LEVELS = new Set(["locate", "outline", "evidence", "expand"]);
|
|
7
|
+
const DEFAULT_BYTES = 65_536;
|
|
8
|
+
const DEFAULT_TOKENS = 16_384;
|
|
9
|
+
const DEFAULT_ITEMS = 100;
|
|
10
|
+
const MIN_PROGRESSIVE_BYTES = 1_600;
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function normalizeFile(repo, file) {
|
|
15
|
+
if (!file || path.isAbsolute(file))
|
|
16
|
+
throw new Error("file must be a repository-relative path");
|
|
17
|
+
const root = fs.realpathSync(repo);
|
|
18
|
+
const lexical = path.resolve(root, file);
|
|
19
|
+
const relative = path.relative(root, lexical).split(path.sep).join("/");
|
|
20
|
+
if (!relative || relative === ".." || relative.startsWith("../"))
|
|
21
|
+
throw new Error("file must be a repository-relative path");
|
|
22
|
+
if (relative === ".knodin" || relative.startsWith(".knodin/"))
|
|
23
|
+
throw new Error("progressive evidence refuses internal knodin state");
|
|
24
|
+
let cursor = root;
|
|
25
|
+
for (const segment of relative.split("/")) {
|
|
26
|
+
cursor = path.join(cursor, segment);
|
|
27
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
28
|
+
throw new Error("progressive evidence refuses symlinked paths");
|
|
29
|
+
}
|
|
30
|
+
const absolute = fs.realpathSync(lexical);
|
|
31
|
+
const realRelative = path.relative(root, absolute);
|
|
32
|
+
if (realRelative === ".." ||
|
|
33
|
+
realRelative.startsWith(`..${path.sep}`) ||
|
|
34
|
+
path.isAbsolute(realRelative))
|
|
35
|
+
throw new Error("file must remain within the repository");
|
|
36
|
+
const stat = fs.lstatSync(absolute);
|
|
37
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
38
|
+
throw new Error("progressive evidence requires a regular repository file");
|
|
39
|
+
return { absolute, relative, root };
|
|
40
|
+
}
|
|
41
|
+
function handleSecret(root) {
|
|
42
|
+
const stateDirectory = resolveStateDir(root);
|
|
43
|
+
if (fs.existsSync(stateDirectory) && fs.lstatSync(stateDirectory).isSymbolicLink())
|
|
44
|
+
throw new Error("progressive evidence refuses a symlinked .knodin directory");
|
|
45
|
+
fs.mkdirSync(stateDirectory, { recursive: true, mode: 0o700 });
|
|
46
|
+
const secretPath = path.join(stateDirectory, "progressive-evidence.key");
|
|
47
|
+
if (!fs.existsSync(secretPath)) {
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(secretPath, randomBytes(32).toString("hex"), { flag: "wx", mode: 0o600 });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== "EEXIST")
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (fs.lstatSync(secretPath).isSymbolicLink())
|
|
57
|
+
throw new Error("progressive evidence refuses a symlinked handle key");
|
|
58
|
+
const secret = fs.readFileSync(secretPath, "utf8");
|
|
59
|
+
if (!/^[a-f0-9]{64}$/.test(secret))
|
|
60
|
+
throw new Error("invalid progressive evidence handle key");
|
|
61
|
+
return { secret: Buffer.from(secret, "hex"), path: secretPath };
|
|
62
|
+
}
|
|
63
|
+
function encodeHandle(payload, secret) {
|
|
64
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
65
|
+
const signature = createHmac("sha256", secret).update(body).digest("hex");
|
|
66
|
+
return `pev1.${body}.${signature}`;
|
|
67
|
+
}
|
|
68
|
+
function decodeHandle(value, secret) {
|
|
69
|
+
if (value.length > 2_048)
|
|
70
|
+
throw new Error("invalid continuation handle");
|
|
71
|
+
const [prefix, body, signature, extra] = value.split(".");
|
|
72
|
+
if (prefix !== "pev1" || !body || !signature || extra !== undefined)
|
|
73
|
+
throw new Error("invalid continuation handle");
|
|
74
|
+
const expected = createHmac("sha256", secret).update(body).digest("hex");
|
|
75
|
+
const actualBuffer = Buffer.from(signature);
|
|
76
|
+
const expectedBuffer = Buffer.from(expected);
|
|
77
|
+
if (actualBuffer.length !== expectedBuffer.length ||
|
|
78
|
+
!timingSafeEqual(actualBuffer, expectedBuffer))
|
|
79
|
+
throw new Error("invalid continuation handle");
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error("invalid continuation handle");
|
|
86
|
+
}
|
|
87
|
+
if (!parsed || typeof parsed !== "object")
|
|
88
|
+
throw new Error("invalid continuation handle");
|
|
89
|
+
const candidate = parsed;
|
|
90
|
+
if (candidate.v !== 1 ||
|
|
91
|
+
typeof candidate.path !== "string" ||
|
|
92
|
+
!/^[a-f0-9]{64}$/.test(candidate.hash ?? "") ||
|
|
93
|
+
!Number.isSafeInteger(candidate.bytes) ||
|
|
94
|
+
(candidate.bytes ?? -1) < 0 ||
|
|
95
|
+
!Number.isSafeInteger(candidate.nextLine) ||
|
|
96
|
+
(candidate.nextLine ?? 0) < 1 ||
|
|
97
|
+
!Number.isSafeInteger(candidate.relatedOffset) ||
|
|
98
|
+
(candidate.relatedOffset ?? -1) < 0 ||
|
|
99
|
+
!Number.isSafeInteger(candidate.outlineOffset) ||
|
|
100
|
+
(candidate.outlineOffset ?? -1) < 0 ||
|
|
101
|
+
!LEVELS.has(candidate.nextLevel))
|
|
102
|
+
throw new Error("invalid continuation handle");
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
function positiveLimit(value, fallback, name) {
|
|
106
|
+
const resolved = value ?? fallback;
|
|
107
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1)
|
|
108
|
+
throw new Error(`${name} must be positive`);
|
|
109
|
+
return resolved;
|
|
110
|
+
}
|
|
111
|
+
function outlineSource(lines) {
|
|
112
|
+
const entries = [];
|
|
113
|
+
const declaration = /^(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+[A-Za-z_$][\w$]*/;
|
|
114
|
+
for (let index = 0; index < lines.length; index++) {
|
|
115
|
+
if (!declaration.test(lines[index] ?? ""))
|
|
116
|
+
continue;
|
|
117
|
+
let depth = 0;
|
|
118
|
+
let sawBrace = false;
|
|
119
|
+
let end = index;
|
|
120
|
+
for (let cursor = index; cursor < lines.length; cursor++) {
|
|
121
|
+
for (const char of lines[cursor] ?? "") {
|
|
122
|
+
if (char === "{") {
|
|
123
|
+
depth++;
|
|
124
|
+
sawBrace = true;
|
|
125
|
+
}
|
|
126
|
+
else if (char === "}")
|
|
127
|
+
depth--;
|
|
128
|
+
}
|
|
129
|
+
end = cursor;
|
|
130
|
+
if (!sawBrace || depth <= 0)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
entries.push({ signature: (lines[index] ?? "").trim(), line: index + 1, endLine: end + 1 });
|
|
134
|
+
}
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
function findRelated(root, target, lines) {
|
|
138
|
+
const names = outlineSource(lines)
|
|
139
|
+
.map(({ signature }) => /\b(?:function|class|interface|type|enum|const|let|var)\s+([\w$]+)/.exec(signature)?.[1])
|
|
140
|
+
.filter((name) => Boolean(name));
|
|
141
|
+
if (names.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const results = [];
|
|
144
|
+
const visit = (directory) => {
|
|
145
|
+
for (const entry of fs
|
|
146
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
147
|
+
// Byte order: this walk order fixes the related-evidence sequence that
|
|
148
|
+
// the integrity-checked paging handle indexes into across calls.
|
|
149
|
+
.sort((a, b) => compareBytes(a.name, b.name))) {
|
|
150
|
+
if ([".git", ".knodin", "node_modules"].includes(entry.name) || entry.isSymbolicLink())
|
|
151
|
+
continue;
|
|
152
|
+
const absolute = path.join(directory, entry.name);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
visit(absolute);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const file = path.relative(root, absolute).split(path.sep).join("/");
|
|
158
|
+
if (!entry.isFile() || file === target || !/\.(?:[cm]?[jt]sx?|py|java|cs|go|rs)$/.test(file))
|
|
159
|
+
continue;
|
|
160
|
+
for (const [index, evidence] of fs.readFileSync(absolute, "utf8").split("\n").entries()) {
|
|
161
|
+
if (!names.some((name) => new RegExp(String.raw `\b${name}\b`).test(evidence)))
|
|
162
|
+
continue;
|
|
163
|
+
results.push({
|
|
164
|
+
file,
|
|
165
|
+
line: index + 1,
|
|
166
|
+
kind: /(?:^|\/)(?:test|tests|__tests__)(?:\/|$)|\.(?:test|spec)\./.test(file)
|
|
167
|
+
? "test-reference"
|
|
168
|
+
: "source-reference",
|
|
169
|
+
evidence,
|
|
170
|
+
confidence: "heuristic",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
visit(root);
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
function omissionPath(request, handle, currentHash, contentBytes, relative) {
|
|
179
|
+
if (handle && handle.path !== relative)
|
|
180
|
+
return "full-renamed-baseline";
|
|
181
|
+
if (handle && (handle.hash !== currentHash || handle.bytes !== contentBytes))
|
|
182
|
+
return "full-stale-baseline";
|
|
183
|
+
if (!request.baselineHash && request.baselineBytes === undefined)
|
|
184
|
+
return "full-missing-baseline";
|
|
185
|
+
if (!request.baselineHash || request.baselineBytes === undefined)
|
|
186
|
+
return "full-incomplete-baseline";
|
|
187
|
+
if (!/^[a-f0-9]{64}$/.test(request.baselineHash))
|
|
188
|
+
return "full-hash-mismatch";
|
|
189
|
+
if (!handle)
|
|
190
|
+
return "full-unverified-baseline";
|
|
191
|
+
if (request.baselineHash !== currentHash || request.baselineBytes !== contentBytes)
|
|
192
|
+
return "full-hash-mismatch";
|
|
193
|
+
return "hash-match-omission";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Produce a deterministic, source-preserving view of one exact repository file.
|
|
197
|
+
* Handles are integrity checked and bind path, content hash, byte length, level,
|
|
198
|
+
* and the next source line; hash omission is allowed only against current disk
|
|
199
|
+
* content and a complete byte-length assertion.
|
|
200
|
+
*/
|
|
201
|
+
export function deliverProgressiveEvidence(request) {
|
|
202
|
+
if (!LEVELS.has(request.level))
|
|
203
|
+
throw new Error("invalid progressive evidence level");
|
|
204
|
+
const { absolute, relative, root } = normalizeFile(request.repo, request.file);
|
|
205
|
+
const handleKey = handleSecret(root);
|
|
206
|
+
const fileStat = fs.statSync(absolute);
|
|
207
|
+
const keyStat = fs.statSync(handleKey.path);
|
|
208
|
+
if (fileStat.dev === keyStat.dev && fileStat.ino === keyStat.ino)
|
|
209
|
+
throw new Error("progressive evidence refuses an alias of internal knodin state");
|
|
210
|
+
const content = fs.readFileSync(absolute, "utf8");
|
|
211
|
+
const contentHash = sha256(content);
|
|
212
|
+
const contentBytes = Buffer.byteLength(content);
|
|
213
|
+
const lines = content.split("\n");
|
|
214
|
+
const secret = handleKey.secret;
|
|
215
|
+
const handle = request.continuation ? decodeHandle(request.continuation, secret) : undefined;
|
|
216
|
+
const handleMatchesCurrentFile = !handle ||
|
|
217
|
+
(handle.path === relative && handle.hash === contentHash && handle.bytes === contentBytes);
|
|
218
|
+
const continuationMismatch = Boolean(handle && !handleMatchesCurrentFile);
|
|
219
|
+
const pagingHandle = handleMatchesCurrentFile ? handle : undefined;
|
|
220
|
+
if (handle && handle.nextLevel !== request.level)
|
|
221
|
+
throw new Error(`continuation requires ${handle.nextLevel}, not ${request.level}`);
|
|
222
|
+
if (pagingHandle &&
|
|
223
|
+
(request.level === "evidence" || request.level === "expand") &&
|
|
224
|
+
request.startLine !== undefined &&
|
|
225
|
+
request.startLine !== pagingHandle.nextLine)
|
|
226
|
+
throw new Error("continuation source range does not match its next line");
|
|
227
|
+
if (handle &&
|
|
228
|
+
(request.level === "locate" || request.level === "outline") &&
|
|
229
|
+
(handle.path !== relative || handle.hash !== contentHash || handle.bytes !== contentBytes))
|
|
230
|
+
throw new Error("stale or renamed continuation");
|
|
231
|
+
const byteLimit = Math.min(positiveLimit(request.byteLimit, DEFAULT_BYTES, "byteLimit"), positiveLimit(request.tokenLimit, DEFAULT_TOKENS, "tokenLimit") * 4);
|
|
232
|
+
const tokenLimit = positiveLimit(request.tokenLimit, DEFAULT_TOKENS, "tokenLimit");
|
|
233
|
+
const itemLimit = positiveLimit(request.itemLimit, DEFAULT_ITEMS, "itemLimit");
|
|
234
|
+
if (byteLimit < MIN_PROGRESSIVE_BYTES)
|
|
235
|
+
throw new Error(`progressive evidence byte/token budget must allow at least ${MIN_PROGRESSIVE_BYTES} bytes`);
|
|
236
|
+
let returned = ["identity", "location"];
|
|
237
|
+
let omitted = ["outline", "source", "related"];
|
|
238
|
+
let reason = "requested-level";
|
|
239
|
+
let more = request.level !== "expand";
|
|
240
|
+
let outline;
|
|
241
|
+
let related;
|
|
242
|
+
let source;
|
|
243
|
+
let sourceRange;
|
|
244
|
+
let nextLine = 1;
|
|
245
|
+
let totalItems = 1;
|
|
246
|
+
let pathTaken = "full";
|
|
247
|
+
let confidence = "exact";
|
|
248
|
+
const outlineOffset = pagingHandle?.outlineOffset ?? 0;
|
|
249
|
+
let outlineTotal = 0;
|
|
250
|
+
if (request.level === "outline") {
|
|
251
|
+
const fullOutline = outlineSource(lines);
|
|
252
|
+
outlineTotal = fullOutline.length;
|
|
253
|
+
outline = fullOutline.slice(outlineOffset, outlineOffset + itemLimit);
|
|
254
|
+
returned = [...returned, "outline"];
|
|
255
|
+
omitted = [
|
|
256
|
+
...(outlineOffset + outline.length < fullOutline.length ? ["outline-tail"] : []),
|
|
257
|
+
"source",
|
|
258
|
+
"related",
|
|
259
|
+
];
|
|
260
|
+
totalItems = fullOutline.length;
|
|
261
|
+
more = true;
|
|
262
|
+
}
|
|
263
|
+
else if (request.level === "evidence" || request.level === "expand") {
|
|
264
|
+
pathTaken = omissionPath(request, handle, contentHash, contentBytes, relative);
|
|
265
|
+
const omitForHash = request.level === "evidence" && pathTaken === "hash-match-omission";
|
|
266
|
+
if (omitForHash) {
|
|
267
|
+
returned = [...returned, "hash-verification"];
|
|
268
|
+
omitted = ["source", "related"];
|
|
269
|
+
reason = "verified-baseline-current";
|
|
270
|
+
more = true;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const requestedStart = continuationMismatch
|
|
274
|
+
? 1
|
|
275
|
+
: (request.startLine ?? pagingHandle?.nextLine ?? 1);
|
|
276
|
+
if (!Number.isSafeInteger(requestedStart) ||
|
|
277
|
+
requestedStart < 1 ||
|
|
278
|
+
requestedStart > lines.length + 1)
|
|
279
|
+
throw new Error("invalid source range");
|
|
280
|
+
if (requestedStart === lines.length + 1 && request.level === "expand") {
|
|
281
|
+
nextLine = requestedStart;
|
|
282
|
+
omitted = [];
|
|
283
|
+
more = false;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const requestedEnd = continuationMismatch
|
|
287
|
+
? lines.length
|
|
288
|
+
: (request.endLine ?? lines.length);
|
|
289
|
+
if (!Number.isSafeInteger(requestedEnd) || requestedEnd < requestedStart)
|
|
290
|
+
throw new Error("invalid source range");
|
|
291
|
+
const last = Math.min(requestedEnd, lines.length, requestedStart + itemLimit - 1);
|
|
292
|
+
sourceRange = { startLine: requestedStart, endLine: last };
|
|
293
|
+
source = lines.slice(requestedStart - 1, last).join("\n");
|
|
294
|
+
returned = [...returned, "source"];
|
|
295
|
+
nextLine = last + 1;
|
|
296
|
+
more = last < lines.length || request.level !== "expand";
|
|
297
|
+
if (last < lines.length)
|
|
298
|
+
omitted = ["source-tail", "related"];
|
|
299
|
+
else if (request.level === "expand")
|
|
300
|
+
omitted = [];
|
|
301
|
+
else
|
|
302
|
+
omitted = ["related"];
|
|
303
|
+
reason = last < lines.length ? "hard-item-budget" : "requested-level";
|
|
304
|
+
}
|
|
305
|
+
totalItems = lines.length;
|
|
306
|
+
}
|
|
307
|
+
if (request.level === "expand") {
|
|
308
|
+
const allRelated = findRelated(root, relative, lines);
|
|
309
|
+
const offset = pagingHandle?.relatedOffset ?? 0;
|
|
310
|
+
const sourceItems = sourceRange ? sourceRange.endLine - sourceRange.startLine + 1 : 0;
|
|
311
|
+
const remainingItems = Math.max(0, itemLimit - sourceItems);
|
|
312
|
+
related = nextLine > lines.length ? allRelated.slice(offset, offset + remainingItems) : [];
|
|
313
|
+
if (related.length > 0) {
|
|
314
|
+
returned = [...returned, "related"];
|
|
315
|
+
confidence = "heuristic";
|
|
316
|
+
}
|
|
317
|
+
if (offset + related.length < allRelated.length) {
|
|
318
|
+
omitted = [...omitted, "related-tail"];
|
|
319
|
+
more = true;
|
|
320
|
+
reason = "hard-item-budget";
|
|
321
|
+
}
|
|
322
|
+
totalItems = lines.length + allRelated.length;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
let nextLevel;
|
|
326
|
+
if (request.level === "locate")
|
|
327
|
+
nextLevel = "outline";
|
|
328
|
+
else if (request.level === "outline")
|
|
329
|
+
nextLevel = outlineOffset + (outline?.length ?? 0) < outlineTotal ? "outline" : "evidence";
|
|
330
|
+
else
|
|
331
|
+
nextLevel = more && nextLine <= lines.length ? request.level : "expand";
|
|
332
|
+
const evidenceHandle = encodeHandle({
|
|
333
|
+
v: 1,
|
|
334
|
+
path: relative,
|
|
335
|
+
hash: contentHash,
|
|
336
|
+
bytes: contentBytes,
|
|
337
|
+
nextLine: 1,
|
|
338
|
+
nextLevel: "evidence",
|
|
339
|
+
outlineOffset: 0,
|
|
340
|
+
relatedOffset: 0,
|
|
341
|
+
}, secret);
|
|
342
|
+
const continuation = more
|
|
343
|
+
? encodeHandle({
|
|
344
|
+
v: 1,
|
|
345
|
+
path: relative,
|
|
346
|
+
hash: contentHash,
|
|
347
|
+
bytes: contentBytes,
|
|
348
|
+
nextLine,
|
|
349
|
+
nextLevel,
|
|
350
|
+
outlineOffset: outlineOffset + (outline?.length ?? 0),
|
|
351
|
+
relatedOffset: (pagingHandle?.relatedOffset ?? 0) + (related?.length ?? 0),
|
|
352
|
+
}, secret)
|
|
353
|
+
: null;
|
|
354
|
+
const response = {
|
|
355
|
+
protocol: "knodin-progressive-evidence-v1",
|
|
356
|
+
level: request.level,
|
|
357
|
+
file: relative,
|
|
358
|
+
identity: `file_${sha256(relative).slice(0, 24)}`,
|
|
359
|
+
contentHash,
|
|
360
|
+
contentBytes,
|
|
361
|
+
returned,
|
|
362
|
+
omitted,
|
|
363
|
+
reason,
|
|
364
|
+
more,
|
|
365
|
+
continuation,
|
|
366
|
+
evidenceHandle,
|
|
367
|
+
freshness: "fresh",
|
|
368
|
+
confidence,
|
|
369
|
+
pathTaken,
|
|
370
|
+
location: { path: relative, lines: lines.length },
|
|
371
|
+
...(outline ? { outline } : {}),
|
|
372
|
+
...(related ? { related } : {}),
|
|
373
|
+
...(source !== undefined ? { source, sourceRange } : {}),
|
|
374
|
+
budget: {
|
|
375
|
+
byteLimit,
|
|
376
|
+
tokenLimit,
|
|
377
|
+
itemLimit,
|
|
378
|
+
serializedBytes: 0,
|
|
379
|
+
estimatedTokens: 0,
|
|
380
|
+
returnedItems: (outline?.length ?? (sourceRange ? sourceRange.endLine - sourceRange.startLine + 1 : 1)) +
|
|
381
|
+
(related?.length ?? 0),
|
|
382
|
+
totalItems,
|
|
383
|
+
truncated: omitted.length > 0,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
const refreshBudget = () => {
|
|
387
|
+
for (let iteration = 0; iteration < 4; iteration++) {
|
|
388
|
+
response.budget.serializedBytes = Buffer.byteLength(JSON.stringify(response));
|
|
389
|
+
response.budget.estimatedTokens = Math.ceil(response.budget.serializedBytes / 4);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
refreshBudget();
|
|
393
|
+
while (response.budget.serializedBytes > byteLimit && response.related?.length) {
|
|
394
|
+
response.related.pop();
|
|
395
|
+
response.more = true;
|
|
396
|
+
response.reason = "hard-byte-budget";
|
|
397
|
+
if (!response.omitted.includes("related-tail"))
|
|
398
|
+
response.omitted.push("related-tail");
|
|
399
|
+
if (response.related.length === 0)
|
|
400
|
+
response.returned = response.returned.filter((v) => v !== "related");
|
|
401
|
+
response.continuation = encodeHandle({
|
|
402
|
+
v: 1,
|
|
403
|
+
path: relative,
|
|
404
|
+
hash: contentHash,
|
|
405
|
+
bytes: contentBytes,
|
|
406
|
+
nextLine,
|
|
407
|
+
nextLevel: "expand",
|
|
408
|
+
outlineOffset: 0,
|
|
409
|
+
relatedOffset: (pagingHandle?.relatedOffset ?? 0) + response.related.length,
|
|
410
|
+
}, secret);
|
|
411
|
+
response.budget.returnedItems--;
|
|
412
|
+
response.budget.truncated = true;
|
|
413
|
+
refreshBudget();
|
|
414
|
+
}
|
|
415
|
+
while (response.budget.serializedBytes > byteLimit && response.outline?.length) {
|
|
416
|
+
response.outline.pop();
|
|
417
|
+
response.more = true;
|
|
418
|
+
response.reason = "hard-byte-budget";
|
|
419
|
+
if (!response.omitted.includes("outline-tail"))
|
|
420
|
+
response.omitted.unshift("outline-tail");
|
|
421
|
+
response.continuation = encodeHandle({
|
|
422
|
+
v: 1,
|
|
423
|
+
path: relative,
|
|
424
|
+
hash: contentHash,
|
|
425
|
+
bytes: contentBytes,
|
|
426
|
+
nextLine: 1,
|
|
427
|
+
nextLevel: "outline",
|
|
428
|
+
outlineOffset: outlineOffset + response.outline.length,
|
|
429
|
+
relatedOffset: 0,
|
|
430
|
+
}, secret);
|
|
431
|
+
response.budget.returnedItems--;
|
|
432
|
+
response.budget.truncated = true;
|
|
433
|
+
refreshBudget();
|
|
434
|
+
}
|
|
435
|
+
while (response.budget.serializedBytes > byteLimit &&
|
|
436
|
+
response.sourceRange &&
|
|
437
|
+
response.sourceRange.endLine >= response.sourceRange.startLine) {
|
|
438
|
+
response.sourceRange.endLine--;
|
|
439
|
+
if (response.sourceRange.endLine < response.sourceRange.startLine) {
|
|
440
|
+
delete response.source;
|
|
441
|
+
delete response.sourceRange;
|
|
442
|
+
response.returned = response.returned.filter((value) => value !== "source");
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
response.source = lines
|
|
446
|
+
.slice(response.sourceRange.startLine - 1, response.sourceRange.endLine)
|
|
447
|
+
.join("\n");
|
|
448
|
+
}
|
|
449
|
+
response.more = true;
|
|
450
|
+
response.reason = "hard-byte-budget";
|
|
451
|
+
if (!response.omitted.includes("source-tail"))
|
|
452
|
+
response.omitted.unshift("source-tail");
|
|
453
|
+
let resumeLine;
|
|
454
|
+
if (response.sourceRange?.endLine)
|
|
455
|
+
resumeLine = response.sourceRange.endLine + 1;
|
|
456
|
+
else if (continuationMismatch)
|
|
457
|
+
resumeLine = 1;
|
|
458
|
+
else
|
|
459
|
+
resumeLine = request.startLine ?? pagingHandle?.nextLine ?? 1;
|
|
460
|
+
response.continuation = encodeHandle({
|
|
461
|
+
v: 1,
|
|
462
|
+
path: relative,
|
|
463
|
+
hash: contentHash,
|
|
464
|
+
bytes: contentBytes,
|
|
465
|
+
nextLine: resumeLine,
|
|
466
|
+
nextLevel: request.level,
|
|
467
|
+
outlineOffset: 0,
|
|
468
|
+
relatedOffset: pagingHandle?.relatedOffset ?? 0,
|
|
469
|
+
}, secret);
|
|
470
|
+
response.budget.returnedItems = Math.max(0, response.budget.returnedItems - 1);
|
|
471
|
+
response.budget.truncated = true;
|
|
472
|
+
refreshBudget();
|
|
473
|
+
}
|
|
474
|
+
if (response.budget.serializedBytes > byteLimit)
|
|
475
|
+
throw new RangeError("progressive evidence response exceeds the hard byte/token budget");
|
|
476
|
+
return response;
|
|
477
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compressOutput, compressOutputFile, } from "./output-compression.js";
|
|
4
|
+
function valueAfter(argv, flag) {
|
|
5
|
+
const index = argv.indexOf(flag);
|
|
6
|
+
return index >= 0
|
|
7
|
+
? argv[index + 1]
|
|
8
|
+
: argv.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
|
|
9
|
+
}
|
|
10
|
+
function boundedInteger(argv, flag, fallback, minimum, maximum) {
|
|
11
|
+
const raw = valueAfter(argv, flag);
|
|
12
|
+
const value = raw === undefined ? fallback : Number(raw);
|
|
13
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum)
|
|
14
|
+
throw new Error(`knodin compress: ${flag} must be an integer from ${minimum} to ${maximum}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function readBoundedStdin(maxBytes) {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
for await (const chunk of process.stdin) {
|
|
21
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
22
|
+
bytes += buffer.byteLength;
|
|
23
|
+
if (bytes > maxBytes)
|
|
24
|
+
throw new Error(`knodin compress: input exceeded bounded retention limit of ${maxBytes} bytes`);
|
|
25
|
+
chunks.push(buffer);
|
|
26
|
+
}
|
|
27
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
28
|
+
}
|
|
29
|
+
function human(result) {
|
|
30
|
+
const omitted = result.omittedRanges.reduce((total, range) => total + range.lineCount, 0);
|
|
31
|
+
let fidelity = `Compressed output; ${omitted} line(s) omitted.`;
|
|
32
|
+
if (result.status === "insufficient-budget")
|
|
33
|
+
fidelity = `INSUFFICIENT BUDGET: ${result.fidelity.unpreservedSignals.length} detected signal line(s) are available only through retained drill-down.`;
|
|
34
|
+
else if (result.complete)
|
|
35
|
+
fidelity = "Complete output; nothing omitted.";
|
|
36
|
+
const artifact = result.artifact.retained
|
|
37
|
+
? ` Retained artifact: ${result.artifact.id} (${result.artifact.path}).`
|
|
38
|
+
: " Raw retention disabled; omitted regions cannot be retrieved.";
|
|
39
|
+
const body = result.content ? `${result.content}\n` : "";
|
|
40
|
+
return `${body}---\n${fidelity} ${result.output.lines}/${result.input.lines} lines, ${result.output.bytes}/${result.input.bytes} bytes; exit=${result.exit.code ?? "unknown"}, signal=${result.exit.signal ?? "none"}.${artifact}\n`;
|
|
41
|
+
}
|
|
42
|
+
/** Handle only pure compression creation. Artifact reads/diagnosis retain the full CLI contract. */
|
|
43
|
+
export async function tryPureCompressionFastPath(argv, cwd) {
|
|
44
|
+
if (argv[0] !== "compress")
|
|
45
|
+
return false;
|
|
46
|
+
const valueFlags = new Set([
|
|
47
|
+
"--strategy",
|
|
48
|
+
"--adapter",
|
|
49
|
+
"--lines",
|
|
50
|
+
"--max-output-bytes",
|
|
51
|
+
"--context",
|
|
52
|
+
"--exit-code",
|
|
53
|
+
"--signal",
|
|
54
|
+
"--max-input-bytes",
|
|
55
|
+
"--repo",
|
|
56
|
+
]);
|
|
57
|
+
const rest = argv.slice(1);
|
|
58
|
+
const positionals = rest.filter((argument, index) => !argument.startsWith("--") && !valueFlags.has(rest[index - 1]));
|
|
59
|
+
if (["read", "diagnose", "delete"].includes(positionals[0] ?? ""))
|
|
60
|
+
return false;
|
|
61
|
+
const strategy = valueAfter(argv, "--strategy") ?? "smart";
|
|
62
|
+
if (!["smart", "head-tail", "errors-only"].includes(strategy))
|
|
63
|
+
return false;
|
|
64
|
+
const adapter = valueAfter(argv, "--adapter") ?? "auto";
|
|
65
|
+
if (![
|
|
66
|
+
"auto",
|
|
67
|
+
"generic",
|
|
68
|
+
"vitest",
|
|
69
|
+
"jest",
|
|
70
|
+
"pytest",
|
|
71
|
+
"go-test",
|
|
72
|
+
"maven",
|
|
73
|
+
"gradle",
|
|
74
|
+
"dotnet",
|
|
75
|
+
"cargo",
|
|
76
|
+
].includes(adapter))
|
|
77
|
+
return false;
|
|
78
|
+
const repo = path.resolve(valueAfter(argv, "--repo") ?? cwd ?? process.cwd());
|
|
79
|
+
const maxInputBytes = boundedInteger(argv, "--max-input-bytes", 16 * 1024 * 1024, 1, 64 * 1024 * 1024);
|
|
80
|
+
const request = {
|
|
81
|
+
exitCode: valueAfter(argv, "--exit-code")
|
|
82
|
+
? boundedInteger(argv, "--exit-code", 0, 0, 255)
|
|
83
|
+
: undefined,
|
|
84
|
+
signal: valueAfter(argv, "--signal"),
|
|
85
|
+
strategy: strategy,
|
|
86
|
+
adapter: adapter,
|
|
87
|
+
lineBudget: boundedInteger(argv, "--lines", 200, 1, 10_000),
|
|
88
|
+
byteBudget: boundedInteger(argv, "--max-output-bytes", 16_384, 256, 4 * 1024 * 1024),
|
|
89
|
+
contextLines: boundedInteger(argv, "--context", 1, 0, 10),
|
|
90
|
+
maxInputBytes,
|
|
91
|
+
retain: !argv.includes("--no-retain"),
|
|
92
|
+
redactSecrets: !argv.includes("--no-redact"),
|
|
93
|
+
};
|
|
94
|
+
const input = positionals[0] === "create" ? (positionals[1] ?? "-") : (positionals[0] ?? "-");
|
|
95
|
+
if (input !== "-" && !fs.existsSync(path.resolve(repo, input)))
|
|
96
|
+
return false;
|
|
97
|
+
const result = input === "-"
|
|
98
|
+
? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
|
|
99
|
+
: compressOutputFile(repo, input, request);
|
|
100
|
+
process.stdout.write(argv.includes("--json") ? `${JSON.stringify(result)}\n` : human(result));
|
|
101
|
+
return true;
|
|
102
|
+
}
|