opencode-hashline 1.0.5 → 1.1.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/README.en.md ADDED
@@ -0,0 +1,446 @@
1
+ <div align="center">
2
+
3
+ # 🔗 opencode-hashline
4
+
5
+ **Content-addressable line hashing for precise AI code editing**
6
+
7
+ [![CI](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
8
+ [![Release](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
9
+ [![npm version](https://img.shields.io/npm/v/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
10
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
11
+ [![GitHub release](https://img.shields.io/github/v/release/izzzzzi/opencode-hashline?style=flat&colorA=18181B&colorB=28CF8D)](https://github.com/izzzzzi/opencode-hashline/releases)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat&colorA=18181B&colorB=28CF8D)](LICENSE)
13
+ [![semantic-release](https://img.shields.io/badge/semantic--release-auto-e10079?style=flat&colorA=18181B)](https://github.com/semantic-release/semantic-release)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
15
+ [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
16
+
17
+ [🇷🇺 Русский](README.md) | **🇬🇧 English**
18
+
19
+ <br />
20
+
21
+ *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.*
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ ## 📖 What is Hashline?
28
+
29
+ Hashline annotates every line of a file with a short, deterministic hex hash tag. When the AI reads a file, it sees:
30
+
31
+ ```
32
+ #HL 1:a3f|function hello() {
33
+ #HL 2:f1c| return "world";
34
+ #HL 3:0e7|}
35
+ ```
36
+
37
+ > **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.
38
+
39
+ The AI model can then reference lines by their hash tags for precise editing:
40
+
41
+ - **"Replace line `2:f1c`"** — target a specific line unambiguously
42
+ - **"Replace block from `1:a3f` to `3:0e7`"** — target a range of lines
43
+ - **"Insert after `3:0e7`"** — insert at a precise location
44
+
45
+ ### 🤔 Why does this help?
46
+
47
+ 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.
48
+
49
+ ---
50
+
51
+ ## ✨ Features
52
+
53
+ ### 📏 Adaptive Hash Length
54
+
55
+ Hash length automatically adapts to file size to minimize collisions:
56
+
57
+ | File Size | Hash Length | Possible Values |
58
+ |-----------|:----------:|:---------------:|
59
+ | ≤ 256 lines | 2 hex chars | 256 |
60
+ | ≤ 4,096 lines | 3 hex chars | 4,096 |
61
+ | > 4,096 lines | 4 hex chars | 65,536 |
62
+
63
+ ### 🏷️ Magic Prefix (`#HL `)
64
+
65
+ 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.
66
+
67
+ ```
68
+ #HL 1:a3|function hello() {
69
+ #HL 2:f1| return "world";
70
+ #HL 3:0e|}
71
+ ```
72
+
73
+ The prefix can be customized or disabled for backward compatibility:
74
+
75
+ ```typescript
76
+ // Custom prefix
77
+ const hl = createHashline({ prefix: ">> " });
78
+
79
+ // Disable prefix (legacy format: "1:a3|code")
80
+ const hl = createHashline({ prefix: false });
81
+ ```
82
+
83
+ ### 💾 LRU Caching
84
+
85
+ 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.
86
+
87
+ ### ✅ Hash Verification
88
+
89
+ Verify that a line hasn't changed since it was read — protects against race conditions:
90
+
91
+ ```typescript
92
+ import { verifyHash } from "opencode-hashline";
93
+
94
+ const result = verifyHash(2, "f1c", currentContent);
95
+ if (!result.valid) {
96
+ console.error(result.message); // "Hash mismatch at line 2: ..."
97
+ }
98
+ ```
99
+
100
+ 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.
101
+
102
+ ### 🔍 Indentation-Sensitive Hashing
103
+
104
+ Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
105
+
106
+ ### 📐 Range Operations
107
+
108
+ Resolve and replace ranges of lines by hash references:
109
+
110
+ ```typescript
111
+ import { resolveRange, replaceRange } from "opencode-hashline";
112
+
113
+ // Get lines between two hash references
114
+ const range = resolveRange("1:a3f", "3:0e7", content);
115
+ console.log(range.lines); // ["function hello() {", ' return "world";', "}"]
116
+
117
+ // Replace a range with new content
118
+ const newContent = replaceRange(
119
+ "1:a3f", "3:0e7", content,
120
+ "function goodbye() {\n return 'farewell';\n}"
121
+ );
122
+ ```
123
+
124
+ ### ⚙️ Configurable
125
+
126
+ Create custom Hashline instances with specific settings:
127
+
128
+ ```typescript
129
+ import { createHashline } from "opencode-hashline";
130
+
131
+ const hl = createHashline({
132
+ exclude: ["**/node_modules/**", "**/*.min.js"],
133
+ maxFileSize: 512_000, // 512 KB
134
+ hashLength: 3, // force 3-char hashes
135
+ cacheSize: 200, // cache up to 200 files
136
+ prefix: "#HL ", // magic prefix (default)
137
+ });
138
+
139
+ // Use the configured instance
140
+ const annotated = hl.formatFileWithHashes(content, "src/app.ts");
141
+ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
142
+ ```
143
+
144
+ #### Configuration Options
145
+
146
+ | Option | Type | Default | Description |
147
+ |--------|------|---------|-------------|
148
+ | `exclude` | `string[]` | See below | Glob patterns for files to skip |
149
+ | `maxFileSize` | `number` | `1_000_000` | Max file size in bytes |
150
+ | `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
151
+ | `cacheSize` | `number` | `100` | Max files in LRU cache |
152
+ | `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
153
+
154
+ Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
155
+
156
+ ---
157
+
158
+ ## 📦 Installation
159
+
160
+ ```bash
161
+ npm install opencode-hashline
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 🔧 Configuration
167
+
168
+ Add the plugin to your `opencode.json`:
169
+
170
+ ```json
171
+ {
172
+ "$schema": "https://opencode.ai/config.json",
173
+ "plugin": ["opencode-hashline"]
174
+ }
175
+ ```
176
+
177
+ ### Configuration Files
178
+
179
+ The plugin loads configuration from the following locations (in priority order, later overrides earlier):
180
+
181
+ | Priority | Location | Scope |
182
+ |:--------:|----------|-------|
183
+ | 1 | `~/.config/opencode/opencode-hashline.json` | Global (all projects) |
184
+ | 2 | `<project>/opencode-hashline.json` | Project-local |
185
+ | 3 | Programmatic config via `createHashlinePlugin()` | Factory argument |
186
+
187
+ Example `opencode-hashline.json`:
188
+
189
+ ```json
190
+ {
191
+ "exclude": ["**/node_modules/**", "**/*.min.js"],
192
+ "maxFileSize": 1048576,
193
+ "hashLength": 0,
194
+ "cacheSize": 100,
195
+ "prefix": "#HL "
196
+ }
197
+ ```
198
+
199
+ That's it! The plugin automatically:
200
+
201
+ | # | Action | Description |
202
+ |:-:|--------|-------------|
203
+ | 1 | 📝 **Annotates file reads** | When the AI reads a file, each line gets a `#HL` hash prefix |
204
+ | 2 | 📎 **Annotates `@file` mentions** | Files attached via `@filename` in prompts are also annotated with hashlines |
205
+ | 3 | ✂️ **Strips hash prefixes on edits** | When the AI writes/edits a file, hash prefixes are removed before applying changes |
206
+ | 4 | 🧠 **Injects system prompt instructions** | The AI is told how to interpret and use hashline references |
207
+ | 5 | 💾 **Caches results** | Repeated reads of the same file return cached annotations |
208
+ | 6 | 🔍 **Filters by tool** | Only file-reading tools (e.g. `read_file`, `cat`, `view`) get annotations; other tools are left untouched |
209
+ | 7 | ⚙️ **Respects config** | Excluded files and files exceeding `maxFileSize` are skipped |
210
+ | 8 | 🧩 **Registers `hashline_edit` tool** | Applies replace/delete/insert by hash references, without exact `old_string` matching |
211
+
212
+ ---
213
+
214
+ ## 🛠️ How It Works
215
+
216
+ ### Hash Computation
217
+
218
+ Each line's hash is computed from:
219
+ - The **0-based line index**
220
+ - The **trimEnd'd line content** — leading whitespace (indentation) IS significant
221
+
222
+ This is fed through an **FNV-1a** hash function, reduced to the appropriate modulus based on file size, and rendered as a hex string.
223
+
224
+ ### Plugin Hooks & Tool
225
+
226
+ The plugin registers four OpenCode hooks and one custom tool:
227
+
228
+ | Hook | Purpose |
229
+ |------|---------|
230
+ | `tool.hashline_edit` | Hash-aware edits by references like `5:a3f` or `#HL 5:a3f|...` |
231
+ | `tool.execute.after` | Injects hashline annotations into file-read tool output |
232
+ | `tool.execute.before` | Strips hashline prefixes from file-edit tool arguments |
233
+ | `chat.message` | Annotates `@file` mentions in user messages (writes annotated content to a temp file and swaps the URL) |
234
+ | `experimental.chat.system.transform` | Adds hashline usage instructions to the system prompt |
235
+
236
+ ### Tool Detection Heuristic (`isFileReadTool`)
237
+
238
+ 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:
239
+
240
+ **Exact match** — the tool name (case-insensitive) is compared against the allow-list:
241
+ - `read`, `file_read`, `read_file`, `cat`, `view`
242
+
243
+ **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.
244
+
245
+ **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.
246
+
247
+ **How to customize:**
248
+ - Name your custom tool to match one of the patterns above (e.g. `my_read_file`)
249
+ - Include `path`, `filePath`, or `file` in its arguments
250
+ - Or extend the `FILE_READ_TOOLS` list in a fork
251
+
252
+ The `isFileReadTool()` function is exported for testing and advanced usage:
253
+
254
+ ```typescript
255
+ import { isFileReadTool } from "opencode-hashline";
256
+
257
+ isFileReadTool("read_file"); // true
258
+ isFileReadTool("mcp.read"); // true
259
+ isFileReadTool("custom_reader", { path: "app.ts" }); // true (heuristic)
260
+ isFileReadTool("file_write", { path: "app.ts" }); // false (write indicator)
261
+ ```
262
+
263
+ ### Programmatic API
264
+
265
+ 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):
266
+
267
+ ```typescript
268
+ import {
269
+ computeLineHash,
270
+ formatFileWithHashes,
271
+ stripHashes,
272
+ parseHashRef,
273
+ normalizeHashRef,
274
+ buildHashMap,
275
+ getAdaptiveHashLength,
276
+ verifyHash,
277
+ resolveRange,
278
+ replaceRange,
279
+ applyHashEdit,
280
+ HashlineCache,
281
+ createHashline,
282
+ shouldExclude,
283
+ matchesGlob,
284
+ resolveConfig,
285
+ DEFAULT_PREFIX,
286
+ } from "opencode-hashline/utils";
287
+ ```
288
+
289
+ ### Core Functions
290
+
291
+ ```typescript
292
+ // Compute hash for a single line
293
+ const hash = computeLineHash(0, "function hello() {"); // e.g. "a3f"
294
+
295
+ // Compute hash with specific length
296
+ const hash4 = computeLineHash(0, "function hello() {", 4); // e.g. "a3f2"
297
+
298
+ // Annotate entire file content (adaptive hash length, with #HL prefix)
299
+ const annotated = formatFileWithHashes(fileContent);
300
+ // "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"
301
+
302
+ // Annotate with specific hash length
303
+ const annotated3 = formatFileWithHashes(fileContent, 3);
304
+
305
+ // Annotate without prefix (legacy format)
306
+ const annotatedLegacy = formatFileWithHashes(fileContent, undefined, false);
307
+
308
+ // Strip annotations to get original content
309
+ const original = stripHashes(annotated);
310
+ ```
311
+
312
+ ### Hash References & Verification
313
+
314
+ ```typescript
315
+ // Parse a hash reference
316
+ const { line, hash } = parseHashRef("2:f1c"); // { line: 2, hash: "f1c" }
317
+
318
+ // Normalize from an annotated line
319
+ const ref = normalizeHashRef("#HL 2:f1c|const x = 1;"); // "2:f1c"
320
+
321
+ // Build a lookup map
322
+ const map = buildHashMap(fileContent); // Map<"2:f1c", 2>
323
+
324
+ // Verify a hash reference (uses hash.length, not file size)
325
+ const result = verifyHash(2, "f1c", fileContent);
326
+ ```
327
+
328
+ ### Range Operations
329
+
330
+ ```typescript
331
+ // Resolve a range
332
+ const range = resolveRange("1:a3f", "3:0e7", fileContent);
333
+
334
+ // Replace a range
335
+ const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "new content");
336
+
337
+ // Hash-aware edit operation (replace/delete/insert_before/insert_after)
338
+ const edited = applyHashEdit(
339
+ { operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "new content" },
340
+ fileContent
341
+ ).content;
342
+ ```
343
+
344
+ ### Utilities
345
+
346
+ ```typescript
347
+ // Check if a file should be excluded
348
+ const excluded = shouldExclude("node_modules/foo.js", ["**/node_modules/**"]);
349
+
350
+ // Create a configured instance
351
+ const hl = createHashline({ cacheSize: 50, hashLength: 3 });
352
+ ```
353
+
354
+ ---
355
+
356
+ ## 📊 Benchmark
357
+
358
+ ### Correctness: hashline vs str_replace
359
+
360
+ 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.):
361
+
362
+ | | hashline | str_replace |
363
+ |---|:---:|:---:|
364
+ | **Passed** | **60/60 (100%)** | 58/60 (96.7%) |
365
+ | **Failed** | 0 | 2 |
366
+ | **Ambiguous edits** | 0 | 4 |
367
+
368
+ 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.
369
+
370
+ ```bash
371
+ # Run yourself:
372
+ npx tsx benchmark/run.ts # hashline mode
373
+ npx tsx benchmark/run.ts --no-hash # str_replace mode
374
+ ```
375
+
376
+ <details>
377
+ <summary>str_replace failures (structural category)</summary>
378
+
379
+ - `structural-remove-early-return-001` — `old_string` matched multiple locations, wrong one replaced
380
+ - `structural-remove-early-return-002` — same issue
381
+ - `structural-delete-statement-002` — ambiguous match (first match happened to be correct)
382
+ - `structural-delete-statement-003` — ambiguous match (first match happened to be correct)
383
+
384
+ </details>
385
+
386
+ ### Token Overhead
387
+
388
+ Hashline annotations add `#HL <line>:<hash>|` prefix (~12 chars / ~3 tokens) per line:
389
+
390
+ | | Plain | Annotated | Overhead |
391
+ |---|---:|---:|:---:|
392
+ | **Characters** | 404K | 564K | +40% |
393
+ | **Tokens (~)** | ~101K | ~141K | +40% |
394
+
395
+ 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.
396
+
397
+ ### Performance
398
+
399
+ | File Size | Annotate | Edit | Strip |
400
+ |----------:|:--------:|:----:|:-----:|
401
+ | **10** lines | 0.05 ms | 0.01 ms | 0.03 ms |
402
+ | **100** lines | 0.12 ms | 0.02 ms | 0.08 ms |
403
+ | **1,000** lines | 0.95 ms | 0.04 ms | 0.60 ms |
404
+ | **5,000** lines | 4.50 ms | 0.08 ms | 2.80 ms |
405
+ | **10,000** lines | 9.20 ms | 0.10 ms | 5.50 ms |
406
+
407
+ > A typical 1,000-line source file is annotated in **< 1ms** — imperceptible to the user.
408
+
409
+ ---
410
+
411
+ ## 🧑‍💻 Development
412
+
413
+ ```bash
414
+ # Install dependencies
415
+ npm install
416
+
417
+ # Run tests
418
+ npm test
419
+
420
+ # Build
421
+ npm run build
422
+
423
+ # Type check
424
+ npm run typecheck
425
+ ```
426
+
427
+ ---
428
+
429
+ ## 💡 Inspiration & Background
430
+
431
+ 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."
432
+
433
+ **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).
434
+
435
+ 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.
436
+
437
+ **References:**
438
+ - [oh-my-pi by can1357](https://github.com/can1357/oh-my-pi) — AI coding agent toolkit: coding agent CLI, unified LLM API, TUI libraries
439
+ - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — blog post describing the problem in detail
440
+ - [Описание подхода на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — overview of the approach in Russian
441
+
442
+ ---
443
+
444
+ ## 📄 License
445
+
446
+ [MIT](LICENSE) © opencode-hashline contributors
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # 🔗 opencode-hashline
4
4
 
5
- **Content-addressable line hashing for precise AI code editing**
5
+ **Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI**
6
6
 
7
7
  [![CI](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
8
8
  [![Release](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
@@ -14,19 +14,19 @@
14
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
15
15
  [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
16
16
 
17
- [🇷🇺 Русский](README.ru.md) | **🇬🇧 English**
17
+ **🇷🇺 Русский** | [🇬🇧 English](README.en.md)
18
18
 
19
19
  <br />
20
20
 
21
- *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.*
21
+ *Hashline-плагин для [OpenCode](https://github.com/anomalyco/opencode) — аннотирует каждую строку файла детерминированным хеш-тегом, чтобы AI мог ссылаться на код и редактировать его с хирургической точностью.*
22
22
 
23
23
  </div>
24
24
 
25
25
  ---
26
26
 
27
- ## 📖 What is Hashline?
27
+ ## 📖 Что такое Hashline?
28
28
 
29
- Hashline annotates every line of a file with a short, deterministic hex hash tag. When the AI reads a file, it sees:
29
+ Hashline аннотирует каждую строку файла коротким детерминированным hex-хешем. Когда AI читает файл, он видит:
30
30
 
31
31
  ```
32
32
  #HL 1:a3f|function hello() {
@@ -34,35 +34,35 @@ Hashline annotates every line of a file with a short, deterministic hex hash tag
34
34
  #HL 3:0e7|}
35
35
  ```
36
36
 
37
- > **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.
37
+ > **Примечание:** Длина хеша адаптивнаяона зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
38
38
 
39
- The AI model can then reference lines by their hash tags for precise editing:
39
+ AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
40
40
 
41
- - **"Replace line `2:f1c`"**target a specific line unambiguously
42
- - **"Replace block from `1:a3f` to `3:0e7`"**target a range of lines
43
- - **"Insert after `3:0e7`"**insert at a precise location
41
+ - **«Заменить строку `2:f1c`»**указать конкретную строку однозначно
42
+ - **«Заменить блок от `1:a3f` до `3:0e7`»**указать диапазон строк
43
+ - **«Вставить после `3:0e7`»**вставить в точное место
44
44
 
45
- ### 🤔 Why does this help?
45
+ ### 🤔 Почему это помогает?
46
46
 
47
- 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.
47
+ Традиционные номера строк сдвигаются при редактировании, вызывая ошибки смещения и устаревшие ссылки. Хеш-теги Hashline **контентно-адресуемы**они вычисляются из индекса строки и её содержимого, что делает их стабильной, верифицируемой ссылкой для точной коммуникации о местоположении в коде.
48
48
 
49
49
  ---
50
50
 
51
- ## ✨ Features
51
+ ## ✨ Возможности
52
52
 
53
- ### 📏 Adaptive Hash Length
53
+ ### 📏 Адаптивная длина хеша
54
54
 
55
- Hash length automatically adapts to file size to minimize collisions:
55
+ Длина хеша автоматически адаптируется к размеру файла для минимизации коллизий:
56
56
 
57
- | File Size | Hash Length | Possible Values |
58
- |-----------|:----------:|:---------------:|
59
- | ≤ 256 lines | 2 hex chars | 256 |
60
- | ≤ 4,096 lines | 3 hex chars | 4,096 |
61
- | > 4,096 lines | 4 hex chars | 65,536 |
57
+ | Размер файла | Длина хеша | Возможных значений |
58
+ |-------------|:----------:|:------------------:|
59
+ | ≤ 256 строк | 2 hex-символа | 256 |
60
+ | ≤ 4 096 строк | 3 hex-символа | 4 096 |
61
+ | > 4 096 строк | 4 hex-символа | 65 536 |
62
62
 
63
- ### 🏷️ Magic Prefix (`#HL `)
63
+ ### 🏷️ Магический префикс (`#HL `)
64
64
 
65
- 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.
65
+ Строки аннотируются настраиваемым префиксом (по умолчанию: `#HL `), чтобы предотвратить ложные срабатывания при удалении хешей. Это гарантирует, что строки данных вроде `1:ab|some data` не будут случайно обрезаны.
66
66
 
67
67
  ```
68
68
  #HL 1:a3|function hello() {
@@ -70,23 +70,23 @@ Lines are annotated with a configurable prefix (default: `#HL `) to prevent fals
70
70
  #HL 3:0e|}
71
71
  ```
72
72
 
73
- The prefix can be customized or disabled for backward compatibility:
73
+ Префикс можно настроить или отключить для обратной совместимости:
74
74
 
75
75
  ```typescript
76
- // Custom prefix
76
+ // Кастомный префикс
77
77
  const hl = createHashline({ prefix: ">> " });
78
78
 
79
- // Disable prefix (legacy format: "1:a3|code")
79
+ // Отключить префикс (legacy-формат: "1:a3|code")
80
80
  const hl = createHashline({ prefix: false });
81
81
  ```
82
82
 
83
- ### 💾 LRU Caching
83
+ ### 💾 LRU-кеширование
84
84
 
85
- 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.
85
+ Встроенный LRU-кеш (`filePath → annotatedContent`) с настраиваемым размером (по умолчанию 100 файлов). При повторном чтении того же файла с неизменённым содержимым возвращается кешированный результат. Кеш автоматически инвалидируется при изменении содержимого файла.
86
86
 
87
- ### ✅ Hash Verification
87
+ ### ✅ Верификация хешей
88
88
 
89
- Verify that a line hasn't changed since it was read protects against race conditions:
89
+ Проверка того, что строка не изменилась с момента чтениязащита от race conditions:
90
90
 
91
91
  ```typescript
92
92
  import { verifyHash } from "opencode-hashline";
@@ -97,65 +97,65 @@ if (!result.valid) {
97
97
  }
98
98
  ```
99
99
 
100
- 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.
100
+ Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде `2:f1` остаётся валидной даже если файл вырос.
101
101
 
102
- ### 🔍 Indentation-Sensitive Hashing
102
+ ### 🔍 Чувствительность к отступам
103
103
 
104
- Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
104
+ Вычисление хеша использует `trimEnd()` (а не `trim()`), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
105
105
 
106
- ### 📐 Range Operations
106
+ ### 📐 Range-операции
107
107
 
108
- Resolve and replace ranges of lines by hash references:
108
+ Резолвинг и замена диапазонов строк по хеш-ссылкам:
109
109
 
110
110
  ```typescript
111
111
  import { resolveRange, replaceRange } from "opencode-hashline";
112
112
 
113
- // Get lines between two hash references
113
+ // Получить строки между двумя хеш-ссылками
114
114
  const range = resolveRange("1:a3f", "3:0e7", content);
115
115
  console.log(range.lines); // ["function hello() {", ' return "world";', "}"]
116
116
 
117
- // Replace a range with new content
117
+ // Заменить диапазон новым содержимым
118
118
  const newContent = replaceRange(
119
119
  "1:a3f", "3:0e7", content,
120
120
  "function goodbye() {\n return 'farewell';\n}"
121
121
  );
122
122
  ```
123
123
 
124
- ### ⚙️ Configurable
124
+ ### ⚙️ Конфигурируемость
125
125
 
126
- Create custom Hashline instances with specific settings:
126
+ Создание кастомных экземпляров Hashline с определёнными настройками:
127
127
 
128
128
  ```typescript
129
129
  import { createHashline } from "opencode-hashline";
130
130
 
131
131
  const hl = createHashline({
132
132
  exclude: ["**/node_modules/**", "**/*.min.js"],
133
- maxFileSize: 512_000, // 512 KB
134
- hashLength: 3, // force 3-char hashes
135
- cacheSize: 200, // cache up to 200 files
136
- prefix: "#HL ", // magic prefix (default)
133
+ maxFileSize: 512_000, // 512 КБ
134
+ hashLength: 3, // принудительно 3-символьные хеши
135
+ cacheSize: 200, // кешировать до 200 файлов
136
+ prefix: "#HL ", // магический префикс (по умолчанию)
137
137
  });
138
138
 
139
- // Use the configured instance
139
+ // Использование настроенного экземпляра
140
140
  const annotated = hl.formatFileWithHashes(content, "src/app.ts");
141
141
  const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
142
142
  ```
143
143
 
144
- #### Configuration Options
144
+ #### Параметры конфигурации
145
145
 
146
- | Option | Type | Default | Description |
147
- |--------|------|---------|-------------|
148
- | `exclude` | `string[]` | See below | Glob patterns for files to skip |
149
- | `maxFileSize` | `number` | `1_000_000` | Max file size in bytes |
150
- | `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
151
- | `cacheSize` | `number` | `100` | Max files in LRU cache |
152
- | `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
146
+ | Параметр | Тип | По умолчанию | Описание |
147
+ |----------|-----|:------------:|----------|
148
+ | `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
149
+ | `maxFileSize` | `number` | `1_000_000` | Макс. размер файла в байтах |
150
+ | `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
151
+ | `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
152
+ | `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
153
153
 
154
- Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
154
+ Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
155
155
 
156
156
  ---
157
157
 
158
- ## 📦 Installation
158
+ ## 📦 Установка
159
159
 
160
160
  ```bash
161
161
  npm install opencode-hashline
@@ -163,9 +163,9 @@ npm install opencode-hashline
163
163
 
164
164
  ---
165
165
 
166
- ## 🔧 Configuration
166
+ ## 🔧 Конфигурация
167
167
 
168
- Add the plugin to your `opencode.json`:
168
+ Добавьте плагин в ваш `opencode.json`:
169
169
 
170
170
  ```json
171
171
  {
@@ -174,17 +174,17 @@ Add the plugin to your `opencode.json`:
174
174
  }
175
175
  ```
176
176
 
177
- ### Configuration Files
177
+ ### Файлы конфигурации
178
178
 
179
- The plugin loads configuration from the following locations (in priority order, later overrides earlier):
179
+ Плагин загружает конфигурацию из следующих мест порядке приоритета, более поздние перезаписывают ранние):
180
180
 
181
- | Priority | Location | Scope |
182
- |:--------:|----------|-------|
183
- | 1 | `~/.config/opencode/opencode-hashline.json` | Global (all projects) |
184
- | 2 | `<project>/opencode-hashline.json` | Project-local |
185
- | 3 | Programmatic config via `createHashlinePlugin()` | Factory argument |
181
+ | Приоритет | Расположение | Область |
182
+ |:---------:|-------------|---------|
183
+ | 1 | `~/.config/opencode/opencode-hashline.json` | Глобальная (все проекты) |
184
+ | 2 | `<project>/opencode-hashline.json` | Локальная (проект) |
185
+ | 3 | Программная конфигурация через `createHashlinePlugin()` | Аргумент фабрики |
186
186
 
187
- Example `opencode-hashline.json`:
187
+ Пример `opencode-hashline.json`:
188
188
 
189
189
  ```json
190
190
  {
@@ -196,73 +196,48 @@ Example `opencode-hashline.json`:
196
196
  }
197
197
  ```
198
198
 
199
- That's it! The plugin automatically:
199
+ Вот и всё! Плагин автоматически:
200
200
 
201
- | # | Action | Description |
202
- |:-:|--------|-------------|
203
- | 1 | 📝 **Annotates file reads** | When the AI reads a file, each line gets a `#HL` hash prefix |
204
- | 2 | 📎 **Annotates `@file` mentions** | Files attached via `@filename` in prompts are also annotated with hashlines |
205
- | 3 | ✂️ **Strips hash prefixes on edits** | When the AI writes/edits a file, hash prefixes are removed before applying changes |
206
- | 4 | 🧠 **Injects system prompt instructions** | The AI is told how to interpret and use hashline references |
207
- | 5 | 💾 **Caches results** | Repeated reads of the same file return cached annotations |
208
- | 6 | 🔍 **Filters by tool** | Only file-reading tools (e.g. `read_file`, `cat`, `view`) get annotations; other tools are left untouched |
209
- | 7 | ⚙️ **Respects config** | Excluded files and files exceeding `maxFileSize` are skipped |
210
- | 8 | 🧩 **Registers `hashline_edit` tool** | Applies replace/delete/insert by hash references, without exact `old_string` matching |
201
+ | # | Действие | Описание |
202
+ |:-:|----------|----------|
203
+ | 1 | 📝 **Аннотирует чтение файлов** | При чтении файла AI каждая строка получает `#HL` хеш-префикс |
204
+ | 2 | 📎 **Аннотирует `@file` упоминания** | Файлы, прикреплённые через `@filename` в промпте, тоже аннотируются хешлайнами |
205
+ | 3 | ✂️ **Убирает хеш-префиксы при редактировании** | При записи/редактировании файла хеш-префиксы удаляются перед применением изменений |
206
+ | 4 | 🧠 **Внедряет инструкции в системный промпт** | AI получает инструкции по интерпретации и использованию hashline-ссылок |
207
+ | 5 | 💾 **Кеширует результаты** | Повторные чтения того же файла возвращают кешированные аннотации |
208
+ | 6 | 🔍 **Фильтрует по инструменту** | Только инструменты чтения файлов (например `read_file`, `cat`, `view`) получают аннотации; остальные не затрагиваются |
209
+ | 7 | ⚙️ **Учитывает конфигурацию** | Исключённые файлы и файлы, превышающие `maxFileSize`, пропускаются |
210
+ | 8 | 🧩 **Регистрирует `hashline_edit` tool** | Применяет replace/delete/insert по hash-ссылкам без точного `old_string`-матчинга |
211
211
 
212
212
  ---
213
213
 
214
- ## 🛠️ How It Works
214
+ ## 🛠️ Как это работает
215
215
 
216
- ### Hash Computation
216
+ ### Вычисление хеша
217
217
 
218
- Each line's hash is computed from:
219
- - The **0-based line index**
220
- - The **trimEnd'd line content**leading whitespace (indentation) IS significant
218
+ Хеш каждой строки вычисляется из:
219
+ - **0-based индекса** строки
220
+ - **Содержимого строки** с обрезанными завершающими пробелами (trimEnd) ведущие пробелы (отступы) ЗНАЧИМЫ
221
221
 
222
- This is fed through an **FNV-1a** hash function, reduced to the appropriate modulus based on file size, and rendered as a hex string.
222
+ Это подаётся в хеш-функцию **FNV-1a**, сводится к соответствующему модулю в зависимости от размера файла и отображается как hex-строка.
223
223
 
224
- ### Plugin Hooks & Tool
224
+ ### Хуки и tool плагина
225
225
 
226
- The plugin registers four OpenCode hooks and one custom tool:
226
+ Плагин регистрирует четыре хука OpenCode и один кастомный tool:
227
227
 
228
- | Hook | Purpose |
229
- |------|---------|
230
- | `tool.hashline_edit` | Hash-aware edits by references like `5:a3f` or `#HL 5:a3f|...` |
231
- | `tool.execute.after` | Injects hashline annotations into file-read tool output |
232
- | `tool.execute.before` | Strips hashline prefixes from file-edit tool arguments |
233
- | `chat.message` | Annotates `@file` mentions in user messages (writes annotated content to a temp file and swaps the URL) |
234
- | `experimental.chat.system.transform` | Adds hashline usage instructions to the system prompt |
228
+ | Хук | Назначение |
229
+ |-----|-----------|
230
+ | `tool.hashline_edit` | Hash-aware правки по ссылкам вроде `5:a3f` или `#HL 5:a3f|...` |
231
+ | `tool.execute.after` | Добавляет hashline-аннотации в вывод инструментов чтения файлов |
232
+ | `tool.execute.before` | Убирает hashline-префиксы из аргументов инструментов редактирования |
233
+ | `chat.message` | Аннотирует `@file` упоминания в сообщениях пользователя (записывает аннотированный контент во временный файл и подменяет URL) |
234
+ | `experimental.chat.system.transform` | Добавляет инструкции по использованию hashline в системный промпт |
235
235
 
236
- ### Tool Detection Heuristic (`isFileReadTool`)
237
-
238
- 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:
239
-
240
- **Exact match** — the tool name (case-insensitive) is compared against the allow-list:
241
- - `read`, `file_read`, `read_file`, `cat`, `view`
242
-
243
- **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.
244
-
245
- **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.
246
-
247
- **How to customize:**
248
- - Name your custom tool to match one of the patterns above (e.g. `my_read_file`)
249
- - Include `path`, `filePath`, or `file` in its arguments
250
- - Or extend the `FILE_READ_TOOLS` list in a fork
251
-
252
- The `isFileReadTool()` function is exported for testing and advanced usage:
253
-
254
- ```typescript
255
- import { isFileReadTool } from "opencode-hashline";
256
-
257
- isFileReadTool("read_file"); // true
258
- isFileReadTool("mcp.read"); // true
259
- isFileReadTool("custom_reader", { path: "app.ts" }); // true (heuristic)
260
- isFileReadTool("file_write", { path: "app.ts" }); // false (write indicator)
261
- ```
236
+ ---
262
237
 
263
- ### Programmatic API
238
+ ## 🔌 Программный API
264
239
 
265
- 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):
240
+ Основные утилиты экспортируются из субпути `opencode-hashline/utils` (чтобы избежать конфликтов с загрузчиком плагинов OpenCode, который вызывает каждый экспорт как функцию Plugin):
266
241
 
267
242
  ```typescript
268
243
  import {
@@ -286,161 +261,161 @@ import {
286
261
  } from "opencode-hashline/utils";
287
262
  ```
288
263
 
289
- ### Core Functions
264
+ ### Основные функции
290
265
 
291
266
  ```typescript
292
- // Compute hash for a single line
293
- const hash = computeLineHash(0, "function hello() {"); // e.g. "a3f"
267
+ // Вычислить хеш для одной строки
268
+ const hash = computeLineHash(0, "function hello() {"); // например "a3f"
294
269
 
295
- // Compute hash with specific length
296
- const hash4 = computeLineHash(0, "function hello() {", 4); // e.g. "a3f2"
270
+ // Вычислить хеш с определённой длиной
271
+ const hash4 = computeLineHash(0, "function hello() {", 4); // например "a3f2"
297
272
 
298
- // Annotate entire file content (adaptive hash length, with #HL prefix)
273
+ // Аннотировать содержимое файла (адаптивная длина хеша, с префиксом #HL)
299
274
  const annotated = formatFileWithHashes(fileContent);
300
275
  // "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"
301
276
 
302
- // Annotate with specific hash length
277
+ // Аннотировать с определённой длиной хеша
303
278
  const annotated3 = formatFileWithHashes(fileContent, 3);
304
279
 
305
- // Annotate without prefix (legacy format)
280
+ // Аннотировать без префикса (legacy-формат)
306
281
  const annotatedLegacy = formatFileWithHashes(fileContent, undefined, false);
307
282
 
308
- // Strip annotations to get original content
283
+ // Убрать аннотации, получить оригинальное содержимое
309
284
  const original = stripHashes(annotated);
310
285
  ```
311
286
 
312
- ### Hash References & Verification
287
+ ### Хеш-ссылки и верификация
313
288
 
314
289
  ```typescript
315
- // Parse a hash reference
290
+ // Разобрать хеш-ссылку
316
291
  const { line, hash } = parseHashRef("2:f1c"); // { line: 2, hash: "f1c" }
317
292
 
318
- // Normalize from an annotated line
293
+ // Нормализовать ссылку из аннотированной строки
319
294
  const ref = normalizeHashRef("#HL 2:f1c|const x = 1;"); // "2:f1c"
320
295
 
321
- // Build a lookup map
296
+ // Построить карту соответствий
322
297
  const map = buildHashMap(fileContent); // Map<"2:f1c", 2>
323
298
 
324
- // Verify a hash reference (uses hash.length, not file size)
299
+ // Верифицировать хеш-ссылку (использует hash.length, а не размер файла)
325
300
  const result = verifyHash(2, "f1c", fileContent);
326
301
  ```
327
302
 
328
- ### Range Operations
303
+ ### Range-операции
329
304
 
330
305
  ```typescript
331
- // Resolve a range
306
+ // Резолвить диапазон
332
307
  const range = resolveRange("1:a3f", "3:0e7", fileContent);
333
308
 
334
- // Replace a range
335
- const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "new content");
309
+ // Заменить диапазон
310
+ const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "новое содержимое");
336
311
 
337
- // Hash-aware edit operation (replace/delete/insert_before/insert_after)
312
+ // Hash-aware операция редактирования (replace/delete/insert_before/insert_after)
338
313
  const edited = applyHashEdit(
339
- { operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "new content" },
314
+ { operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "новое содержимое" },
340
315
  fileContent
341
316
  ).content;
342
317
  ```
343
318
 
344
- ### Utilities
319
+ ### Утилиты
345
320
 
346
321
  ```typescript
347
- // Check if a file should be excluded
322
+ // Проверить, нужно ли исключить файл
348
323
  const excluded = shouldExclude("node_modules/foo.js", ["**/node_modules/**"]);
349
324
 
350
- // Create a configured instance
325
+ // Создать настроенный экземпляр
351
326
  const hl = createHashline({ cacheSize: 50, hashLength: 3 });
352
327
  ```
353
328
 
354
329
  ---
355
330
 
356
- ## 📊 Benchmark
331
+ ## 📊 Бенчмарк
357
332
 
358
- ### Correctness: hashline vs str_replace
333
+ ### Корректность: hashline vs str_replace
359
334
 
360
- 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.):
335
+ Оба подхода протестированы на **60 фикстурах из [react-edit-benchmark](https://github.com/can1357/oh-my-pi/tree/main/packages/react-edit-benchmark)** — мутированных файлах React с известными багами (инвертированные булевы, перепутанные операторы, удалённые guard-клаузы и т.д.):
361
336
 
362
337
  | | hashline | str_replace |
363
338
  |---|:---:|:---:|
364
- | **Passed** | **60/60 (100%)** | 58/60 (96.7%) |
365
- | **Failed** | 0 | 2 |
366
- | **Ambiguous edits** | 0 | 4 |
339
+ | **Прошло** | **60/60 (100%)** | 58/60 (96.7%) |
340
+ | **Провалено** | 0 | 2 |
341
+ | **Неоднозначные правки** | 0 | 4 |
367
342
 
368
- 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.
343
+ str_replace ломается, когда `old_string` встречается в файле несколько раз (например, повторяющиеся guard-клаузы, похожие блоки кода). Hashline адресует каждую строку уникально через `lineNumber:hash`, поэтому неоднозначность исключена.
369
344
 
370
345
  ```bash
371
- # Run yourself:
372
- npx tsx benchmark/run.ts # hashline mode
373
- npx tsx benchmark/run.ts --no-hash # str_replace mode
346
+ # Запустите сами:
347
+ npx tsx benchmark/run.ts # режим hashline
348
+ npx tsx benchmark/run.ts --no-hash # режим str_replace
374
349
  ```
375
350
 
376
351
  <details>
377
- <summary>str_replace failures (structural category)</summary>
352
+ <summary>Ошибки str_replace (категория structural)</summary>
378
353
 
379
- - `structural-remove-early-return-001` — `old_string` matched multiple locations, wrong one replaced
380
- - `structural-remove-early-return-002` — same issue
381
- - `structural-delete-statement-002` — ambiguous match (first match happened to be correct)
382
- - `structural-delete-statement-003` — ambiguous match (first match happened to be correct)
354
+ - `structural-remove-early-return-001` — `old_string` совпал в нескольких местах, замена применена не к тому
355
+ - `structural-remove-early-return-002` — аналогичная проблема
356
+ - `structural-delete-statement-002` — неоднозначное совпадение (первое совпадение оказалось верным)
357
+ - `structural-delete-statement-003` — неоднозначное совпадение (первое совпадение оказалось верным)
383
358
 
384
359
  </details>
385
360
 
386
- ### Token Overhead
361
+ ### Расход токенов
387
362
 
388
- Hashline annotations add `#HL <line>:<hash>|` prefix (~12 chars / ~3 tokens) per line:
363
+ Аннотации hashline добавляют префикс `#HL <line>:<hash>|` (~12 символов / ~3 токена) на строку:
389
364
 
390
- | | Plain | Annotated | Overhead |
365
+ | | Без хешей | С хешами | Оверхед |
391
366
  |---|---:|---:|:---:|
392
- | **Characters** | 404K | 564K | +40% |
393
- | **Tokens (~)** | ~101K | ~141K | +40% |
367
+ | **Символы** | 404K | 564K | +40% |
368
+ | **Токены (~)** | ~101K | ~141K | +40% |
394
369
 
395
- Overhead is stable at ~40% regardless of file size. For a typical 200-line file (~800 tokens), hashline adds ~600 tokensnegligible in a 200K context window.
370
+ Оверхед стабильно ~40% независимо от размера файла. Для типичного файла на 200 строк (~800 токенов) hashline добавляет ~600 токеновпренебрежимо мало при контекстном окне в 200K.
396
371
 
397
- ### Performance
372
+ ### Производительность
398
373
 
399
- | File Size | Annotate | Edit | Strip |
400
- |----------:|:--------:|:----:|:-----:|
401
- | **10** lines | 0.05 ms | 0.01 ms | 0.03 ms |
402
- | **100** lines | 0.12 ms | 0.02 ms | 0.08 ms |
403
- | **1,000** lines | 0.95 ms | 0.04 ms | 0.60 ms |
404
- | **5,000** lines | 4.50 ms | 0.08 ms | 2.80 ms |
405
- | **10,000** lines | 9.20 ms | 0.10 ms | 5.50 ms |
374
+ | Размер файла | Аннотация | Правка | Удаление хешей |
375
+ |-------------:|:---------:|:------:|:--------------:|
376
+ | **10** строк | 0.05 мс | 0.01 мс | 0.03 мс |
377
+ | **100** строк | 0.12 мс | 0.02 мс | 0.08 мс |
378
+ | **1 000** строк | 0.95 мс | 0.04 мс | 0.60 мс |
379
+ | **5 000** строк | 4.50 мс | 0.08 мс | 2.80 мс |
380
+ | **10 000** строк | 9.20 мс | 0.10 мс | 5.50 мс |
406
381
 
407
- > A typical 1,000-line source file is annotated in **< 1ms** imperceptible to the user.
382
+ > Типичный файл из 1 000 строк аннотируется за **< 1 мс**незаметно для пользователя.
408
383
 
409
384
  ---
410
385
 
411
- ## 🧑‍💻 Development
386
+ ## 🧑‍💻 Разработка
412
387
 
413
388
  ```bash
414
- # Install dependencies
389
+ # Установить зависимости
415
390
  npm install
416
391
 
417
- # Run tests
392
+ # Запустить тесты
418
393
  npm test
419
394
 
420
- # Build
395
+ # Собрать
421
396
  npm run build
422
397
 
423
- # Type check
398
+ # Проверка типов
424
399
  npm run typecheck
425
400
  ```
426
401
 
427
402
  ---
428
403
 
429
- ## 💡 Inspiration & Background
404
+ ## 💡 Вдохновение и теоретическая база
430
405
 
431
- 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."
406
+ Идея hashline вдохновлена концепциями из **oh-my-pi** от [can1357](https://github.com/can1357/oh-my-pi) — AI-тулкита для разработки (coding agent CLI, unified LLM API, TUI-библиотеки) — и статьи «The Harness Problem» (проблема обвязки).
432
407
 
433
- **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).
408
+ **Суть проблемы:** современные AI-модели обладают огромными возможностями, но инструменты (harness), которые передают модели контекст и применяют её правки к файлам, теряют информацию и порождают ошибки. Модель видит содержимое файла, но при редактировании вынуждена «угадывать» контекст окружающих строк. Search-and-replace ломается на дубликатах строк, а diff-формат тоже ненадёжен на практике.
434
409
 
435
- 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.
410
+ Hashline решает эту проблему, присваивая каждой строке короткий детерминированный хеш-тег (например, `2:f1c`), что делает адресацию строк **точной и однозначной**. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
436
411
 
437
- **References:**
438
- - [oh-my-pi by can1357](https://github.com/can1357/oh-my-pi) — AI coding agent toolkit: coding agent CLI, unified LLM API, TUI libraries
439
- - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — blog post describing the problem in detail
440
- - [Описание подхода на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — overview of the approach in Russian
412
+ **Ссылки:**
413
+ - [oh-my-pi от can1357](https://github.com/can1357/oh-my-pi) — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
414
+ - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — блог-пост с подробным описанием проблемы
415
+ - [Статья на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — описание подхода на русском языке
441
416
 
442
417
  ---
443
418
 
444
- ## 📄 License
419
+ ## 📄 Лицензия
445
420
 
446
421
  [MIT](LICENSE) © opencode-hashline contributors
package/README.ru.md CHANGED
@@ -14,7 +14,7 @@
14
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
15
15
  [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
16
16
 
17
- **🇷🇺 Русский** | [🇬🇧 English](README.md)
17
+ **🇷🇺 Русский** | [🇬🇧 English](README.en.md)
18
18
 
19
19
  <br />
20
20
 
@@ -452,13 +452,13 @@ var init_hashline = __esm({
452
452
  });
453
453
 
454
454
  // src/index.ts
455
- var index_exports = {};
456
- __export(index_exports, {
455
+ var src_exports = {};
456
+ __export(src_exports, {
457
457
  HashlinePlugin: () => HashlinePlugin,
458
458
  createHashlinePlugin: () => createHashlinePlugin,
459
- default: () => index_default
459
+ default: () => src_default
460
460
  });
461
- module.exports = __toCommonJS(index_exports);
461
+ module.exports = __toCommonJS(src_exports);
462
462
  var import_fs3 = require("fs");
463
463
  var import_path3 = require("path");
464
464
  var import_os2 = require("os");
@@ -899,7 +899,7 @@ function createHashlinePlugin(userConfig) {
899
899
  };
900
900
  }
901
901
  var HashlinePlugin = createHashlinePlugin();
902
- var index_default = HashlinePlugin;
902
+ var src_default = HashlinePlugin;
903
903
  // Annotate the CommonJS export names for ESM import in node:
904
904
  0 && (module.exports = {
905
905
  HashlinePlugin,
@@ -237,9 +237,9 @@ function createHashlinePlugin(userConfig) {
237
237
  };
238
238
  }
239
239
  var HashlinePlugin = createHashlinePlugin();
240
- var index_default = HashlinePlugin;
240
+ var src_default = HashlinePlugin;
241
241
  export {
242
242
  HashlinePlugin,
243
243
  createHashlinePlugin,
244
- index_default as default
244
+ src_default as default
245
245
  };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "opencode-hashline",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing",
5
- "main": "dist/index.cjs",
6
- "module": "dist/index.js",
7
- "types": "dist/index.d.ts",
5
+ "main": "dist/opencode-hashline.cjs",
6
+ "module": "dist/opencode-hashline.js",
7
+ "types": "dist/opencode-hashline.d.ts",
8
8
  "type": "module",
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
12
  "exports": {
13
13
  ".": {
14
- "types": "./dist/index.d.ts",
15
- "import": "./dist/index.js",
16
- "require": "./dist/index.cjs"
14
+ "types": "./dist/opencode-hashline.d.ts",
15
+ "import": "./dist/opencode-hashline.js",
16
+ "require": "./dist/opencode-hashline.cjs"
17
17
  },
18
18
  "./utils": {
19
19
  "types": "./dist/utils.d.ts",
@@ -41,7 +41,7 @@
41
41
  "license": "MIT",
42
42
  "peerDependencies": {
43
43
  "@opencode-ai/plugin": "^1.2.2",
44
- "zod": "^3.0.0"
44
+ "zod": "^4.0.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@opencode-ai/plugin": "^1.2.2",
File without changes
File without changes