opencode-hashline
v1.3.0
Published
Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing
Maintainers
Readme
🔗 opencode-hashline
Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI
🇷🇺 Русский | 🇬🇧 English
Hashline-плагин для OpenCode — аннотирует каждую строку файла детерминированным хеш-тегом, чтобы AI мог ссылаться на код и редактировать его с хирургической точностью.
📖 Что такое Hashline?
Hashline аннотирует каждую строку файла коротким детерминированным hex-хешем. Когда AI читает файл, он видит:
#HL 1:a3f|function hello() {
#HL 2:f1c| return "world";
#HL 3:0e7|}Примечание: Длина хеша адаптивная — она зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс
#HLзащищает от ложных срабатываний при удалении хешей и является настраиваемым.
AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
- «Заменить строку
2:f1c» — указать конкретную строку однозначно - «Заменить блок от
1:a3fдо3:0e7» — указать диапазон строк - «Вставить после
3:0e7» — вставить в точное место
🤔 Почему это помогает?
Традиционные номера строк сдвигаются при редактировании, вызывая ошибки смещения и устаревшие ссылки. Хеш-теги Hashline контентно-адресуемы — они вычисляются из индекса строки и её содержимого, что делает их стабильной, верифицируемой ссылкой для точной коммуникации о местоположении в коде.
✨ Возможности
📏 Адаптивная длина хеша
Длина хеша автоматически адаптируется к размеру файла для минимизации коллизий:
| Размер файла | Длина хеша | Возможных значений | |-------------|:----------:|:------------------:| | ≤ 256 строк | 2 hex-символа | 256 | | ≤ 4 096 строк | 3 hex-символа | 4 096 | | > 4 096 строк | 4 hex-символа | 65 536 |
🏷️ Магический префикс (#HL )
Строки аннотируются настраиваемым префиксом (по умолчанию: #HL ), чтобы предотвратить ложные срабатывания при удалении хешей. Это гарантирует, что строки данных вроде 1:ab|some data не будут случайно обрезаны.
#HL 1:a3|function hello() {
#HL 2:f1| return "world";
#HL 3:0e|}Префикс можно настроить или отключить для обратной совместимости:
// Кастомный префикс
const hl = createHashline({ prefix: ">> " });
// Отключить префикс (legacy-формат: "1:a3|code")
const hl = createHashline({ prefix: false });💾 LRU-кеширование
Встроенный LRU-кеш (filePath → annotatedContent) с настраиваемым размером (по умолчанию 100 файлов). При повторном чтении того же файла с неизменённым содержимым возвращается кешированный результат. Кеш автоматически инвалидируется при изменении содержимого файла.
✅ Верификация хешей
Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
import { verifyHash } from "opencode-hashline";
const result = verifyHash(2, "f1c", currentContent);
if (!result.valid) {
console.error(result.message); // "Hash mismatch at line 2: ..."
}Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде 2:f1 остаётся валидной даже если файл вырос.
🔍 Чувствительность к отступам
Вычисление хеша использует trimEnd() (а не trim()), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
📐 Range-операции
Резолвинг и замена диапазонов строк по хеш-ссылкам:
import { resolveRange, replaceRange } from "opencode-hashline";
// Получить строки между двумя хеш-ссылками
const range = resolveRange("1:a3f", "3:0e7", content);
console.log(range.lines); // ["function hello() {", ' return "world";', "}"]
// Заменить диапазон новым содержимым
const newContent = replaceRange(
"1:a3f", "3:0e7", content,
"function goodbye() {\n return 'farewell';\n}"
);⚙️ Конфигурируемость
Создание кастомных экземпляров Hashline с определёнными настройками:
import { createHashline } from "opencode-hashline";
const hl = createHashline({
exclude: ["**/node_modules/**", "**/*.min.js"],
maxFileSize: 512_000, // 512 КБ
hashLength: 3, // принудительно 3-символьные хеши
cacheSize: 200, // кешировать до 200 файлов
prefix: "#HL ", // магический префикс (по умолчанию)
});
// Использование настроенного экземпляра
const annotated = hl.formatFileWithHashes(content, "src/app.ts");
const isExcluded = hl.shouldExclude("node_modules/foo.js"); // trueПараметры конфигурации
| Параметр | Тип | По умолчанию | Описание |
|----------|-----|:------------:|----------|
| exclude | string[] | См. ниже | Glob-паттерны для исключения файлов |
| maxFileSize | number | 1_000_000 | Макс. размер файла в байтах |
| hashLength | number \| undefined | undefined (адаптивно) | Принудительная длина хеша |
| cacheSize | number | 100 | Макс. файлов в LRU-кеше |
| prefix | string \| false | "#HL " | Префикс строки (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-строка.
Хуки и tool плагина
Плагин регистрирует четыре хука 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 в системный промпт |
🔌 Программный API
Основные утилиты экспортируются из субпути 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";Основные функции
// Вычислить хеш для одной строки
const hash = computeLineHash(0, "function hello() {"); // например "a3f"
// Вычислить хеш с определённой длиной
const hash4 = computeLineHash(0, "function hello() {", 4); // например "a3f2"
// Аннотировать содержимое файла (адаптивная длина хеша, с префиксом #HL)
const annotated = formatFileWithHashes(fileContent);
// "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"
// Аннотировать с определённой длиной хеша
const annotated3 = formatFileWithHashes(fileContent, 3);
// Аннотировать без префикса (legacy-формат)
const annotatedLegacy = formatFileWithHashes(fileContent, undefined, false);
// Убрать аннотации, получить оригинальное содержимое
const original = stripHashes(annotated);Хеш-ссылки и верификация
// Разобрать хеш-ссылку
const { line, hash } = parseHashRef("2:f1c"); // { line: 2, hash: "f1c" }
// Нормализовать ссылку из аннотированной строки
const ref = normalizeHashRef("#HL 2:f1c|const x = 1;"); // "2:f1c"
// Построить карту соответствий
const map = buildHashMap(fileContent); // Map<"2:f1c", 2>
// Верифицировать хеш-ссылку (использует hash.length, а не размер файла)
const result = verifyHash(2, "f1c", fileContent);Range-операции
// Резолвить диапазон
const range = resolveRange("1:a3f", "3:0e7", fileContent);
// Заменить диапазон
const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "новое содержимое");
// Hash-aware операция редактирования (replace/delete/insert_before/insert_after)
const edited = applyHashEdit(
{ operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "новое содержимое" },
fileContent
).content;Утилиты
// Проверить, нужно ли исключить файл
const excluded = shouldExclude("node_modules/foo.js", ["**/node_modules/**"]);
// Создать настроенный экземпляр
const hl = createHashline({ cacheSize: 50, hashLength: 3 });📊 Бенчмарк
Корректность: hashline vs str_replace
Оба подхода протестированы на 60 фикстурах из react-edit-benchmark — мутированных файлах React с известными багами (инвертированные булевы, перепутанные операторы, удалённые guard-клаузы и т.д.):
| | hashline | str_replace | |---|:---:|:---:| | Прошло | 60/60 (100%) | 58/60 (96.7%) | | Провалено | 0 | 2 | | Неоднозначные правки | 0 | 4 |
str_replace ломается, когда old_string встречается в файле несколько раз (например, повторяющиеся guard-клаузы, похожие блоки кода). Hashline адресует каждую строку уникально через lineNumber:hash, поэтому неоднозначность исключена.
# Запустите сами:
npx tsx benchmark/run.ts # режим hashline
npx tsx benchmark/run.ts --no-hash # режим str_replacestructural-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), что делает адресацию строк точной и однозначной. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
Ссылки:
- oh-my-pi от can1357 — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
- The Harness Problem — блог-пост с подробным описанием проблемы
- Статья на Хабре — описание подхода на русском языке
📄 Лицензия
MIT © opencode-hashline contributors
