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 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
- 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 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 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.):
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
- 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.
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 # hashline mode
373
- npx tsx benchmark/run.ts --no-hash # str_replace mode
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
- Традиционные номера строк сдвигаются при редактировании, вызывая ошибки смещения и устаревшие ссылки. Хеш-теги Hashline **контентно-адресуемы** они вычисляются из индекса строки и её содержимого, что делает их стабильной, верифицируемой ссылкой для точной коммуникации о местоположении в коде.
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
- Оба подхода протестированы на **60 фикстурах из [react-edit-benchmark](https://github.com/can1357/oh-my-pi/tree/main/packages/react-edit-benchmark)** — мутированных файлах React с известными багами (инвертированные булевы, перепутанные операторы, удалённые guard-клаузы и т.д.):
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
- str_replace ломается, когда `old_string` встречается в файле несколько раз (например, повторяющиеся guard-клаузы, похожие блоки кода). Hashline адресует каждую строку уникально через `lineNumber:hash`, поэтому неоднозначность исключена.
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 # режим hashline
348
- npx tsx benchmark/run.ts --no-hash # режим str_replace
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-I6RACR3D.js";
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
  };