opencode-hashline 1.1.3 → 1.3.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 +67 -11
- package/README.md +67 -11
- package/dist/{chunk-VPCMHCTB.js → chunk-7KUPGN4M.js} +25 -3
- package/dist/{chunk-I6RACR3D.js → chunk-DOR4YDIS.js} +242 -43
- package/dist/{hashline-yhMw1Abs.d.ts → hashline-A7k2yn3G.d.cts} +70 -5
- package/dist/{hashline-yhMw1Abs.d.cts → hashline-A7k2yn3G.d.ts} +70 -5
- package/dist/{hashline-5PFAXY3H.js → hashline-MGDEWZ77.js} +11 -1
- package/dist/opencode-hashline.cjs +285 -50
- package/dist/opencode-hashline.d.cts +2 -2
- package/dist/opencode-hashline.d.ts +2 -2
- package/dist/opencode-hashline.js +22 -7
- package/dist/utils.cjs +271 -45
- package/dist/utils.d.cts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +12 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -44,7 +44,12 @@ The AI model can then reference lines by their hash tags for precise editing:
|
|
|
44
44
|
|
|
45
45
|
### 🤔 Why does this help?
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Hashline solves the fundamental problems of the two existing AI file-editing approaches:
|
|
48
|
+
|
|
49
|
+
- **`str_replace`** requires an absolutely exact match of `old_string`. Any extra whitespace, wrong indentation, or duplicate lines in the file — and the edit fails with "String to replace not found". This is so common it has a [mega-thread of 27+ related issues on GitHub](https://github.com/anthropics/claude-code/issues).
|
|
50
|
+
- **`apply_patch`** (unified diff) only works on models specifically trained for this format. On other models the results are catastrophic: Grok 4 fails **50.7%** of patches, GLM-4.7 fails **46.2%** ([source](https://habr.com/ru/companies/bothub/news/995986/)).
|
|
51
|
+
|
|
52
|
+
Hashline addresses each line with a unique `lineNumber:hash`. No string matching, no model-specific training dependency — just precise, verifiable line addressing.
|
|
48
53
|
|
|
49
54
|
---
|
|
50
55
|
|
|
@@ -99,6 +104,49 @@ if (!result.valid) {
|
|
|
99
104
|
|
|
100
105
|
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
106
|
|
|
107
|
+
### 🔒 File Revision (`fileRev`)
|
|
108
|
+
|
|
109
|
+
In addition to per-line hashes, hashline computes a whole-file hash (FNV-1a, 8 hex chars). It's prepended as the first annotation line:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
#HL REV:72c4946c
|
|
113
|
+
#HL 1:a3f|function hello() {
|
|
114
|
+
#HL 2:f1c| return "world";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pass `fileRev` to `hashline_edit` when editing — if the file changed since it was read, the edit is rejected with `FILE_REV_MISMATCH`.
|
|
118
|
+
|
|
119
|
+
### 🔄 Safe Reapply
|
|
120
|
+
|
|
121
|
+
If a line moved (e.g., due to insertions above), `safeReapply` finds it by content hash:
|
|
122
|
+
|
|
123
|
+
- **1 candidate** — edit applies at the new position
|
|
124
|
+
- **>1 candidates** — `AMBIGUOUS_REAPPLY` error (ambiguous)
|
|
125
|
+
- **0 candidates** — `HASH_MISMATCH` error
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const result = applyHashEdit(
|
|
129
|
+
{ operation: "replace", startRef: "1:a3f", replacement: "new" },
|
|
130
|
+
content,
|
|
131
|
+
undefined,
|
|
132
|
+
true, // safeReapply
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 🏷️ Structured Errors
|
|
137
|
+
|
|
138
|
+
All hashline errors are instances of `HashlineError` with error codes, diagnostics, and hints:
|
|
139
|
+
|
|
140
|
+
| Code | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `HASH_MISMATCH` | Line content changed since last read |
|
|
143
|
+
| `FILE_REV_MISMATCH` | File was modified since last read |
|
|
144
|
+
| `AMBIGUOUS_REAPPLY` | Multiple candidates found during safe reapply |
|
|
145
|
+
| `TARGET_OUT_OF_RANGE` | Line number exceeds file length |
|
|
146
|
+
| `INVALID_REF` | Malformed hash reference |
|
|
147
|
+
| `INVALID_RANGE` | Start line is after end line |
|
|
148
|
+
| `MISSING_REPLACEMENT` | Replace/insert operation without content |
|
|
149
|
+
|
|
102
150
|
### 🔍 Indentation-Sensitive Hashing
|
|
103
151
|
|
|
104
152
|
Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
|
|
@@ -150,6 +198,8 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
|
|
|
150
198
|
| `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
|
|
151
199
|
| `cacheSize` | `number` | `100` | Max files in LRU cache |
|
|
152
200
|
| `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
|
|
201
|
+
| `fileRev` | `boolean` | `true` | Include file revision hash (`#HL REV:...`) in annotations |
|
|
202
|
+
| `safeReapply` | `boolean` | `false` | Auto-relocate moved lines by content hash |
|
|
153
203
|
|
|
154
204
|
Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
|
|
155
205
|
|
|
@@ -355,22 +405,25 @@ const hl = createHashline({ cacheSize: 50, hashLength: 3 });
|
|
|
355
405
|
|
|
356
406
|
## 📊 Benchmark
|
|
357
407
|
|
|
358
|
-
### Correctness: hashline vs str_replace
|
|
408
|
+
### Correctness: hashline vs str_replace vs apply_patch
|
|
359
409
|
|
|
360
|
-
We tested
|
|
410
|
+
We tested all three 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
411
|
|
|
362
|
-
| | hashline | str_replace |
|
|
363
|
-
|
|
364
|
-
| **Passed** | **60/60 (100%)** | 58/60 (96.7%) |
|
|
365
|
-
| **Failed** | 0 | 2 |
|
|
366
|
-
| **Ambiguous edits** | 0 | 4 |
|
|
412
|
+
| | hashline | str_replace | apply_patch |
|
|
413
|
+
|---|:---:|:---:|:---:|
|
|
414
|
+
| **Passed** | **60/60 (100%)** | 58/60 (96.7%) | **60/60 (100%)** |
|
|
415
|
+
| **Failed** | 0 | 2 | 0 |
|
|
416
|
+
| **Ambiguous edits** | 0 | 4 | 0 |
|
|
367
417
|
|
|
368
|
-
|
|
418
|
+
`apply_patch` with context lines matches hashline's reliability — **when the model generates the patch correctly**. The key weakness of `apply_patch` is its dependency on model-specific training: models not trained on this format produce malformed diffs (missing context lines, wrong indentation), causing patch application to fail.
|
|
419
|
+
|
|
420
|
+
`str_replace` fails when `old_string` appears multiple times in the file (repeated guard clauses, similar code blocks). Hashline addresses each line uniquely via `lineNumber:hash` — ambiguity is impossible and no model-specific format is required.
|
|
369
421
|
|
|
370
422
|
```bash
|
|
371
423
|
# Run yourself:
|
|
372
|
-
npx tsx benchmark/run.ts
|
|
373
|
-
npx tsx benchmark/run.ts --no-hash
|
|
424
|
+
npx tsx benchmark/run.ts # hashline mode
|
|
425
|
+
npx tsx benchmark/run.ts --no-hash # str_replace mode
|
|
426
|
+
npx tsx benchmark/run.ts --apply-patch # apply_patch mode
|
|
374
427
|
```
|
|
375
428
|
|
|
376
429
|
<details>
|
|
@@ -434,9 +487,12 @@ The idea behind hashline is inspired by concepts from **oh-my-pi** by [can1357](
|
|
|
434
487
|
|
|
435
488
|
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
489
|
|
|
490
|
+
The advanced features — **file revision** (`fileRev`), **safe reapply**, and **structured errors** — are inspired by the hash-based editing implementation in **AssistAgents** by [OzeroHAX](https://github.com/OzeroHAX/AssistAgents), which independently applied a similar approach for OpenCode with additional integrity checks and error diagnostics.
|
|
491
|
+
|
|
437
492
|
**References:**
|
|
438
493
|
- [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
494
|
- [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — blog post describing the problem in detail
|
|
495
|
+
- [AssistAgents by OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing for OpenCode with file revision, safe reapply, and structured conflicts
|
|
440
496
|
- [Описание подхода на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — overview of the approach in Russian
|
|
441
497
|
|
|
442
498
|
---
|
package/README.md
CHANGED
|
@@ -44,7 +44,12 @@ AI-модель может ссылаться на строки по их хеш
|
|
|
44
44
|
|
|
45
45
|
### 🤔 Почему это помогает?
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Hashline решает фундаментальные проблемы двух существующих подходов к редактированию файлов AI:
|
|
48
|
+
|
|
49
|
+
- **`str_replace`** требует абсолютно точного совпадения `old_string`. Любой лишний пробел, неверный отступ или дублирующиеся строки в файле — и редактирование завершается ошибкой «String to replace not found». Это настолько распространённая проблема, что у неё есть [мегатред на 27+ тикетов на GitHub](https://github.com/anthropics/claude-code/issues).
|
|
50
|
+
- **`apply_patch`** (unified diff) работает только на моделях, специально обученных этому формату. На других моделях результаты катастрофические: Grok 4 проваливает **50.7%** патчей, GLM-4.7 — **46.2%** ([источник](https://habr.com/ru/companies/bothub/news/995986/)).
|
|
51
|
+
|
|
52
|
+
Hashline адресует каждую строку уникальным хешем `lineNumber:hash`. Никакого строкового совпадения, никакой зависимости от специального обучения модели — только точная, верифицируемая адресация.
|
|
48
53
|
|
|
49
54
|
---
|
|
50
55
|
|
|
@@ -99,6 +104,49 @@ if (!result.valid) {
|
|
|
99
104
|
|
|
100
105
|
Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде `2:f1` остаётся валидной даже если файл вырос.
|
|
101
106
|
|
|
107
|
+
### 🔒 Ревизия файла (`fileRev`)
|
|
108
|
+
|
|
109
|
+
Помимо построчных хешей, hashline вычисляет хеш всего файла (FNV-1a, 8 hex-символов). Он добавляется первой строкой аннотации:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
#HL REV:72c4946c
|
|
113
|
+
#HL 1:a3f|function hello() {
|
|
114
|
+
#HL 2:f1c| return "world";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
При редактировании передайте `fileRev` в `hashline_edit` — если файл изменился с момента чтения, правка будет отклонена с ошибкой `FILE_REV_MISMATCH`.
|
|
118
|
+
|
|
119
|
+
### 🔄 Safe Reapply
|
|
120
|
+
|
|
121
|
+
Если строка переместилась (например, из-за вставки строк выше), `safeReapply` находит её по хешу контента:
|
|
122
|
+
|
|
123
|
+
- **1 кандидат** — правка применяется к новой позиции
|
|
124
|
+
- **>1 кандидатов** — ошибка `AMBIGUOUS_REAPPLY` (неоднозначность)
|
|
125
|
+
- **0 кандидатов** — ошибка `HASH_MISMATCH`
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const result = applyHashEdit(
|
|
129
|
+
{ operation: "replace", startRef: "1:a3f", replacement: "new" },
|
|
130
|
+
content,
|
|
131
|
+
undefined,
|
|
132
|
+
true, // safeReapply
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 🏷️ Structured Errors
|
|
137
|
+
|
|
138
|
+
Все ошибки hashline — экземпляры `HashlineError` с кодом, диагностикой и подсказками:
|
|
139
|
+
|
|
140
|
+
| Код | Описание |
|
|
141
|
+
|-----|----------|
|
|
142
|
+
| `HASH_MISMATCH` | Содержимое строки изменилось |
|
|
143
|
+
| `FILE_REV_MISMATCH` | Файл модифицирован с момента чтения |
|
|
144
|
+
| `AMBIGUOUS_REAPPLY` | Несколько кандидатов при safe reapply |
|
|
145
|
+
| `TARGET_OUT_OF_RANGE` | Номер строки за пределами файла |
|
|
146
|
+
| `INVALID_REF` | Некорректная хеш-ссылка |
|
|
147
|
+
| `INVALID_RANGE` | Начало диапазона после конца |
|
|
148
|
+
| `MISSING_REPLACEMENT` | Операция replace/insert без содержимого |
|
|
149
|
+
|
|
102
150
|
### 🔍 Чувствительность к отступам
|
|
103
151
|
|
|
104
152
|
Вычисление хеша использует `trimEnd()` (а не `trim()`), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
|
|
@@ -150,6 +198,8 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
|
|
|
150
198
|
| `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
|
|
151
199
|
| `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
|
|
152
200
|
| `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
|
|
201
|
+
| `fileRev` | `boolean` | `true` | Включать ревизию файла (`#HL REV:...`) в аннотации |
|
|
202
|
+
| `safeReapply` | `boolean` | `false` | Автоматический поиск перемещённых строк по хешу |
|
|
153
203
|
|
|
154
204
|
Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
|
|
155
205
|
|
|
@@ -330,22 +380,25 @@ const hl = createHashline({ cacheSize: 50, hashLength: 3 });
|
|
|
330
380
|
|
|
331
381
|
## 📊 Бенчмарк
|
|
332
382
|
|
|
333
|
-
### Корректность: hashline vs str_replace
|
|
383
|
+
### Корректность: hashline vs str_replace vs apply_patch
|
|
334
384
|
|
|
335
|
-
|
|
385
|
+
Все три подхода протестированы на **60 фикстурах из [react-edit-benchmark](https://github.com/can1357/oh-my-pi/tree/main/packages/react-edit-benchmark)** — мутированных файлах React с известными багами (инвертированные булевы, перепутанные операторы, удалённые guard-клаузы и т.д.):
|
|
336
386
|
|
|
337
|
-
| | hashline | str_replace |
|
|
338
|
-
|
|
339
|
-
| **Прошло** | **60/60 (100%)** | 58/60 (96.7%) |
|
|
340
|
-
| **Провалено** | 0 | 2 |
|
|
341
|
-
| **Неоднозначные правки** | 0 | 4 |
|
|
387
|
+
| | hashline | str_replace | apply_patch |
|
|
388
|
+
|---|:---:|:---:|:---:|
|
|
389
|
+
| **Прошло** | **60/60 (100%)** | 58/60 (96.7%) | **60/60 (100%)** |
|
|
390
|
+
| **Провалено** | 0 | 2 | 0 |
|
|
391
|
+
| **Неоднозначные правки** | 0 | 4 | 0 |
|
|
342
392
|
|
|
343
|
-
|
|
393
|
+
`apply_patch` с контекстными строками работает так же надёжно, как hashline — **при условии, что модель правильно генерирует патч**. Слабое место `apply_patch` — зависимость от обучения конкретной модели: не обученные под этот формат модели производят некорректные diff-ы (пропускают контекст, путают отступы), что приводит к провалу применения патча.
|
|
394
|
+
|
|
395
|
+
`str_replace` ломается, когда `old_string` встречается в файле несколько раз (повторяющиеся guard-клаузы, похожие блоки кода). Hashline адресует каждую строку уникально через `lineNumber:hash` — неоднозначность исключена, модельный формат не нужен.
|
|
344
396
|
|
|
345
397
|
```bash
|
|
346
398
|
# Запустите сами:
|
|
347
|
-
npx tsx benchmark/run.ts
|
|
348
|
-
npx tsx benchmark/run.ts --no-hash
|
|
399
|
+
npx tsx benchmark/run.ts # режим hashline
|
|
400
|
+
npx tsx benchmark/run.ts --no-hash # режим str_replace
|
|
401
|
+
npx tsx benchmark/run.ts --apply-patch # режим apply_patch
|
|
349
402
|
```
|
|
350
403
|
|
|
351
404
|
<details>
|
|
@@ -409,9 +462,12 @@ npm run typecheck
|
|
|
409
462
|
|
|
410
463
|
Hashline решает эту проблему, присваивая каждой строке короткий детерминированный хеш-тег (например, `2:f1c`), что делает адресацию строк **точной и однозначной**. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
|
|
411
464
|
|
|
465
|
+
Продвинутые фичи — **ревизия файла** (`fileRev`), **safe reapply** и **structured errors** — вдохновлены реализацией hash-based editing в проекте **AssistAgents** от [OzeroHAX](https://github.com/OzeroHAX/AssistAgents), который независимо применил аналогичный подход для OpenCode с дополнительными механизмами проверки целостности и диагностики ошибок.
|
|
466
|
+
|
|
412
467
|
**Ссылки:**
|
|
413
468
|
- [oh-my-pi от can1357](https://github.com/can1357/oh-my-pi) — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
|
|
414
469
|
- [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — блог-пост с подробным описанием проблемы
|
|
470
|
+
- [AssistAgents от OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing для OpenCode с file revision, safe reapply и structured conflicts
|
|
415
471
|
- [Статья на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — описание подхода на русском языке
|
|
416
472
|
|
|
417
473
|
---
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
resolveConfig,
|
|
5
5
|
shouldExclude,
|
|
6
6
|
stripHashes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DOR4YDIS.js";
|
|
8
8
|
|
|
9
9
|
// src/hooks.ts
|
|
10
10
|
import { appendFileSync } from "fs";
|
|
@@ -94,7 +94,7 @@ function createFileReadAfterHook(cache, config) {
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
|
|
97
|
+
const annotated = formatFileWithHashes(content, hashLen || void 0, prefix, resolved.fileRev);
|
|
98
98
|
output.output = annotated;
|
|
99
99
|
debug("annotated", typeof filePath === "string" ? filePath : input.tool, "lines:", content.split("\n").length);
|
|
100
100
|
if (cache && typeof filePath === "string") {
|
|
@@ -206,10 +206,32 @@ function createSystemPromptHook(config) {
|
|
|
206
206
|
'- Hash references include both the line number AND the content hash, so `2:f1c` means "line 2 with hash f1c".',
|
|
207
207
|
"- If you see a mismatch, do NOT proceed with the edit \u2014 re-read the file to get fresh references.",
|
|
208
208
|
"",
|
|
209
|
+
"### File revision (`#HL REV:<hash>`):",
|
|
210
|
+
"- When files are read, the first line may contain a file revision header: `" + prefix + "REV:<8-char-hex>`.",
|
|
211
|
+
"- This is a hash of the entire file content. Pass it as the `fileRev` parameter to `hashline_edit` to verify the file hasn't changed.",
|
|
212
|
+
"- If the file was modified between read and edit, the revision check fails with `FILE_REV_MISMATCH` \u2014 re-read the file.",
|
|
213
|
+
"",
|
|
214
|
+
"### Safe reapply (`safeReapply`):",
|
|
215
|
+
"- Pass `safeReapply: true` to `hashline_edit` to enable automatic line relocation.",
|
|
216
|
+
"- If a line moved (e.g., due to insertions above), safe reapply finds it by content hash.",
|
|
217
|
+
"- If exactly one match is found, the edit proceeds at the new location.",
|
|
218
|
+
"- If multiple matches exist, the edit fails with `AMBIGUOUS_REAPPLY` \u2014 re-read the file.",
|
|
219
|
+
"",
|
|
220
|
+
"### Structured error codes:",
|
|
221
|
+
"- `HASH_MISMATCH` \u2014 line content changed since last read",
|
|
222
|
+
"- `FILE_REV_MISMATCH` \u2014 file was modified since last read",
|
|
223
|
+
"- `AMBIGUOUS_REAPPLY` \u2014 multiple candidate lines found during safe reapply",
|
|
224
|
+
"- `TARGET_OUT_OF_RANGE` \u2014 line number exceeds file length",
|
|
225
|
+
"- `INVALID_REF` \u2014 malformed hash reference",
|
|
226
|
+
"- `INVALID_RANGE` \u2014 start line is after end line",
|
|
227
|
+
"- `MISSING_REPLACEMENT` \u2014 replace/insert operation without replacement content",
|
|
228
|
+
"",
|
|
209
229
|
"### Best practices:",
|
|
210
230
|
"- Use hash references for all edit operations to ensure precision.",
|
|
211
231
|
"- When making multiple edits, work from bottom to top to avoid line number shifts.",
|
|
212
|
-
"- For large replacements, use range references (e.g., `1:a3f to 10:b2c`) instead of individual lines."
|
|
232
|
+
"- For large replacements, use range references (e.g., `1:a3f to 10:b2c`) instead of individual lines.",
|
|
233
|
+
"- Use `fileRev` to guard against stale edits on critical files.",
|
|
234
|
+
"- Use `safeReapply: true` when editing files that may have shifted due to earlier edits."
|
|
213
235
|
].join("\n")
|
|
214
236
|
);
|
|
215
237
|
};
|