opencode-hashline 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +442 -0
- package/README.ru.md +417 -0
- package/dist/chunk-C2EVIAGV.js +177 -0
- package/dist/chunk-IVZSANZ4.js +411 -0
- package/dist/hashline-Civwirvf.d.cts +278 -0
- package/dist/hashline-Civwirvf.d.ts +278 -0
- package/dist/hashline-W2FT5QN4.js +44 -0
- package/dist/index.cjs +811 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +197 -0
- package/dist/utils.cjs +637 -0
- package/dist/utils.d.cts +74 -0
- package/dist/utils.d.ts +74 -0
- package/dist/utils.js +54 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 izzzzzi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# π opencode-hashline
|
|
4
|
+
|
|
5
|
+
**Content-addressable line hashing for precise AI code editing**
|
|
6
|
+
|
|
7
|
+
[](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/opencode-hashline)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
|
|
13
|
+
[π·πΊ Π ΡΡΡΠΊΠΈΠΉ](README.ru.md) | **π¬π§ English**
|
|
14
|
+
|
|
15
|
+
<br />
|
|
16
|
+
|
|
17
|
+
*Hashline plugin for [OpenCode](https://github.com/anomalyco/opencode) β annotate every line with a deterministic hash tag so the AI can reference and edit code with surgical precision.*
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## π What is Hashline?
|
|
24
|
+
|
|
25
|
+
Hashline annotates every line of a file with a short, deterministic hex hash tag. When the AI reads a file, it sees:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
#HL 1:a3f|function hello() {
|
|
29
|
+
#HL 2:f1c| return "world";
|
|
30
|
+
#HL 3:0e7|}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **Note:** Hash length is adaptive β it depends on file size (3 chars for β€4096 lines, 4 chars for >4096 lines). Minimum hash length is 3 to reduce collision risk. The `#HL ` prefix protects against false positives when stripping hashes and is configurable.
|
|
34
|
+
|
|
35
|
+
The AI model can then reference lines by their hash tags for precise editing:
|
|
36
|
+
|
|
37
|
+
- **"Replace line `2:f1c`"** β target a specific line unambiguously
|
|
38
|
+
- **"Replace block from `1:a3f` to `3:0e7`"** β target a range of lines
|
|
39
|
+
- **"Insert after `3:0e7`"** β insert at a precise location
|
|
40
|
+
|
|
41
|
+
### π€ Why does this help?
|
|
42
|
+
|
|
43
|
+
Traditional line numbers shift as edits are made, causing off-by-one errors and stale references. Hashline tags are **content-addressable** β they're derived from both the line index and the line's content, so they serve as a stable, verifiable reference that the AI can use to communicate about code locations with precision.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## β¨ Features
|
|
48
|
+
|
|
49
|
+
### π Adaptive Hash Length
|
|
50
|
+
|
|
51
|
+
Hash length automatically adapts to file size to minimize collisions:
|
|
52
|
+
|
|
53
|
+
| File Size | Hash Length | Possible Values |
|
|
54
|
+
|-----------|:----------:|:---------------:|
|
|
55
|
+
| β€ 256 lines | 2 hex chars | 256 |
|
|
56
|
+
| β€ 4,096 lines | 3 hex chars | 4,096 |
|
|
57
|
+
| > 4,096 lines | 4 hex chars | 65,536 |
|
|
58
|
+
|
|
59
|
+
### π·οΈ Magic Prefix (`#HL `)
|
|
60
|
+
|
|
61
|
+
Lines are annotated with a configurable prefix (default: `#HL `) to prevent false positives when stripping hashes. This ensures that data lines like `1:ab|some data` are not accidentally stripped.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
#HL 1:a3|function hello() {
|
|
65
|
+
#HL 2:f1| return "world";
|
|
66
|
+
#HL 3:0e|}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The prefix can be customized or disabled for backward compatibility:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Custom prefix
|
|
73
|
+
const hl = createHashline({ prefix: ">> " });
|
|
74
|
+
|
|
75
|
+
// Disable prefix (legacy format: "1:a3|code")
|
|
76
|
+
const hl = createHashline({ prefix: false });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### πΎ LRU Caching
|
|
80
|
+
|
|
81
|
+
Built-in LRU cache (`filePath β annotatedContent`) with configurable size (default: 100 files). When the same file is read again with unchanged content, the cached result is returned instantly. Cache is automatically invalidated when file content changes.
|
|
82
|
+
|
|
83
|
+
### β
Hash Verification
|
|
84
|
+
|
|
85
|
+
Verify that a line hasn't changed since it was read β protects against race conditions:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { verifyHash } from "opencode-hashline";
|
|
89
|
+
|
|
90
|
+
const result = verifyHash(2, "f1c", currentContent);
|
|
91
|
+
if (!result.valid) {
|
|
92
|
+
console.error(result.message); // "Hash mismatch at line 2: ..."
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Hash verification uses the length of the provided hash reference (not the current file size), so a reference like `2:f1` remains valid even if the file has grown.
|
|
97
|
+
|
|
98
|
+
### π Indentation-Sensitive Hashing
|
|
99
|
+
|
|
100
|
+
Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
|
|
101
|
+
|
|
102
|
+
### π Range Operations
|
|
103
|
+
|
|
104
|
+
Resolve and replace ranges of lines by hash references:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { resolveRange, replaceRange } from "opencode-hashline";
|
|
108
|
+
|
|
109
|
+
// Get lines between two hash references
|
|
110
|
+
const range = resolveRange("1:a3f", "3:0e7", content);
|
|
111
|
+
console.log(range.lines); // ["function hello() {", ' return "world";', "}"]
|
|
112
|
+
|
|
113
|
+
// Replace a range with new content
|
|
114
|
+
const newContent = replaceRange(
|
|
115
|
+
"1:a3f", "3:0e7", content,
|
|
116
|
+
"function goodbye() {\n return 'farewell';\n}"
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### βοΈ Configurable
|
|
121
|
+
|
|
122
|
+
Create custom Hashline instances with specific settings:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { createHashline } from "opencode-hashline";
|
|
126
|
+
|
|
127
|
+
const hl = createHashline({
|
|
128
|
+
exclude: ["**/node_modules/**", "**/*.min.js"],
|
|
129
|
+
maxFileSize: 512_000, // 512 KB
|
|
130
|
+
hashLength: 3, // force 3-char hashes
|
|
131
|
+
cacheSize: 200, // cache up to 200 files
|
|
132
|
+
prefix: "#HL ", // magic prefix (default)
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Use the configured instance
|
|
136
|
+
const annotated = hl.formatFileWithHashes(content, "src/app.ts");
|
|
137
|
+
const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Configuration Options
|
|
141
|
+
|
|
142
|
+
| Option | Type | Default | Description |
|
|
143
|
+
|--------|------|---------|-------------|
|
|
144
|
+
| `exclude` | `string[]` | See below | Glob patterns for files to skip |
|
|
145
|
+
| `maxFileSize` | `number` | `1_000_000` | Max file size in bytes |
|
|
146
|
+
| `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
|
|
147
|
+
| `cacheSize` | `number` | `100` | Max files in LRU cache |
|
|
148
|
+
| `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
|
|
149
|
+
|
|
150
|
+
Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## π¦ Installation
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm install opencode-hashline
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## π§ Configuration
|
|
163
|
+
|
|
164
|
+
Add the plugin to your `opencode.json`:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"$schema": "https://opencode.ai/config.json",
|
|
169
|
+
"plugin": ["opencode-hashline"]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Configuration Files
|
|
174
|
+
|
|
175
|
+
The plugin loads configuration from the following locations (in priority order, later overrides earlier):
|
|
176
|
+
|
|
177
|
+
| Priority | Location | Scope |
|
|
178
|
+
|:--------:|----------|-------|
|
|
179
|
+
| 1 | `~/.config/opencode/opencode-hashline.json` | Global (all projects) |
|
|
180
|
+
| 2 | `<project>/opencode-hashline.json` | Project-local |
|
|
181
|
+
| 3 | Programmatic config via `createHashlinePlugin()` | Factory argument |
|
|
182
|
+
|
|
183
|
+
Example `opencode-hashline.json`:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"exclude": ["**/node_modules/**", "**/*.min.js"],
|
|
188
|
+
"maxFileSize": 1048576,
|
|
189
|
+
"hashLength": 0,
|
|
190
|
+
"cacheSize": 100,
|
|
191
|
+
"prefix": "#HL "
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
That's it! The plugin automatically:
|
|
196
|
+
|
|
197
|
+
| # | Action | Description |
|
|
198
|
+
|:-:|--------|-------------|
|
|
199
|
+
| 1 | π **Annotates file reads** | When the AI reads a file, each line gets a `#HL` hash prefix |
|
|
200
|
+
| 2 | π **Annotates `@file` mentions** | Files attached via `@filename` in prompts are also annotated with hashlines |
|
|
201
|
+
| 3 | βοΈ **Strips hash prefixes on edits** | When the AI writes/edits a file, hash prefixes are removed before applying changes |
|
|
202
|
+
| 4 | π§ **Injects system prompt instructions** | The AI is told how to interpret and use hashline references |
|
|
203
|
+
| 5 | πΎ **Caches results** | Repeated reads of the same file return cached annotations |
|
|
204
|
+
| 6 | π **Filters by tool** | Only file-reading tools (e.g. `read_file`, `cat`, `view`) get annotations; other tools are left untouched |
|
|
205
|
+
| 7 | βοΈ **Respects config** | Excluded files and files exceeding `maxFileSize` are skipped |
|
|
206
|
+
| 8 | π§© **Registers `hashline_edit` tool** | Applies replace/delete/insert by hash references, without exact `old_string` matching |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## π οΈ How It Works
|
|
211
|
+
|
|
212
|
+
### Hash Computation
|
|
213
|
+
|
|
214
|
+
Each line's hash is computed from:
|
|
215
|
+
- The **0-based line index**
|
|
216
|
+
- The **trimEnd'd line content** β leading whitespace (indentation) IS significant
|
|
217
|
+
|
|
218
|
+
This is fed through an **FNV-1a** hash function, reduced to the appropriate modulus based on file size, and rendered as a hex string.
|
|
219
|
+
|
|
220
|
+
### Plugin Hooks & Tool
|
|
221
|
+
|
|
222
|
+
The plugin registers four OpenCode hooks and one custom tool:
|
|
223
|
+
|
|
224
|
+
| Hook | Purpose |
|
|
225
|
+
|------|---------|
|
|
226
|
+
| `tool.hashline_edit` | Hash-aware edits by references like `5:a3f` or `#HL 5:a3f|...` |
|
|
227
|
+
| `tool.execute.after` | Injects hashline annotations into file-read tool output |
|
|
228
|
+
| `tool.execute.before` | Strips hashline prefixes from file-edit tool arguments |
|
|
229
|
+
| `chat.message` | Annotates `@file` mentions in user messages (writes annotated content to a temp file and swaps the URL) |
|
|
230
|
+
| `experimental.chat.system.transform` | Adds hashline usage instructions to the system prompt |
|
|
231
|
+
|
|
232
|
+
### Tool Detection Heuristic (`isFileReadTool`)
|
|
233
|
+
|
|
234
|
+
The plugin needs to determine which tools are "file-read" tools (to annotate their output) vs "file-edit" tools (to strip hash prefixes from their input). Since the OpenCode plugin API does not expose a semantic tool category, the plugin uses a name-based heuristic:
|
|
235
|
+
|
|
236
|
+
**Exact match** β the tool name (case-insensitive) is compared against the allow-list:
|
|
237
|
+
- `read`, `file_read`, `read_file`, `cat`, `view`
|
|
238
|
+
|
|
239
|
+
**Dotted suffix match** β for namespaced tools like `mcp.read` or `custom_provider.file_read`, the part after the last `.` is matched against the same list.
|
|
240
|
+
|
|
241
|
+
**Fallback heuristic** β if the tool has `path`, `filePath`, or `file` arguments AND the tool name does NOT contain write/edit/execute indicators (`write`, `edit`, `patch`, `execute`, `run`, `command`, `shell`, `bash`), it is treated as a file-read tool.
|
|
242
|
+
|
|
243
|
+
**How to customize:**
|
|
244
|
+
- Name your custom tool to match one of the patterns above (e.g. `my_read_file`)
|
|
245
|
+
- Include `path`, `filePath`, or `file` in its arguments
|
|
246
|
+
- Or extend the `FILE_READ_TOOLS` list in a fork
|
|
247
|
+
|
|
248
|
+
The `isFileReadTool()` function is exported for testing and advanced usage:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { isFileReadTool } from "opencode-hashline";
|
|
252
|
+
|
|
253
|
+
isFileReadTool("read_file"); // true
|
|
254
|
+
isFileReadTool("mcp.read"); // true
|
|
255
|
+
isFileReadTool("custom_reader", { path: "app.ts" }); // true (heuristic)
|
|
256
|
+
isFileReadTool("file_write", { path: "app.ts" }); // false (write indicator)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Programmatic API
|
|
260
|
+
|
|
261
|
+
The core utilities are exported from the `opencode-hashline/utils` subpath (to avoid conflicts with OpenCode's plugin loader, which calls every export as a Plugin function):
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import {
|
|
265
|
+
computeLineHash,
|
|
266
|
+
formatFileWithHashes,
|
|
267
|
+
stripHashes,
|
|
268
|
+
parseHashRef,
|
|
269
|
+
normalizeHashRef,
|
|
270
|
+
buildHashMap,
|
|
271
|
+
getAdaptiveHashLength,
|
|
272
|
+
verifyHash,
|
|
273
|
+
resolveRange,
|
|
274
|
+
replaceRange,
|
|
275
|
+
applyHashEdit,
|
|
276
|
+
HashlineCache,
|
|
277
|
+
createHashline,
|
|
278
|
+
shouldExclude,
|
|
279
|
+
matchesGlob,
|
|
280
|
+
resolveConfig,
|
|
281
|
+
DEFAULT_PREFIX,
|
|
282
|
+
} from "opencode-hashline/utils";
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Core Functions
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Compute hash for a single line
|
|
289
|
+
const hash = computeLineHash(0, "function hello() {"); // e.g. "a3f"
|
|
290
|
+
|
|
291
|
+
// Compute hash with specific length
|
|
292
|
+
const hash4 = computeLineHash(0, "function hello() {", 4); // e.g. "a3f2"
|
|
293
|
+
|
|
294
|
+
// Annotate entire file content (adaptive hash length, with #HL prefix)
|
|
295
|
+
const annotated = formatFileWithHashes(fileContent);
|
|
296
|
+
// "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"
|
|
297
|
+
|
|
298
|
+
// Annotate with specific hash length
|
|
299
|
+
const annotated3 = formatFileWithHashes(fileContent, 3);
|
|
300
|
+
|
|
301
|
+
// Annotate without prefix (legacy format)
|
|
302
|
+
const annotatedLegacy = formatFileWithHashes(fileContent, undefined, false);
|
|
303
|
+
|
|
304
|
+
// Strip annotations to get original content
|
|
305
|
+
const original = stripHashes(annotated);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Hash References & Verification
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Parse a hash reference
|
|
312
|
+
const { line, hash } = parseHashRef("2:f1c"); // { line: 2, hash: "f1c" }
|
|
313
|
+
|
|
314
|
+
// Normalize from an annotated line
|
|
315
|
+
const ref = normalizeHashRef("#HL 2:f1c|const x = 1;"); // "2:f1c"
|
|
316
|
+
|
|
317
|
+
// Build a lookup map
|
|
318
|
+
const map = buildHashMap(fileContent); // Map<"2:f1c", 2>
|
|
319
|
+
|
|
320
|
+
// Verify a hash reference (uses hash.length, not file size)
|
|
321
|
+
const result = verifyHash(2, "f1c", fileContent);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Range Operations
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// Resolve a range
|
|
328
|
+
const range = resolveRange("1:a3f", "3:0e7", fileContent);
|
|
329
|
+
|
|
330
|
+
// Replace a range
|
|
331
|
+
const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "new content");
|
|
332
|
+
|
|
333
|
+
// Hash-aware edit operation (replace/delete/insert_before/insert_after)
|
|
334
|
+
const edited = applyHashEdit(
|
|
335
|
+
{ operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "new content" },
|
|
336
|
+
fileContent
|
|
337
|
+
).content;
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Utilities
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Check if a file should be excluded
|
|
344
|
+
const excluded = shouldExclude("node_modules/foo.js", ["**/node_modules/**"]);
|
|
345
|
+
|
|
346
|
+
// Create a configured instance
|
|
347
|
+
const hl = createHashline({ cacheSize: 50, hashLength: 3 });
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## π Benchmark
|
|
353
|
+
|
|
354
|
+
### Correctness: hashline vs str_replace
|
|
355
|
+
|
|
356
|
+
We tested both approaches on **60 fixtures from [react-edit-benchmark](https://github.com/can1357/oh-my-pi/tree/main/packages/react-edit-benchmark)** β mutated React source files with known bugs (flipped booleans, swapped operators, removed guard clauses, etc.):
|
|
357
|
+
|
|
358
|
+
| | hashline | str_replace |
|
|
359
|
+
|---|:---:|:---:|
|
|
360
|
+
| **Passed** | **60/60 (100%)** | 58/60 (96.7%) |
|
|
361
|
+
| **Failed** | 0 | 2 |
|
|
362
|
+
| **Ambiguous edits** | 0 | 4 |
|
|
363
|
+
|
|
364
|
+
str_replace fails when the `old_string` appears multiple times in the file (e.g. repeated guard clauses, similar code blocks). Hashline addresses each line uniquely via `lineNumber:hash`, so ambiguity is impossible.
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Run yourself:
|
|
368
|
+
npx tsx benchmark/run.ts # hashline mode
|
|
369
|
+
npx tsx benchmark/run.ts --no-hash # str_replace mode
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
<details>
|
|
373
|
+
<summary>str_replace failures (structural category)</summary>
|
|
374
|
+
|
|
375
|
+
- `structural-remove-early-return-001` β `old_string` matched multiple locations, wrong one replaced
|
|
376
|
+
- `structural-remove-early-return-002` β same issue
|
|
377
|
+
- `structural-delete-statement-002` β ambiguous match (first match happened to be correct)
|
|
378
|
+
- `structural-delete-statement-003` β ambiguous match (first match happened to be correct)
|
|
379
|
+
|
|
380
|
+
</details>
|
|
381
|
+
|
|
382
|
+
### Token Overhead
|
|
383
|
+
|
|
384
|
+
Hashline annotations add `#HL <line>:<hash>|` prefix (~12 chars / ~3 tokens) per line:
|
|
385
|
+
|
|
386
|
+
| | Plain | Annotated | Overhead |
|
|
387
|
+
|---|---:|---:|:---:|
|
|
388
|
+
| **Characters** | 404K | 564K | +40% |
|
|
389
|
+
| **Tokens (~)** | ~101K | ~141K | +40% |
|
|
390
|
+
|
|
391
|
+
Overhead is stable at ~40% regardless of file size. For a typical 200-line file (~800 tokens), hashline adds ~600 tokens β negligible in a 200K context window.
|
|
392
|
+
|
|
393
|
+
### Performance
|
|
394
|
+
|
|
395
|
+
| File Size | Annotate | Edit | Strip |
|
|
396
|
+
|----------:|:--------:|:----:|:-----:|
|
|
397
|
+
| **10** lines | 0.05 ms | 0.01 ms | 0.03 ms |
|
|
398
|
+
| **100** lines | 0.12 ms | 0.02 ms | 0.08 ms |
|
|
399
|
+
| **1,000** lines | 0.95 ms | 0.04 ms | 0.60 ms |
|
|
400
|
+
| **5,000** lines | 4.50 ms | 0.08 ms | 2.80 ms |
|
|
401
|
+
| **10,000** lines | 9.20 ms | 0.10 ms | 5.50 ms |
|
|
402
|
+
|
|
403
|
+
> A typical 1,000-line source file is annotated in **< 1ms** β imperceptible to the user.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## π§βπ» Development
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
# Install dependencies
|
|
411
|
+
npm install
|
|
412
|
+
|
|
413
|
+
# Run tests
|
|
414
|
+
npm test
|
|
415
|
+
|
|
416
|
+
# Build
|
|
417
|
+
npm run build
|
|
418
|
+
|
|
419
|
+
# Type check
|
|
420
|
+
npm run typecheck
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## π‘ Inspiration & Background
|
|
426
|
+
|
|
427
|
+
The idea behind hashline is inspired by concepts from **oh-my-pi** by [can1357](https://github.com/can1357/oh-my-pi) β an AI coding agent toolkit (coding agent CLI, unified LLM API, TUI libraries) β and the article "The Harness Problem."
|
|
428
|
+
|
|
429
|
+
**The Harness Problem** describes a fundamental limitation of current AI coding tools: while modern LLMs are extremely capable, the *harness* layer β the tooling that feeds context to the model and applies its edits back to files β loses information and introduces errors. The model sees a file's content, but when it needs to edit, it must "guess" surrounding context for search-and-replace (which breaks on duplicate lines) or produce diffs (which are unreliable in practice).
|
|
430
|
+
|
|
431
|
+
Hashline solves this by assigning each line a short, deterministic hash tag (e.g. `2:f1c`), making line addressing **exact and unambiguous**. The model can reference any line or range precisely, eliminating off-by-one errors and duplicate-line confusion.
|
|
432
|
+
|
|
433
|
+
**References:**
|
|
434
|
+
- [oh-my-pi by can1357](https://github.com/can1357/oh-my-pi) β AI coding agent toolkit: coding agent CLI, unified LLM API, TUI libraries
|
|
435
|
+
- [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) β blog post describing the problem in detail
|
|
436
|
+
- [ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ ΠΏΠΎΠ΄Ρ
ΠΎΠ΄Π° Π½Π° Π₯Π°Π±ΡΠ΅](https://habr.com/ru/companies/bothub/news/995986/) β overview of the approach in Russian
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## π License
|
|
441
|
+
|
|
442
|
+
[MIT](LICENSE) Β© opencode-hashline contributors
|