@levnikolaevich/hex-line-mcp 1.3.3 → 1.3.5
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/dist/hook.mjs +428 -0
- package/dist/server.mjs +2645 -0
- package/package.json +8 -8
- package/benchmark/atomic.mjs +0 -502
- package/benchmark/graph.mjs +0 -80
- package/benchmark/index.mjs +0 -144
- package/benchmark/workflows.mjs +0 -350
- package/hook.mjs +0 -466
- package/lib/benchmark-helpers.mjs +0 -541
- package/lib/bulk-replace.mjs +0 -65
- package/lib/changes.mjs +0 -176
- package/lib/coerce.mjs +0 -1
- package/lib/edit.mjs +0 -534
- package/lib/format.mjs +0 -138
- package/lib/graph-enrich.mjs +0 -226
- package/lib/hash.mjs +0 -1
- package/lib/info.mjs +0 -91
- package/lib/normalize.mjs +0 -1
- package/lib/outline.mjs +0 -145
- package/lib/read.mjs +0 -138
- package/lib/revisions.mjs +0 -238
- package/lib/search.mjs +0 -268
- package/lib/security.mjs +0 -112
- package/lib/setup.mjs +0 -275
- package/lib/tree.mjs +0 -236
- package/lib/update-check.mjs +0 -1
- package/lib/verify.mjs +0 -70
- package/server.mjs +0 -375
package/package.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
|
|
7
|
-
"main": "server.mjs",
|
|
7
|
+
"main": "dist/server.mjs",
|
|
8
8
|
"bin": {
|
|
9
|
-
"hex-line-mcp": "server.mjs"
|
|
9
|
+
"hex-line-mcp": "dist/server.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"hook.mjs",
|
|
14
|
-
"benchmark/",
|
|
15
|
-
"lib/",
|
|
12
|
+
"dist/",
|
|
16
13
|
"README.md",
|
|
17
14
|
"output-style.md"
|
|
18
15
|
],
|
|
19
16
|
"scripts": {
|
|
20
17
|
"start": "node server.mjs",
|
|
18
|
+
"build": "node build.mjs",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
21
20
|
"lint": "eslint .",
|
|
22
21
|
"lint:fix": "eslint . --fix",
|
|
23
22
|
"test": "node --test test/*.mjs",
|
|
@@ -32,8 +31,9 @@
|
|
|
32
31
|
"better-sqlite3": "Optional. Used only by lib/graph-enrich.mjs for readonly access to hex-graph .codegraph/index.db. Graceful fallback if absent."
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
|
-
"@levnikolaevich/hex-common": "file:../hex-common",
|
|
36
34
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
35
|
+
"tree-sitter-wasms": "^0.1.0",
|
|
36
|
+
"web-tree-sitter": "^0.25.0",
|
|
37
37
|
"diff": "^8.0.3",
|
|
38
38
|
"ignore": "^7.0.5",
|
|
39
39
|
"zod": "^3.25.0",
|
package/benchmark/atomic.mjs
DELETED
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TEST 1-15: Individual tool comparisons (atomic benchmarks).
|
|
3
|
-
*
|
|
4
|
-
* Each test compares "agent without hex-line" vs "agent with hex-line"
|
|
5
|
-
* for a single tool or error-recovery scenario.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, unlinkSync, readdirSync } from "node:fs";
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { performance } from "node:perf_hooks";
|
|
12
|
-
import { fnv1a, lineTag, rangeChecksum } from "../lib/hash.mjs";
|
|
13
|
-
import { readFile } from "../lib/read.mjs";
|
|
14
|
-
import { directoryTree } from "../lib/tree.mjs";
|
|
15
|
-
import { fileInfo } from "../lib/info.mjs";
|
|
16
|
-
import { verifyChecksums } from "../lib/verify.mjs";
|
|
17
|
-
import { fileChanges } from "../lib/changes.mjs";
|
|
18
|
-
import {
|
|
19
|
-
getFileLines,
|
|
20
|
-
simBuiltInReadFull, simBuiltInOutlineFull, simBuiltInGrep,
|
|
21
|
-
simBuiltInLsR, simBuiltInStat, simBuiltInWrite, simBuiltInEdit, simBuiltInVerify,
|
|
22
|
-
simHexLineOutlinePlusRead, simHexLineGrep, simHexLineWrite, simHexLineEditDiff,
|
|
23
|
-
runN, pctSavings,
|
|
24
|
-
} from "../lib/benchmark-helpers.mjs";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Run TEST 1-15 atomic benchmarks.
|
|
28
|
-
*
|
|
29
|
-
* @param {object} config
|
|
30
|
-
* @param {string[]} config.allFiles - All discovered code files
|
|
31
|
-
* @param {object} config.cats - Categorized files { small, medium, large, xl }
|
|
32
|
-
* @param {string} config.tmpPath - Path to temp benchmark file
|
|
33
|
-
* @param {string} config.tmpContent - Content of temp file
|
|
34
|
-
* @param {string[]} config.tmpLines - Lines of temp file
|
|
35
|
-
* @param {string} config.repoRoot - Repository root path
|
|
36
|
-
* @param {number} config.ts - Timestamp for unique temp file names
|
|
37
|
-
* @returns {Promise<object[]>} Array of result objects
|
|
38
|
-
*/
|
|
39
|
-
export async function runAtomic(config) {
|
|
40
|
-
const { allFiles, cats, tmpPath, tmpContent, tmpLines, repoRoot, ts } = config;
|
|
41
|
-
const results = [];
|
|
42
|
-
|
|
43
|
-
// ===================================================================
|
|
44
|
-
// TEST 1: Read full file
|
|
45
|
-
// ===================================================================
|
|
46
|
-
for (const [cat, files] of Object.entries(cats)) {
|
|
47
|
-
if (files.length === 0) continue;
|
|
48
|
-
const withoutArr = [];
|
|
49
|
-
const withArr = [];
|
|
50
|
-
|
|
51
|
-
for (const f of files) {
|
|
52
|
-
const lines = getFileLines(f);
|
|
53
|
-
if (!lines) continue;
|
|
54
|
-
withoutArr.push(runN(() => simBuiltInReadFull(f, lines).length));
|
|
55
|
-
withArr.push(runN(() => readFile(f).length));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (withoutArr.length === 0) continue;
|
|
59
|
-
const avgWithout = Math.round(withoutArr.reduce((a, b) => a + b.value, 0) / withoutArr.length);
|
|
60
|
-
const avgWith = Math.round(withArr.reduce((a, b) => a + b.value, 0) / withArr.length);
|
|
61
|
-
const avgMsWithout = parseFloat((withoutArr.reduce((a, b) => a + b.ms, 0) / withoutArr.length).toFixed(1));
|
|
62
|
-
const avgMsWith = parseFloat((withArr.reduce((a, b) => a + b.ms, 0) / withArr.length).toFixed(1));
|
|
63
|
-
|
|
64
|
-
const label = { small: "<50L", medium: "50-200L", large: "200-500L", xl: "500L+" }[cat];
|
|
65
|
-
results.push({
|
|
66
|
-
num: 1, scenario: `Read full (${label})`,
|
|
67
|
-
without: avgWithout, withSL: avgWith,
|
|
68
|
-
savings: pctSavings(avgWithout, avgWith),
|
|
69
|
-
latencyWithout: avgMsWithout, latencyWith: avgMsWith,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ===================================================================
|
|
74
|
-
// TEST 2: Read with outline — full read vs outline + targeted read
|
|
75
|
-
// ===================================================================
|
|
76
|
-
for (const cat of ["large", "xl"]) {
|
|
77
|
-
const files = cats[cat] || [];
|
|
78
|
-
if (files.length === 0) continue;
|
|
79
|
-
const withoutArr = [];
|
|
80
|
-
const withArr = [];
|
|
81
|
-
|
|
82
|
-
for (const f of files) {
|
|
83
|
-
const lines = getFileLines(f);
|
|
84
|
-
if (!lines) continue;
|
|
85
|
-
withoutArr.push(runN(() => simBuiltInOutlineFull(f, lines).length));
|
|
86
|
-
withArr.push(runN(() => simHexLineOutlinePlusRead(f, lines).length));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (withoutArr.length === 0) continue;
|
|
90
|
-
const avgWithout = Math.round(withoutArr.reduce((a, b) => a + b.value, 0) / withoutArr.length);
|
|
91
|
-
const avgWith = Math.round(withArr.reduce((a, b) => a + b.value, 0) / withArr.length);
|
|
92
|
-
const avgMsWithout = parseFloat((withoutArr.reduce((a, b) => a + b.ms, 0) / withoutArr.length).toFixed(1));
|
|
93
|
-
const avgMsWith = parseFloat((withArr.reduce((a, b) => a + b.ms, 0) / withArr.length).toFixed(1));
|
|
94
|
-
|
|
95
|
-
const label = cat === "large" ? "200-500L" : "500L+";
|
|
96
|
-
results.push({
|
|
97
|
-
num: 2, scenario: `Outline+read (${label})`,
|
|
98
|
-
without: avgWithout, withSL: avgWith,
|
|
99
|
-
savings: pctSavings(avgWithout, avgWith),
|
|
100
|
-
latencyWithout: avgMsWithout, latencyWith: avgMsWith,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ===================================================================
|
|
105
|
-
// TEST 3: Grep search
|
|
106
|
-
// ===================================================================
|
|
107
|
-
{
|
|
108
|
-
const grepFiles = [...(cats.medium || []), ...(cats.large || []), ...(cats.xl || [])].slice(0, 3);
|
|
109
|
-
if (grepFiles.length > 0) {
|
|
110
|
-
const withoutArr = [];
|
|
111
|
-
const withArr = [];
|
|
112
|
-
|
|
113
|
-
for (const f of grepFiles) {
|
|
114
|
-
const lines = getFileLines(f);
|
|
115
|
-
if (!lines) continue;
|
|
116
|
-
const pattern = "function|class|const";
|
|
117
|
-
withoutArr.push(runN(() => simBuiltInGrep(pattern, f).length));
|
|
118
|
-
withArr.push(runN(() => simHexLineGrep(f, lines, pattern).length));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (withoutArr.length > 0) {
|
|
122
|
-
const avgWithout = Math.round(withoutArr.reduce((a, b) => a + b.value, 0) / withoutArr.length);
|
|
123
|
-
const avgWith = Math.round(withArr.reduce((a, b) => a + b.value, 0) / withArr.length);
|
|
124
|
-
const avgMsWithout = parseFloat((withoutArr.reduce((a, b) => a + b.ms, 0) / withoutArr.length).toFixed(1));
|
|
125
|
-
const avgMsWith = parseFloat((withArr.reduce((a, b) => a + b.ms, 0) / withArr.length).toFixed(1));
|
|
126
|
-
results.push({
|
|
127
|
-
num: 3, scenario: "Grep search",
|
|
128
|
-
without: avgWithout, withSL: avgWith,
|
|
129
|
-
savings: pctSavings(avgWithout, avgWith),
|
|
130
|
-
latencyWithout: avgMsWithout, latencyWith: avgMsWith,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ===================================================================
|
|
137
|
-
// TEST 4: Directory tree
|
|
138
|
-
// ===================================================================
|
|
139
|
-
{
|
|
140
|
-
const { value: without, ms: withoutMs } = runN(() => simBuiltInLsR(repoRoot, 0, 3).length);
|
|
141
|
-
const { value: withSL, ms: withMs } = runN(() => directoryTree(repoRoot, { max_depth: 3 }).length);
|
|
142
|
-
results.push({
|
|
143
|
-
num: 4, scenario: "Directory tree",
|
|
144
|
-
without, withSL,
|
|
145
|
-
savings: pctSavings(without, withSL),
|
|
146
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ===================================================================
|
|
151
|
-
// TEST 5: File info
|
|
152
|
-
// ===================================================================
|
|
153
|
-
{
|
|
154
|
-
const infoFile = allFiles[Math.floor(allFiles.length / 2)] || allFiles[0];
|
|
155
|
-
const { value: without, ms: withoutMs } = runN(() => simBuiltInStat(infoFile).length);
|
|
156
|
-
const { value: withSL, ms: withMs } = runN(() => fileInfo(infoFile).length);
|
|
157
|
-
results.push({
|
|
158
|
-
num: 5, scenario: "File info",
|
|
159
|
-
without, withSL,
|
|
160
|
-
savings: pctSavings(without, withSL),
|
|
161
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ===================================================================
|
|
166
|
-
// TEST 6: Create file (write)
|
|
167
|
-
// ===================================================================
|
|
168
|
-
{
|
|
169
|
-
const { value: without, ms: withoutMs } = runN(() => simBuiltInWrite(tmpPath, tmpContent).length);
|
|
170
|
-
const { value: withSL, ms: withMs } = runN(() => simHexLineWrite(tmpPath, tmpContent).length);
|
|
171
|
-
results.push({
|
|
172
|
-
num: 6, scenario: "Create file (200L)",
|
|
173
|
-
without, withSL,
|
|
174
|
-
savings: pctSavings(without, withSL),
|
|
175
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ===================================================================
|
|
180
|
-
// TEST 7: Edit x5 sequential
|
|
181
|
-
// ===================================================================
|
|
182
|
-
{
|
|
183
|
-
const editTargets = [
|
|
184
|
-
{ line: 13, new: ' this.configPath = resolve(configPath || ".");' },
|
|
185
|
-
{ line: 55, new: " const { retries = MAX_RETRIES, delay = 200, backoff = 3 } = options;" },
|
|
186
|
-
{ line: 75, new: " this.timeout = options.timeout ?? DEFAULT_TIMEOUT;" },
|
|
187
|
-
{ line: 116, new: " return this; // chainable" },
|
|
188
|
-
{ line: 148, new: " /** @type {string[]} */\n const errors = [];" },
|
|
189
|
-
];
|
|
190
|
-
|
|
191
|
-
let totalWithout = 0;
|
|
192
|
-
let totalWith = 0;
|
|
193
|
-
let totalMsWithout = 0;
|
|
194
|
-
let totalMsWith = 0;
|
|
195
|
-
|
|
196
|
-
for (const edit of editTargets) {
|
|
197
|
-
const origLines = [...tmpLines];
|
|
198
|
-
const newLines = [...tmpLines];
|
|
199
|
-
const idx = edit.line - 1;
|
|
200
|
-
if (idx < newLines.length) {
|
|
201
|
-
newLines[idx] = edit.new;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const rW = runN(() => simBuiltInEdit(tmpPath, origLines, newLines).length);
|
|
205
|
-
const rH = runN(() => simHexLineEditDiff(origLines, newLines).length);
|
|
206
|
-
totalWithout += rW.value;
|
|
207
|
-
totalWith += rH.value;
|
|
208
|
-
totalMsWithout += rW.ms;
|
|
209
|
-
totalMsWith += rH.ms;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
results.push({
|
|
213
|
-
num: 7, scenario: "Edit x5 sequential",
|
|
214
|
-
without: totalWithout, withSL: totalWith,
|
|
215
|
-
savings: pctSavings(totalWithout, totalWith),
|
|
216
|
-
latencyWithout: parseFloat(totalMsWithout.toFixed(1)), latencyWith: parseFloat(totalMsWith.toFixed(1)),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ===================================================================
|
|
221
|
-
// TEST 8: Verify checksums
|
|
222
|
-
// ===================================================================
|
|
223
|
-
{
|
|
224
|
-
const fileLines = readFileSync(tmpPath, "utf-8").replace(/\r\n/g, "\n").split("\n");
|
|
225
|
-
const hashes = fileLines.map(l => fnv1a(l));
|
|
226
|
-
const cs1 = rangeChecksum(hashes.slice(0, 50), 1, 50);
|
|
227
|
-
const cs2 = rangeChecksum(hashes.slice(50, 100), 51, 100);
|
|
228
|
-
const cs3 = rangeChecksum(hashes.slice(100, 150), 101, 150);
|
|
229
|
-
const cs4 = rangeChecksum(hashes.slice(150, 200), 151, 200);
|
|
230
|
-
const checksums = [cs1, cs2, cs3, cs4];
|
|
231
|
-
|
|
232
|
-
const { value: without, ms: withoutMs } = runN(() => simBuiltInVerify(tmpPath, fileLines).length);
|
|
233
|
-
const { value: withSL, ms: withMs } = runN(() => verifyChecksums(tmpPath, checksums).length);
|
|
234
|
-
|
|
235
|
-
results.push({
|
|
236
|
-
num: 8, scenario: "Verify checksums (4 ranges)",
|
|
237
|
-
without, withSL,
|
|
238
|
-
savings: pctSavings(without, withSL),
|
|
239
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ===================================================================
|
|
244
|
-
// TEST 9: Multi-file read (batch)
|
|
245
|
-
// ===================================================================
|
|
246
|
-
{
|
|
247
|
-
const batchFiles = (cats.small || []).slice(0, 3);
|
|
248
|
-
if (batchFiles.length >= 2) {
|
|
249
|
-
// Without hex-line: N separate Read calls
|
|
250
|
-
const { value: without, ms: withoutMs } = runN(() => {
|
|
251
|
-
let total = 0;
|
|
252
|
-
for (const f of batchFiles) {
|
|
253
|
-
const lines = getFileLines(f);
|
|
254
|
-
if (lines) total += simBuiltInReadFull(f, lines).length;
|
|
255
|
-
}
|
|
256
|
-
return total;
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// With hex-line: 1 read_file call with paths:[] — concatenated output
|
|
260
|
-
const { value: withSL, ms: withMs } = runN(() => {
|
|
261
|
-
const parts = [];
|
|
262
|
-
for (const f of batchFiles) {
|
|
263
|
-
parts.push(readFile(f));
|
|
264
|
-
}
|
|
265
|
-
return parts.join("\n\n---\n\n").length;
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
results.push({
|
|
269
|
-
num: 9, scenario: `Multi-file read (${batchFiles.length} files)`,
|
|
270
|
-
without, withSL,
|
|
271
|
-
savings: pctSavings(without, withSL),
|
|
272
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ===================================================================
|
|
278
|
-
// TEST 10: bulk_replace dry_run
|
|
279
|
-
// ===================================================================
|
|
280
|
-
{
|
|
281
|
-
const bulkTmpPaths = [];
|
|
282
|
-
for (let i = 0; i < 5; i++) {
|
|
283
|
-
const p = resolve(tmpdir(), `hex-line-bulk-${ts}-${i}.js`);
|
|
284
|
-
writeFileSync(p, tmpContent, "utf-8");
|
|
285
|
-
bulkTmpPaths.push(p);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const editLine = 13;
|
|
289
|
-
const editNew = ' this.configPath = resolve(configPath || ".");';
|
|
290
|
-
|
|
291
|
-
// Without hex-line: 5 separate edit_file calls
|
|
292
|
-
const { value: without, ms: withoutMs } = runN(() => {
|
|
293
|
-
let total = 0;
|
|
294
|
-
for (const p of bulkTmpPaths) {
|
|
295
|
-
const origLines = [...tmpLines];
|
|
296
|
-
const newLines = [...tmpLines];
|
|
297
|
-
newLines[editLine - 1] = editNew;
|
|
298
|
-
total += simBuiltInEdit(p, origLines, newLines).length;
|
|
299
|
-
}
|
|
300
|
-
return total;
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// With hex-line: 1 bulk_replace — summary + per-file compact diff
|
|
304
|
-
const { value: withSL, ms: withMs } = runN(() => {
|
|
305
|
-
let response = "5 files changed, 0 errors\n";
|
|
306
|
-
for (let i = 0; i < bulkTmpPaths.length; i++) {
|
|
307
|
-
const origLines = [...tmpLines];
|
|
308
|
-
const newLines = [...tmpLines];
|
|
309
|
-
newLines[editLine - 1] = editNew;
|
|
310
|
-
response += simHexLineEditDiff(origLines, newLines) + "\n";
|
|
311
|
-
}
|
|
312
|
-
return response.length;
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
results.push({
|
|
316
|
-
num: 10, scenario: "bulk_replace dry_run (5 files)",
|
|
317
|
-
without, withSL,
|
|
318
|
-
savings: pctSavings(without, withSL),
|
|
319
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
for (const p of bulkTmpPaths) {
|
|
323
|
-
try { unlinkSync(p); } catch { /* ok */ }
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ===================================================================
|
|
328
|
-
// TEST 11: changes (semantic diff)
|
|
329
|
-
// ===================================================================
|
|
330
|
-
{
|
|
331
|
-
// Without hex-line: raw unified diff output
|
|
332
|
-
const { value: without, ms: withoutMs } = runN(() => {
|
|
333
|
-
const diffLines = [
|
|
334
|
-
`diff --git a/benchmark-target.js b/benchmark-target.js`,
|
|
335
|
-
`index abc1234..def5678 100644`,
|
|
336
|
-
`--- a/benchmark-target.js`,
|
|
337
|
-
`+++ b/benchmark-target.js`,
|
|
338
|
-
`@@ -10,6 +10,12 @@ const DEFAULT_TIMEOUT = 5000;`,
|
|
339
|
-
];
|
|
340
|
-
// Simulate ~15 context + change lines typical of a small diff
|
|
341
|
-
for (let i = 0; i < 5; i++) {
|
|
342
|
-
diffLines.push(` ${tmpLines[i + 5] || " // context line"}`); // context
|
|
343
|
-
}
|
|
344
|
-
diffLines.push(`-${tmpLines[12] || " old line"}`);
|
|
345
|
-
diffLines.push(`+ this.configPath = resolve(configPath || ".");`);
|
|
346
|
-
for (let i = 0; i < 5; i++) {
|
|
347
|
-
diffLines.push(` ${tmpLines[i + 14] || " // context line"}`); // context
|
|
348
|
-
}
|
|
349
|
-
// Second hunk — added function
|
|
350
|
-
diffLines.push(`@@ -195,0 +201,8 @@`);
|
|
351
|
-
for (let i = 0; i < 3; i++) {
|
|
352
|
-
diffLines.push(` ${tmpLines[i + 150] || " // context"}`);
|
|
353
|
-
}
|
|
354
|
-
for (let i = 0; i < 5; i++) {
|
|
355
|
-
diffLines.push(`+ // new function line ${i}`);
|
|
356
|
-
}
|
|
357
|
-
for (let i = 0; i < 3; i++) {
|
|
358
|
-
diffLines.push(` ${tmpLines[i + 155] || " // context"}`);
|
|
359
|
-
}
|
|
360
|
-
return diffLines.join("\n").length;
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// With hex-line: real fileChanges() semantic diff (async, called once — deterministic)
|
|
364
|
-
let withSL;
|
|
365
|
-
let withMs = 0;
|
|
366
|
-
try {
|
|
367
|
-
const t0 = performance.now();
|
|
368
|
-
const changesOut = await fileChanges(allFiles[0]);
|
|
369
|
-
withMs = parseFloat((performance.now() - t0).toFixed(1));
|
|
370
|
-
withSL = changesOut.length;
|
|
371
|
-
} catch {
|
|
372
|
-
withSL = 133; // fallback if no git history
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
results.push({
|
|
376
|
-
num: 11, scenario: "Changes (semantic diff)",
|
|
377
|
-
without, withSL,
|
|
378
|
-
savings: pctSavings(without, withSL),
|
|
379
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ===================================================================
|
|
384
|
-
// TEST 12: FILE_NOT_FOUND recovery
|
|
385
|
-
// ===================================================================
|
|
386
|
-
{
|
|
387
|
-
const missingPath = resolve(repoRoot, "src/utils/halper.js");
|
|
388
|
-
const parentDir = resolve(repoRoot, "src/utils");
|
|
389
|
-
|
|
390
|
-
// Without hex-line: 3 round-trips (error → ls → retry)
|
|
391
|
-
const { value: without, ms: withoutMs } = runN(() => {
|
|
392
|
-
// Round 1: real ENOENT error
|
|
393
|
-
let r1;
|
|
394
|
-
try { readFileSync(missingPath, "utf-8"); r1 = ""; } catch (e) { r1 = e.message; }
|
|
395
|
-
// Round 2: real directory listing to find correct name
|
|
396
|
-
let r2;
|
|
397
|
-
try { r2 = readdirSync(parentDir).join("\n"); } catch { r2 = `${parentDir}: directory not found`; }
|
|
398
|
-
// Round 3: agent re-reads correct file (small file ~30 lines)
|
|
399
|
-
const r3 = simBuiltInReadFull(missingPath, tmpLines.slice(0, 30));
|
|
400
|
-
return (r1 + r2 + r3).length;
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// With hex-line: real readFile() on nonexistent path — returns error + parent dir listing
|
|
404
|
-
const { value: withSL, ms: withMs } = runN(() => {
|
|
405
|
-
try {
|
|
406
|
-
return readFile(missingPath).length;
|
|
407
|
-
} catch (e) {
|
|
408
|
-
return e.message.length;
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
results.push({
|
|
413
|
-
num: 12, scenario: "FILE_NOT_FOUND recovery*",
|
|
414
|
-
without, withSL,
|
|
415
|
-
savings: pctSavings(without, withSL),
|
|
416
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// ===================================================================
|
|
421
|
-
// TEST 13: Hash mismatch recovery
|
|
422
|
-
// ===================================================================
|
|
423
|
-
{
|
|
424
|
-
// Without hex-line: 3 round-trips (stale error → re-read full → retry edit)
|
|
425
|
-
const { value: without, ms: withoutMs } = runN(() => {
|
|
426
|
-
// Round 1: error
|
|
427
|
-
const r1 = 'Error: file content has changed (stale). Please re-read the file.';
|
|
428
|
-
// Round 2: full re-read
|
|
429
|
-
const r2 = simBuiltInReadFull(tmpPath, tmpLines);
|
|
430
|
-
// Round 3: retry edit response
|
|
431
|
-
const origLines = [...tmpLines];
|
|
432
|
-
const newLines = [...tmpLines];
|
|
433
|
-
newLines[12] = ' this.configPath = resolve(configPath || ".");';
|
|
434
|
-
const r3 = simBuiltInEdit(tmpPath, origLines, newLines);
|
|
435
|
-
return (r1 + r2 + r3).length;
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// With hex-line: 1 round-trip (error + fresh snippet +/-5 lines around target)
|
|
439
|
-
const { value: withSL, ms: withMs } = runN(() => {
|
|
440
|
-
const targetLine = 13;
|
|
441
|
-
const snippetStart = Math.max(0, targetLine - 6);
|
|
442
|
-
const snippetEnd = Math.min(tmpLines.length, targetLine + 5);
|
|
443
|
-
const snippet = tmpLines.slice(snippetStart, snippetEnd);
|
|
444
|
-
const annotated = snippet.map((l, i) => {
|
|
445
|
-
const lineNum = snippetStart + i + 1;
|
|
446
|
-
const tag = lineTag(fnv1a(l));
|
|
447
|
-
return `${tag}.${lineNum}\t${l}`;
|
|
448
|
-
}).join("\n");
|
|
449
|
-
const response = `HASH_MISMATCH at line ${targetLine}. Fresh snippet:\n\`\`\`\n${annotated}\n\`\`\``;
|
|
450
|
-
return response.length;
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
results.push({
|
|
454
|
-
num: 13, scenario: "Hash mismatch recovery*",
|
|
455
|
-
without, withSL,
|
|
456
|
-
savings: pctSavings(without, withSL),
|
|
457
|
-
latencyWithout: withoutMs, latencyWith: withMs,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ===================================================================
|
|
462
|
-
// TEST 14: Bash redirect savings
|
|
463
|
-
// ===================================================================
|
|
464
|
-
{
|
|
465
|
-
const infoFile = allFiles[Math.floor(allFiles.length / 2)] || allFiles[0];
|
|
466
|
-
const infoLines = getFileLines(infoFile);
|
|
467
|
-
if (infoLines) {
|
|
468
|
-
// Sub-test A: cat vs read_file
|
|
469
|
-
const catW = runN(() => {
|
|
470
|
-
// cat output: raw lines, no line numbers (agent redirect)
|
|
471
|
-
return infoLines.join("\n").length;
|
|
472
|
-
});
|
|
473
|
-
const catH = runN(() => readFile(infoFile).length);
|
|
474
|
-
|
|
475
|
-
// Sub-test B: ls -la vs directory_tree
|
|
476
|
-
const dirTarget = resolve(repoRoot);
|
|
477
|
-
const lsW = runN(() => simBuiltInLsR(dirTarget, 0, 1).length);
|
|
478
|
-
const lsH = runN(() => directoryTree(dirTarget, { max_depth: 1 }).length);
|
|
479
|
-
|
|
480
|
-
// Sub-test C: stat vs get_file_info
|
|
481
|
-
const stW = runN(() => simBuiltInStat(infoFile).length);
|
|
482
|
-
const stH = runN(() => fileInfo(infoFile).length);
|
|
483
|
-
|
|
484
|
-
// Combined: without = raw outputs (no follow-up possible)
|
|
485
|
-
// With = structured output (enables follow-up without extra calls)
|
|
486
|
-
const totalWithout = catW.value + lsW.value + stW.value;
|
|
487
|
-
const totalWith = catH.value + lsH.value + stH.value;
|
|
488
|
-
const totalMsWithout = catW.ms + lsW.ms + stW.ms;
|
|
489
|
-
const totalMsWith = catH.ms + lsH.ms + stH.ms;
|
|
490
|
-
|
|
491
|
-
results.push({
|
|
492
|
-
num: 14, scenario: "Bash redirects (cat+ls+stat)",
|
|
493
|
-
without: totalWithout, withSL: totalWith,
|
|
494
|
-
savings: pctSavings(totalWithout, totalWith),
|
|
495
|
-
latencyWithout: parseFloat(totalMsWithout.toFixed(1)), latencyWith: parseFloat(totalMsWith.toFixed(1)),
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return results;
|
|
502
|
-
}
|
package/benchmark/graph.mjs
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TEST 16-18: Graph enrichment benchmarks (--with-graph only).
|
|
3
|
-
*
|
|
4
|
-
* Both sides use hex-line; difference is whether .codegraph/index.db exists.
|
|
5
|
-
* Requires hex-graph index_project to have been run first.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { writeFileSync, unlinkSync } from "node:fs";
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { readFile } from "../lib/read.mjs";
|
|
12
|
-
import { editFile } from "../lib/edit.mjs";
|
|
13
|
-
import { grepSearch } from "../lib/search.mjs";
|
|
14
|
-
import { fnv1a, lineTag } from "../lib/hash.mjs";
|
|
15
|
-
import { getFileLines, fmt, pctSavings } from "../lib/benchmark-helpers.mjs";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Run TEST 16-18 graph enrichment benchmarks.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} config
|
|
21
|
-
* @param {string[]} config.allFiles - All discovered code files
|
|
22
|
-
* @param {string[]} config.largeFiles - Top 3 largest code files
|
|
23
|
-
* @param {string} config.repoRoot - Repository root path
|
|
24
|
-
* @returns {Promise<string[]>} Array of pre-formatted table row strings
|
|
25
|
-
*/
|
|
26
|
-
export async function runGraph(config) {
|
|
27
|
-
const { allFiles, largeFiles, repoRoot } = config;
|
|
28
|
-
const graphOut = [];
|
|
29
|
-
|
|
30
|
-
const { getGraphDB } = await import("../lib/graph-enrich.mjs");
|
|
31
|
-
const db = getGraphDB(resolve(repoRoot, "server.mjs"));
|
|
32
|
-
if (!db) {
|
|
33
|
-
console.error("--with-graph: .codegraph/index.db not found. Run hex-graph index_project first.");
|
|
34
|
-
return graphOut;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const graphFile = largeFiles[0] || allFiles[0];
|
|
38
|
-
const graphLines = getFileLines(graphFile);
|
|
39
|
-
|
|
40
|
-
if (!graphLines) return graphOut;
|
|
41
|
-
|
|
42
|
-
// TEST 16: Read with/without Graph header
|
|
43
|
-
{
|
|
44
|
-
const withGraphResult = readFile(graphFile);
|
|
45
|
-
const noGraphResult = withGraphResult.replace(/\nGraph:.*\n/, "\n");
|
|
46
|
-
const savings = pctSavings(noGraphResult.length, withGraphResult.length);
|
|
47
|
-
graphOut.push(`| 16 | Graph: Read (${graphLines.length}L) | ${fmt(noGraphResult.length)} chars | ${fmt(withGraphResult.length)} chars | ${savings} | 2\u21921 | 2\u21921 |`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// TEST 17: Edit with/without call impact
|
|
51
|
-
{
|
|
52
|
-
const editTmpPath = resolve(tmpdir(), `hex-bench-edit-${Date.now()}.js`);
|
|
53
|
-
writeFileSync(editTmpPath, graphLines.join("\n"), "utf-8");
|
|
54
|
-
try {
|
|
55
|
-
const tag = lineTag(fnv1a(graphLines[5]));
|
|
56
|
-
const editResult = editFile(editTmpPath, [{ set_line: { anchor: `${tag}.6`, new_text: graphLines[5] + " // modified" } }]);
|
|
57
|
-
const noBlastOut = editResult.replace(/\n.*Call impact.*$/s, "");
|
|
58
|
-
const savings = pctSavings(noBlastOut.length, editResult.length);
|
|
59
|
-
graphOut.push(`| 17 | Graph: Edit + call impact | ${fmt(noBlastOut.length)} chars | ${fmt(editResult.length)} chars | ${savings} | 2\u21921 | 2\u21921 |`);
|
|
60
|
-
} catch {
|
|
61
|
-
graphOut.push(`| 17 | Graph: Edit + call impact | \u2014 | \u2014 | \u2014 | | |`);
|
|
62
|
-
}
|
|
63
|
-
try { unlinkSync(editTmpPath); } catch {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// TEST 18: Grep with/without annotations
|
|
67
|
-
{
|
|
68
|
-
try {
|
|
69
|
-
const grepResult = await grepSearch("function", { path: resolve(repoRoot), glob: "*.mjs", limit: 10 });
|
|
70
|
-
const noAnnoResult = grepResult.replace(/ \[[^\]]+\]/g, "");
|
|
71
|
-
const savings = pctSavings(noAnnoResult.length, grepResult.length);
|
|
72
|
-
const annoCount = (grepResult.match(/\[[^\]]+\]/g) || []).length;
|
|
73
|
-
graphOut.push(`| 18 | Graph: Grep + ${annoCount} annotations | ${fmt(noAnnoResult.length)} chars | ${fmt(grepResult.length)} chars | ${savings} | 6\u21921 | 6\u21921 |`);
|
|
74
|
-
} catch {
|
|
75
|
-
graphOut.push(`| 18 | Graph: Grep + context | \u2014 | \u2014 | \u2014 | | |`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return graphOut;
|
|
80
|
-
}
|