
Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI
🇷🇺 Русский | 🇬🇧 English
Hashline-плагин для OpenCode — аннотирует каждую строку файла детерминированным хеш-тегом, чтобы AI мог ссылаться на код и редактировать его с хирургической точностью.
Hashline аннотирует каждую строку файла коротким детерминированным hex-хешем. Когда AI читает файл, он видит:
#HL 1:a3f|function hello() {
#HL 2:f1c| return "world";
#HL 3:0e7|}
Примечание: Длина хеша адаптивная — она зависит от размера файла (3 символа для ≤4096 строк, 4 символа для >4096 строк). Минимальная длина хеша — 3 символа. В примерах ниже используются 3-символьные хеши. Префикс
#HLзащищает от ложных срабатываний при удалении хешей и является настраиваемым.
AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
- «Заменить строку
2:f1c» — указать конкретную строку однозначно - «Заменить блок от
1:a3fдо3:0e7» — указать диапазон строк - «Вставить после
3:0e7» — вставить в точное место
Hashline решает фундаментальные проблемы двух существующих подходов к редактированию файлов AI:
str_replaceтребует абсолютно точного совпаденияold_string. Любой лишний пробел, неверный отступ или дублирующиеся строки в файле — и редактирование завершается ошибкой «String to replace not found». Это настолько распространённая проблема, что у неё есть мегатред на 27+ тикетов на GitHub.apply_patch(unified diff) работает только на моделях, специально обученных этому формату. На других моделях результаты катастрофические: Grok 4 проваливает 50.7% патчей, GLM-4.7 — 46.2% (источник).
Hashline адресует каждую строку уникальным хешем lineNumber:hash. Никакого строкового совпадения, никакой зависимости от специального обучения модели — только точная, верифицируемая адресация.
Длина хеша автоматически адаптируется к размеру файла для минимизации коллизий:
| Размер файла | Длина хеша | Возможных значений |
|---|---|---|
| ≤ 4 096 строк | 3 hex-символа | 4 096 |
| > 4 096 строк | 4 hex-символа | 65 536 |
Строки аннотируются настраиваемым префиксом (по умолчанию: #HL ), чтобы предотвратить ложные срабатывания при удалении хешей. Это гарантирует, что строки данных вроде 1:ab|some data не будут случайно обрезаны.
#HL 1:a3|function hello() {
#HL 2:f1| return "world";
#HL 3:0e|}
Префикс можно настроить или отключить для обратной совместимости:
// Кастомный префиксconsthl=createHashline({prefix: ">> "});// Отключить префикс (legacy-формат: "1:a3|code")consthl=createHashline({prefix: false});Встроенный LRU-кеш (filePath → annotatedContent) с настраиваемым размером (по умолчанию 100 файлов). При повторном чтении того же файла с неизменённым содержимым возвращается кешированный результат. Кеш автоматически инвалидируется при изменении содержимого файла.
Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
import{verifyHash}from"opencode-hashline/utils";constresult=verifyHash(2,"f1c",currentContent);if(!result.valid){console.error(result.message);// "Hash mismatch at line 2: ..."}Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде 2:f1 остаётся валидной даже если файл вырос.
Помимо построчных хешей, hashline вычисляет хеш всего файла (FNV-1a, 8 hex-символов). Он добавляется первой строкой аннотации:
#HL REV:72c4946c
#HL 1:a3f|function hello() {
#HL 2:f1c| return "world";
При редактировании передайте fileRev в hashline_edit — если файл изменился с момента чтения, правка будет отклонена с ошибкой FILE_REV_MISMATCH.
Если строка переместилась (например, из-за вставки строк выше), safeReapply находит её по хешу контента:
- 1 кандидат — правка применяется к новой позиции
- >1 кандидатов — ошибка
AMBIGUOUS_REAPPLY(неоднозначность) - 0 кандидатов — ошибка
HASH_MISMATCH
constresult=applyHashEdit({operation: "replace",startRef: "1:a3f",replacement: "new"},content,undefined,true,// safeReapply);Все ошибки hashline — экземпляры HashlineError с кодом, диагностикой и подсказками:
| Код | Описание |
|---|---|
HASH_MISMATCH | Содержимое строки изменилось |
FILE_REV_MISMATCH | Файл модифицирован с момента чтения |
AMBIGUOUS_REAPPLY | Несколько кандидатов при safe reapply |
TARGET_OUT_OF_RANGE | Номер строки за пределами файла |
INVALID_REF | Некорректная хеш-ссылка |
INVALID_RANGE | Начало диапазона после конца |
MISSING_REPLACEMENT | Операция replace/insert без содержимого |
Вычисление хеша использует trimEnd() (а не trim()), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
Резолвинг и замена диапазонов строк по хеш-ссылкам:
import{resolveRange,replaceRange}from"opencode-hashline/utils";// Получить строки между двумя хеш-ссылкамиconstrange=resolveRange("1:a3f","3:0e7",content);console.log(range.lines);// ["function hello() {", ' return "world";', "}"]// Заменить диапазон новым содержимымconstnewContent=replaceRange("1:a3f","3:0e7",content,"function goodbye() {\n return 'farewell';\n}");Создание кастомных экземпляров Hashline с определёнными настройками:
import{createHashline}from"opencode-hashline/utils";consthl=createHashline({exclude: ["**/node_modules/**","**/*.min.js"],maxFileSize: 512_000,// 512 КБhashLength: 3,// принудительно 3-символьные хешиcacheSize: 200,// кешировать до 200 файловprefix: "#HL ",// магический префикс (по умолчанию)});// Использование настроенного экземпляраconstannotated=hl.formatFileWithHashes(content,"src/app.ts");constisExcluded=hl.shouldExclude("node_modules/foo.js");// true| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
exclude | string[] | См. ниже | Glob-паттерны для исключения файлов |
maxFileSize | number | 1_048_576 (1 МиБ) | Макс. размер файла в байтах |
hashLength | number | undefined | undefined (адаптивно) | Принудительная длина хеша |
cacheSize | number | 100 | Макс. файлов в LRU-кеше |
prefix | string | false | "#HL " | Префикс строки (false для отключения) |
fileRev | boolean | true | Включать ревизию файла (#HL REV:...) в аннотации |
safeReapply | boolean | false | Автоматический поиск перемещённых строк по хешу |
Паттерны исключения по умолчанию: lock-файлы, node_modules, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
npm install opencode-hashlineДобавьте плагин в ваш opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-hashline"]
}Плагин загружает конфигурацию из следующих мест (в порядке приоритета, более поздние перезаписывают ранние):
| Приоритет | Расположение | Область |
|---|---|---|
| 1 | ~/.config/opencode/opencode-hashline.json | Глобальная (все проекты) |
| 2 | <project>/opencode-hashline.json | Локальная (проект) |
| 3 | Программная конфигурация через createHashlinePlugin() | Аргумент фабрики |
Пример opencode-hashline.json:
{
"exclude": ["**/node_modules/**", "**/*.min.js"],
"maxFileSize": 1048576,
"hashLength": 0,
"cacheSize": 100,
"prefix": "#HL "
}Вот и всё! Плагин автоматически:
| # | Действие | Описание |
|---|---|---|
| 1 | 📝 Аннотирует чтение файлов | При чтении файла AI каждая строка получает #HL хеш-префикс |
| 2 | 📎 Аннотирует @file упоминания | Файлы, прикреплённые через @filename в промпте, тоже аннотируются хешлайнами |
| 3 | ✂️ Убирает хеш-префиксы при редактировании | При записи/редактировании файла хеш-префиксы удаляются перед применением изменений |
| 4 | 🧠 Внедряет инструкции в системный промпт | AI получает инструкции по интерпретации и использованию hashline-ссылок |
| 5 | 💾 Кеширует результаты | Повторные чтения того же файла возвращают кешированные аннотации |
| 6 | 🔍 Фильтрует по инструменту | Только инструменты чтения файлов (например read_file, cat, view) получают аннотации; остальные не затрагиваются |
| 7 | ⚙️ Учитывает конфигурацию | Исключённые файлы и файлы, превышающие maxFileSize, пропускаются |
| 8 | 🧩 Регистрирует hashline_edit tool | Применяет replace/delete/insert по hash-ссылкам без точного old_string-матчинга |
Хеш каждой строки вычисляется из:
- 0-based индекса строки
- Содержимого строки с обрезанными завершающими пробелами (trimEnd) — ведущие пробелы (отступы) ЗНАЧИМЫ
Это подаётся в хеш-функцию FNV-1a, сводится к соответствующему модулю в зависимости от размера файла и отображается как hex-строка.
Плагин регистрирует четыре хука OpenCode и один кастомный tool:
| Хук | Назначение |
|---|---|
tool.hashline_edit | Hash-aware правки по ссылкам вроде 5:a3f или `#HL 5:a3f |
tool.execute.after | Добавляет hashline-аннотации в вывод инструментов чтения файлов |
tool.execute.before | Убирает hashline-префиксы из аргументов инструментов редактирования |
chat.message | Аннотирует @file упоминания в сообщениях пользователя (записывает аннотированный контент во временный файл и подменяет URL) |
experimental.chat.system.transform | Добавляет инструкции по использованию hashline в системный промпт |
Основные утилиты экспортируются из субпути opencode-hashline/utils (чтобы избежать конфликтов с загрузчиком плагинов OpenCode, который вызывает каждый экспорт как функцию Plugin):
import{computeLineHash,formatFileWithHashes,stripHashes,parseHashRef,normalizeHashRef,buildHashMap,getAdaptiveHashLength,verifyHash,resolveRange,replaceRange,applyHashEdit,HashlineCache,createHashline,shouldExclude,matchesGlob,resolveConfig,DEFAULT_PREFIX,}from"opencode-hashline/utils";// Вычислить хеш для одной строкиconsthash=computeLineHash(0,"function hello() {");// например "a3f"// Вычислить хеш с определённой длинойconsthash4=computeLineHash(0,"function hello() {",4);// например "a3f2"// Аннотировать содержимое файла (адаптивная длина хеша, с префиксом #HL)constannotated=formatFileWithHashes(fileContent);// "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"// Аннотировать с определённой длиной хешаconstannotated3=formatFileWithHashes(fileContent,3);// Аннотировать без префикса (legacy-формат)constannotatedLegacy=formatFileWithHashes(fileContent,undefined,false);// Убрать аннотации, получить оригинальное содержимоеconstoriginal=stripHashes(annotated);// Разобрать хеш-ссылкуconst{ line, hash }=parseHashRef("2:f1c");// { line: 2, hash: "f1c" }// Нормализовать ссылку из аннотированной строкиconstref=normalizeHashRef("#HL 2:f1c|const x = 1;");// "2:f1c"// Построить карту соответствийconstmap=buildHashMap(fileContent);// Map<"2:f1c", 2>// Верифицировать хеш-ссылку (использует hash.length, а не размер файла)constresult=verifyHash(2,"f1c",fileContent);// Резолвить диапазонconstrange=resolveRange("1:a3f","3:0e7",fileContent);// Заменить диапазонconstnewContent=replaceRange("1:a3f","3:0e7",fileContent,"новое содержимое");// Hash-aware операция редактирования (replace/delete/insert_before/insert_after)constedited=applyHashEdit({operation: "replace",startRef: "1:a3f",endRef: "3:0e7",replacement: "новое содержимое"},fileContent).content;// Проверить, нужно ли исключить файлconstexcluded=shouldExclude("node_modules/foo.js",["**/node_modules/**"]);// Создать настроенный экземплярconsthl=createHashline({cacheSize: 50,hashLength: 3});Все три подхода протестированы на 60 фикстурах из react-edit-benchmark — мутированных файлах React с известными багами (инвертированные булевы, перепутанные операторы, удалённые guard-клаузы и т.д.):
| hashline | str_replace | apply_patch | |
|---|---|---|---|
| Прошло | 60/60 (100%) | 58/60 (96.7%) | 60/60 (100%) |
| Провалено | 0 | 2 | 0 |
| Неоднозначные правки | 0 | 4 | 0 |
apply_patch с контекстными строками работает так же надёжно, как hashline — при условии, что модель правильно генерирует патч. Слабое место apply_patch — зависимость от обучения конкретной модели: не обученные под этот формат модели производят некорректные diff-ы (пропускают контекст, путают отступы), что приводит к провалу применения патча.
str_replace ломается, когда old_string встречается в файле несколько раз (повторяющиеся guard-клаузы, похожие блоки кода). Hashline адресует каждую строку уникально через lineNumber:hash — неоднозначность исключена, модельный формат не нужен.
# Запустите сами:
npx tsx benchmark/run.ts # режим hashline
npx tsx benchmark/run.ts --no-hash # режим str_replace
npx tsx benchmark/run.ts --apply-patch # режим apply_patchОшибки str_replace (категория structural)
structural-remove-early-return-001—old_stringсовпал в нескольких местах, замена применена не к томуstructural-remove-early-return-002— аналогичная проблемаstructural-delete-statement-002— неоднозначное совпадение (первое совпадение оказалось верным)structural-delete-statement-003— неоднозначное совпадение (первое совпадение оказалось верным)
Аннотации hashline добавляют префикс #HL <line>:<hash>| (~12 символов / ~3 токена) на строку:
| Без хешей | С хешами | Оверхед | |
|---|---|---|---|
| Символы | 404K | 564K | +40% |
| Токены (~) | ~101K | ~141K | +40% |
Оверхед стабильно ~40% независимо от размера файла. Для типичного файла на 200 строк (~800 токенов) hashline добавляет ~600 токенов — пренебрежимо мало при контекстном окне в 200K.
| Размер файла | Аннотация | Правка | Удаление хешей |
|---|---|---|---|
| 10 строк | 0.05 мс | 0.01 мс | 0.03 мс |
| 100 строк | 0.12 мс | 0.02 мс | 0.08 мс |
| 1 000 строк | 0.95 мс | 0.04 мс | 0.60 мс |
| 5 000 строк | 4.50 мс | 0.08 мс | 2.80 мс |
| 10 000 строк | 9.20 мс | 0.10 мс | 5.50 мс |
Типичный файл из 1 000 строк аннотируется за < 1 мс — незаметно для пользователя.
# Установить зависимости
npm install
# Запустить тесты
npm test# Собрать
npm run build
# Проверка типов
npm run typecheckИдея hashline вдохновлена концепциями из oh-my-pi от can1357 — AI-тулкита для разработки (coding agent CLI, unified LLM API, TUI-библиотеки) — и статьи «The Harness Problem» (проблема обвязки).
Суть проблемы: современные AI-модели обладают огромными возможностями, но инструменты (harness), которые передают модели контекст и применяют её правки к файлам, теряют информацию и порождают ошибки. Модель видит содержимое файла, но при редактировании вынуждена «угадывать» контекст окружающих строк. Search-and-replace ломается на дубликатах строк, а diff-формат тоже ненадёжен на практике.
Hashline решает эту проблему, присваивая каждой строке короткий детерминированный хеш-тег (например, 2:f1c), что делает адресацию строк точной и однозначной. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
Продвинутые фичи — ревизия файла (fileRev), safe reapply и structured errors — вдохновлены реализацией hash-based editing в проекте AssistAgents от OzeroHAX, который независимо применил аналогичный подход для OpenCode с дополнительными механизмами проверки целостности и диагностики ошибок.
Ссылки:
- oh-my-pi от can1357 — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
- The Harness Problem — блог-пост с подробным описанием проблемы
- AssistAgents от OzeroHAX — hash-based editing для OpenCode с file revision, safe reapply и structured conflicts
- Статья на Хабре — описание подхода на русском языке
MIT © opencode-hashline contributors