opencode-hashline 1.2.0 → 1.3.1

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
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="banner.jpg" alt="opencode-hashline banner" width="100%" />
4
+
3
5
  # 🔗 opencode-hashline
4
6
 
5
7
  **Content-addressable line hashing for precise AI code editing**
@@ -61,7 +63,6 @@ Hash length automatically adapts to file size to minimize collisions:
61
63
 
62
64
  | File Size | Hash Length | Possible Values |
63
65
  |-----------|:----------:|:---------------:|
64
- | ≤ 256 lines | 2 hex chars | 256 |
65
66
  | ≤ 4,096 lines | 3 hex chars | 4,096 |
66
67
  | > 4,096 lines | 4 hex chars | 65,536 |
67
68
 
@@ -94,7 +95,7 @@ Built-in LRU cache (`filePath → annotatedContent`) with configurable size (def
94
95
  Verify that a line hasn't changed since it was read — protects against race conditions:
95
96
 
96
97
  ```typescript
97
- import { verifyHash } from "opencode-hashline";
98
+ import { verifyHash } from "opencode-hashline/utils";
98
99
 
99
100
  const result = verifyHash(2, "f1c", currentContent);
100
101
  if (!result.valid) {
@@ -104,6 +105,49 @@ if (!result.valid) {
104
105
 
105
106
  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.
106
107
 
108
+ ### 🔒 File Revision (`fileRev`)
109
+
110
+ 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:
111
+
112
+ ```
113
+ #HL REV:72c4946c
114
+ #HL 1:a3f|function hello() {
115
+ #HL 2:f1c| return "world";
116
+ ```
117
+
118
+ Pass `fileRev` to `hashline_edit` when editing — if the file changed since it was read, the edit is rejected with `FILE_REV_MISMATCH`.
119
+
120
+ ### 🔄 Safe Reapply
121
+
122
+ If a line moved (e.g., due to insertions above), `safeReapply` finds it by content hash:
123
+
124
+ - **1 candidate** — edit applies at the new position
125
+ - **>1 candidates** — `AMBIGUOUS_REAPPLY` error (ambiguous)
126
+ - **0 candidates** — `HASH_MISMATCH` error
127
+
128
+ ```typescript
129
+ const result = applyHashEdit(
130
+ { operation: "replace", startRef: "1:a3f", replacement: "new" },
131
+ content,
132
+ undefined,
133
+ true, // safeReapply
134
+ );
135
+ ```
136
+
137
+ ### 🏷️ Structured Errors
138
+
139
+ All hashline errors are instances of `HashlineError` with error codes, diagnostics, and hints:
140
+
141
+ | Code | Description |
142
+ |------|-------------|
143
+ | `HASH_MISMATCH` | Line content changed since last read |
144
+ | `FILE_REV_MISMATCH` | File was modified since last read |
145
+ | `AMBIGUOUS_REAPPLY` | Multiple candidates found during safe reapply |
146
+ | `TARGET_OUT_OF_RANGE` | Line number exceeds file length |
147
+ | `INVALID_REF` | Malformed hash reference |
148
+ | `INVALID_RANGE` | Start line is after end line |
149
+ | `MISSING_REPLACEMENT` | Replace/insert operation without content |
150
+
107
151
  ### 🔍 Indentation-Sensitive Hashing
108
152
 
109
153
  Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
@@ -113,7 +157,7 @@ Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespa
113
157
  Resolve and replace ranges of lines by hash references:
114
158
 
115
159
  ```typescript
116
- import { resolveRange, replaceRange } from "opencode-hashline";
160
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
117
161
 
118
162
  // Get lines between two hash references
119
163
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -131,7 +175,7 @@ const newContent = replaceRange(
131
175
  Create custom Hashline instances with specific settings:
132
176
 
133
177
  ```typescript
134
- import { createHashline } from "opencode-hashline";
178
+ import { createHashline } from "opencode-hashline/utils";
135
179
 
136
180
  const hl = createHashline({
137
181
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -151,10 +195,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
151
195
  | Option | Type | Default | Description |
152
196
  |--------|------|---------|-------------|
153
197
  | `exclude` | `string[]` | See below | Glob patterns for files to skip |
154
- | `maxFileSize` | `number` | `1_000_000` | Max file size in bytes |
198
+ | `maxFileSize` | `number` | `1_048_576` (1 MB) | Max file size in bytes |
155
199
  | `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
156
200
  | `cacheSize` | `number` | `100` | Max files in LRU cache |
157
201
  | `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
202
+ | `fileRev` | `boolean` | `true` | Include file revision hash (`#HL REV:...`) in annotations |
203
+ | `safeReapply` | `boolean` | `false` | Auto-relocate moved lines by content hash |
158
204
 
159
205
  Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
160
206
 
@@ -257,7 +303,7 @@ The plugin needs to determine which tools are "file-read" tools (to annotate the
257
303
  The `isFileReadTool()` function is exported for testing and advanced usage:
258
304
 
259
305
  ```typescript
260
- import { isFileReadTool } from "opencode-hashline";
306
+ import { isFileReadTool } from "opencode-hashline/utils";
261
307
 
262
308
  isFileReadTool("read_file"); // true
263
309
  isFileReadTool("mcp.read"); // true
@@ -442,9 +488,12 @@ The idea behind hashline is inspired by concepts from **oh-my-pi** by [can1357](
442
488
 
443
489
  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.
444
490
 
491
+ 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.
492
+
445
493
  **References:**
446
494
  - [oh-my-pi by can1357](https://github.com/can1357/oh-my-pi) — AI coding agent toolkit: coding agent CLI, unified LLM API, TUI libraries
447
495
  - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — blog post describing the problem in detail
496
+ - [AssistAgents by OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing for OpenCode with file revision, safe reapply, and structured conflicts
448
497
  - [Описание подхода на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — overview of the approach in Russian
449
498
 
450
499
  ---
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="banner.jpg" alt="opencode-hashline banner" width="100%" />
4
+
3
5
  # 🔗 opencode-hashline
4
6
 
5
7
  **Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI**
@@ -34,7 +36,7 @@ Hashline аннотирует каждую строку файла коротк
34
36
  #HL 3:0e7|}
35
37
  ```
36
38
 
37
- > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
39
+ > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (3 символа для ≤4096 строк, 4 символа для >4096 строк). Минимальная длина хеша — 3 символа. В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
38
40
 
39
41
  AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
40
42
 
@@ -61,7 +63,6 @@ Hashline адресует каждую строку уникальным хеш
61
63
 
62
64
  | Размер файла | Длина хеша | Возможных значений |
63
65
  |-------------|:----------:|:------------------:|
64
- | ≤ 256 строк | 2 hex-символа | 256 |
65
66
  | ≤ 4 096 строк | 3 hex-символа | 4 096 |
66
67
  | > 4 096 строк | 4 hex-символа | 65 536 |
67
68
 
@@ -94,7 +95,7 @@ const hl = createHashline({ prefix: false });
94
95
  Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
95
96
 
96
97
  ```typescript
97
- import { verifyHash } from "opencode-hashline";
98
+ import { verifyHash } from "opencode-hashline/utils";
98
99
 
99
100
  const result = verifyHash(2, "f1c", currentContent);
100
101
  if (!result.valid) {
@@ -104,6 +105,49 @@ if (!result.valid) {
104
105
 
105
106
  Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде `2:f1` остаётся валидной даже если файл вырос.
106
107
 
108
+ ### 🔒 Ревизия файла (`fileRev`)
109
+
110
+ Помимо построчных хешей, hashline вычисляет хеш всего файла (FNV-1a, 8 hex-символов). Он добавляется первой строкой аннотации:
111
+
112
+ ```
113
+ #HL REV:72c4946c
114
+ #HL 1:a3f|function hello() {
115
+ #HL 2:f1c| return "world";
116
+ ```
117
+
118
+ При редактировании передайте `fileRev` в `hashline_edit` — если файл изменился с момента чтения, правка будет отклонена с ошибкой `FILE_REV_MISMATCH`.
119
+
120
+ ### 🔄 Safe Reapply
121
+
122
+ Если строка переместилась (например, из-за вставки строк выше), `safeReapply` находит её по хешу контента:
123
+
124
+ - **1 кандидат** — правка применяется к новой позиции
125
+ - **>1 кандидатов** — ошибка `AMBIGUOUS_REAPPLY` (неоднозначность)
126
+ - **0 кандидатов** — ошибка `HASH_MISMATCH`
127
+
128
+ ```typescript
129
+ const result = applyHashEdit(
130
+ { operation: "replace", startRef: "1:a3f", replacement: "new" },
131
+ content,
132
+ undefined,
133
+ true, // safeReapply
134
+ );
135
+ ```
136
+
137
+ ### 🏷️ Structured Errors
138
+
139
+ Все ошибки hashline — экземпляры `HashlineError` с кодом, диагностикой и подсказками:
140
+
141
+ | Код | Описание |
142
+ |-----|----------|
143
+ | `HASH_MISMATCH` | Содержимое строки изменилось |
144
+ | `FILE_REV_MISMATCH` | Файл модифицирован с момента чтения |
145
+ | `AMBIGUOUS_REAPPLY` | Несколько кандидатов при safe reapply |
146
+ | `TARGET_OUT_OF_RANGE` | Номер строки за пределами файла |
147
+ | `INVALID_REF` | Некорректная хеш-ссылка |
148
+ | `INVALID_RANGE` | Начало диапазона после конца |
149
+ | `MISSING_REPLACEMENT` | Операция replace/insert без содержимого |
150
+
107
151
  ### 🔍 Чувствительность к отступам
108
152
 
109
153
  Вычисление хеша использует `trimEnd()` (а не `trim()`), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
@@ -113,7 +157,7 @@ if (!result.valid) {
113
157
  Резолвинг и замена диапазонов строк по хеш-ссылкам:
114
158
 
115
159
  ```typescript
116
- import { resolveRange, replaceRange } from "opencode-hashline";
160
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
117
161
 
118
162
  // Получить строки между двумя хеш-ссылками
119
163
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -131,7 +175,7 @@ const newContent = replaceRange(
131
175
  Создание кастомных экземпляров Hashline с определёнными настройками:
132
176
 
133
177
  ```typescript
134
- import { createHashline } from "opencode-hashline";
178
+ import { createHashline } from "opencode-hashline/utils";
135
179
 
136
180
  const hl = createHashline({
137
181
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -151,10 +195,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
151
195
  | Параметр | Тип | По умолчанию | Описание |
152
196
  |----------|-----|:------------:|----------|
153
197
  | `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
154
- | `maxFileSize` | `number` | `1_000_000` | Макс. размер файла в байтах |
198
+ | `maxFileSize` | `number` | `1_048_576` (1 МБ) | Макс. размер файла в байтах |
155
199
  | `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
156
200
  | `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
157
201
  | `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
202
+ | `fileRev` | `boolean` | `true` | Включать ревизию файла (`#HL REV:...`) в аннотации |
203
+ | `safeReapply` | `boolean` | `false` | Автоматический поиск перемещённых строк по хешу |
158
204
 
159
205
  Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
160
206
 
@@ -417,9 +463,12 @@ npm run typecheck
417
463
 
418
464
  Hashline решает эту проблему, присваивая каждой строке короткий детерминированный хеш-тег (например, `2:f1c`), что делает адресацию строк **точной и однозначной**. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
419
465
 
466
+ Продвинутые фичи — **ревизия файла** (`fileRev`), **safe reapply** и **structured errors** — вдохновлены реализацией hash-based editing в проекте **AssistAgents** от [OzeroHAX](https://github.com/OzeroHAX/AssistAgents), который независимо применил аналогичный подход для OpenCode с дополнительными механизмами проверки целостности и диагностики ошибок.
467
+
420
468
  **Ссылки:**
421
469
  - [oh-my-pi от can1357](https://github.com/can1357/oh-my-pi) — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
422
470
  - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — блог-пост с подробным описанием проблемы
471
+ - [AssistAgents от OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing для OpenCode с file revision, safe reapply и structured conflicts
423
472
  - [Статья на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — описание подхода на русском языке
424
473
 
425
474
  ---
package/README.ru.md CHANGED
@@ -34,7 +34,7 @@ Hashline аннотирует каждую строку файла коротк
34
34
  #HL 3:0e7|}
35
35
  ```
36
36
 
37
- > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
37
+ > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (3 символа для ≤4096 строк, 4 символа для >4096 строк). Минимальная длина хеша — 3 символа. В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
38
38
 
39
39
  AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
40
40
 
@@ -56,7 +56,6 @@ AI-модель может ссылаться на строки по их хеш
56
56
 
57
57
  | Размер файла | Длина хеша | Возможных значений |
58
58
  |-------------|:----------:|:------------------:|
59
- | ≤ 256 строк | 2 hex-символа | 256 |
60
59
  | ≤ 4 096 строк | 3 hex-символа | 4 096 |
61
60
  | > 4 096 строк | 4 hex-символа | 65 536 |
62
61
 
@@ -89,7 +88,7 @@ const hl = createHashline({ prefix: false });
89
88
  Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
90
89
 
91
90
  ```typescript
92
- import { verifyHash } from "opencode-hashline";
91
+ import { verifyHash } from "opencode-hashline/utils";
93
92
 
94
93
  const result = verifyHash(2, "f1c", currentContent);
95
94
  if (!result.valid) {
@@ -108,7 +107,7 @@ if (!result.valid) {
108
107
  Резолвинг и замена диапазонов строк по хеш-ссылкам:
109
108
 
110
109
  ```typescript
111
- import { resolveRange, replaceRange } from "opencode-hashline";
110
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
112
111
 
113
112
  // Получить строки между двумя хеш-ссылками
114
113
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -126,7 +125,7 @@ const newContent = replaceRange(
126
125
  Создание кастомных экземпляров Hashline с определёнными настройками:
127
126
 
128
127
  ```typescript
129
- import { createHashline } from "opencode-hashline";
128
+ import { createHashline } from "opencode-hashline/utils";
130
129
 
131
130
  const hl = createHashline({
132
131
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -146,10 +145,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
146
145
  | Параметр | Тип | По умолчанию | Описание |
147
146
  |----------|-----|:------------:|----------|
148
147
  | `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
149
- | `maxFileSize` | `number` | `1_000_000` | Макс. размер файла в байтах |
148
+ | `maxFileSize` | `number` | `1_048_576` (1 МБ) | Макс. размер файла в байтах |
150
149
  | `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
151
150
  | `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
152
151
  | `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
152
+ | `fileRev` | `boolean` | `true` | Включать ревизию файла (`#HL REV:...`) в аннотации |
153
+ | `safeReapply` | `boolean` | `false` | Автоматический поиск перемещённых строк по хешу |
153
154
 
154
155
  Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
155
156