# dsh-patch-edit-plus

Русский | [English](./README.md) | [简体中文](./README.zh.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [Français](./README.fr.md) | [Deutsch](./README.de.md) | [Italiano](./README.it.md) | [Español](./README.es.md)

Патчевое редактирование файлов для [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness): единственный доступный модели инструмент `apply_patch`, который принимает **git/unified diff** (по умолчанию) и **синтаксис `apply_patch` от Codex** (по явному включению), применяет каждое изменение по принципу **всё-или-ничего** и абсолютно не мешает работе DSH.

## Совместимость с версиями DSH

**v0.4.0 — единая версия, покрывающая всю линию хостов**: одна версия обслуживает все DSH от `0.1.0-rc.2` до `0.2.1-rc.5` (все 20 rc плюс desktop). Четыре прежние линии (`compat/0.1.2`, `compat/0.1.5`, `compat/0.1.7` и схема разделения по `main`) выведены из эксплуатации; после публикации npm dist-tag'и `dsh-0.2.0`, `dsh-0.1.7`, `dsh-0.1.5` и `dsh-0.1.2` будут указывать на 0.4.0.

| Версия DSH | Статус | Примечания |
|---|---|---|
| `0.2.0-rc.1+` | ✅ поддерживается (та же v0.4.0) | Декларативные настройки: поля, помеченные `.volatile()`, становятся формой настроек, а `loader/volatile-update` перерегистрирует инструмент без перемонтирования. Карточка конфигурации на странице плагина (клиентская половина) на этой линии присутствует. |
| `0.1.0-rc.2` ~ `0.1.7-rc.2` | ✅ поддерживается (та же v0.4.0) | Настройки по поколениям: экземплярные `settings.installSection`/`register` (0.1.2/0.1.5) либо модульный `installSettingsSection` из `@deepseek-ai/dsh-settings` (0.1.0/0.1.1). Карточка конфигурации на странице плагина здесь изящно отсутствует. |

**Изменение поведения:** значение `allowCodexPatch` по умолчанию теперь `false` (включение вручную, выровнено с линией 0.2.0). При обновлении со старой линии 0.1.7 (где по умолчанию было `true`) переключатель нужно включить вручную после обновления.

Настройки охватывают три поколения хостов, и все они обслуживаются этой единственной версией: на 0.1.7+ (и desktop) конфигурация декларативна — поля, помеченные `.volatile()` в `Config`, и есть форма настроек, а плагин подписывается на `loader/volatile-update`; на 0.1.2/0.1.5 используются экземплярные API `settings.installSection`/`register`; на 0.1.0/0.1.1 — модульный `installSettingsSection` из `@deepseek-ai/dsh-settings`. Карточка конфигурации на странице плагина (клиентская половина) присутствует только на 0.1.7+/desktop и на старых линиях изящно отсутствует.

Возможности инструмента идентичны на каждой линии: один инструмент `apply_patch`, git/unified diff по умолчанию плюс Codex по явному включению, применение всё-или-ничего, intent-танец «сначала прочитай, потом пиши» (на линиях, где в `fs` нет write-intent, запись деградирует до прямой с одним `console.warn`) и sandbox-осознанные delete/move.

## Зачем вообще патчи? (руководство по маршрутизации инструментов)

Описание инструмента явно направляет модель:

- **Одиночное небольшое изменение в уже прочитанном файле** → предпочитайте нативный инструмент `edit` (патч стоит больше токенов, чем буквальное редактирование).
- **Небольшое изменение внутри БОЛЬШОГО файла** → `apply_patch` (не нужно повторять весь файл).
- **Пакетные изменения во МНОГИХ файлах** → `apply_patch` (один вызов, атомарная проверка).
- **Новый файл с существенным содержимым / удаление / переименование** → `apply_patch`.

## Установка

```bash
dsh plugin --profile web add <path-to-dsh-patch-edit-plus>
dsh web --dump-config   # verify the plugin row appears
# restart DSH
```

## Использование

### Unified diff (по умолчанию, включён)

```text
--- a/src/app.ts
+++ b/src/app.ts
@@ -10,4 +10,4 @@ export function main() {
   init()
-  start(oldPort)
+  start(newPort)
   await shutdown()
 }
```

### Синтаксис apply_patch от Codex (по явному включению)

```text
*** Begin Patch
*** Update File: src/app.ts
@@
   init()
-  start(oldPort)
+  start(newPort)
*** Add File: docs/notes.md
+# Notes
*** Delete File: tmp/junk.txt
*** Move to: src/app.ts   (via `*** Move to:` after *** Update File:)
*** End Patch
```

Включается через `allowCodexPatch: true`. Если патч Codex приходит при выключенной опции, инструмент возвращает **действенную подсказку** (как её включить или как повторить попытку в unified diff) вместо общей ошибки разбора — синтаксис Codex является сильным априорным признаком для моделей семейства GPT, и молчаливый сбой затягивает их в циклы повторов.

### Операции

| Операция | Unified diff | Codex | Путь выполнения |
|---|---|---|---|
| Добавление | `new file mode` + `/dev/null` | `*** Add File:` | `ctx.fs.writeText` (официальная intent-последовательность) |
| Обновление (multi-hunk) | `@@` hunks | `@@` hunks | `ctx.fs.writeText` |
| Удаление | `deleted file mode` + `/dev/null` | `*** Delete File:` | `ctx.shell` (с учётом sandbox, путь передаётся через env) |
| Перемещение / переименование | `rename from/to` | `*** Move to:` | `ctx.shell` |

## Гарантии

- **Всё-или-ничего**: весь патч сначала проверяется на соответствие текущему содержимому файлов (локализация контекста, удержание в рабочей области, политика symlink, дубликаты путей); любой сбой прерывает операцию без записи хотя бы байта.
- **Соблюдение запрета на запись без чтения**: каждая запись воспроизводит intent-последовательность официального инструмента `write` — водопад `fs/write-intent` → защищённый `writeText` → эмиссия `fs/observed`. Голый `writeText` молча обходил бы этот барьер, поскольку провайдеры DSH никогда сами не диспетчеризуют события `fs/*`.
- **Точная диагностика сбоев**: при несовпадении хунка ошибка содержит файл + индекс хунка, исходную строку поиска, превью ожидаемой строки (с показанными пробелами), фактический фрагмент файла и адресную подсказку.
- **Точность переводов строк**: файлы CRLF записываются обратно как CRLF; окончания без завершающего перевода строки сохраняются, если только патч не меняет окончание.
- **Чистое дополнение**: плагин регистрирует ровно один инструмент, никогда не вызывает `tools.restrict()`, никогда не переопределяет нативный инструмент, не регистрирует глобальных служб и при выгрузке удаляет всё. Конфликты имён с другими провайдерами `apply_patch` (`bainianlaoyao/dsh-codex-mode`, `shuind/dsh-codex-harness`, …) разрешаются избеганием переименования (`apply_patch_1`, `…_2`, …), так что DSH никогда не падает при старте.

## Конфигурация

| Опция | По умолчанию | Описание |
|---|---|---|
| `toolName` | `apply_patch` | Имя инструмента, доступного модели. |
| `conflictPolicy` | `rename` | `rename` / `skip` / `fail`, если имя инструмента уже занято. |
| `renameSuffix` | `_1` | Суффикс, используемый при избегании переименования. |
| `allowUnifiedDiff` | `true` | Принимать git/unified diff. |
| `allowCodexPatch` | `false` | Принимать синтаксис `apply_patch` от Codex. Также редактируется в панели настроек DSH; изменения вступают в силу немедленно, без перезапуска. |
| `deleteBackend` | `shell` | `shell` или `none` (Delete/Move возвращают структурированную ошибку). |
| `shellDialect` | `auto` | `auto` (pwsh на win32) / `posix` / `pwsh`. |
| `deleteCommand` / `moveCommand` | built-in | Пользовательские шаблоны команд. Пути по-прежнему передаются **только через переменные окружения** (`DSH_PATCH_TARGET` / `DSH_PATCH_SOURCE`); никогда не подставляйте их в строку команды интерполяцией. |
| `dryRunByDefault` | `false` | Считать вызовы пробными запусками (dry run), если не задано `dryRun: false`. |
| `followSymlinks` | `false` | Разрешить путям патча переходить по symlink. |
| `maxFiles` | `50` | Максимум файловых секций на один патч. |
| `maxPatchBytes` | `524288` | Максимальный размер текста патча в байтах. |
| `maxDiffBytes` | `16384` | Максимальный размер диффа на файл, сохраняемого в метаданных результата (большие диффы опустошаются и помечаются `truncated`). |

## Ограничения (раскрытые)

- **Delete/Move выполняются через `ctx.shell`.** Сила sandbox такова, каков обеспечивающий её загруженный shell-исполнитель (`bash-sandbox` изолирует; `bash-local` — нет) — та же позиция риска, что и у нативного инструмента bash. Каждый запрос несёт `sandboxPolicy` и сообщает факты о sandbox, чтобы «отказ политики» можно было отличить от «сбоя команды».
- **Каждая запись и каждый Delete/Move несут политику в области действия сессии.** `apply_patch` разрешает `ctx.sandboxPolicy.resolve({ session })` при каждом вызове — переопределение режима сессии плюс её cwd как корень рабочей области — в точности как нативные инструменты `write`/`edit`, и разрешает пути плана относительно того же корня. Без этого файловая система принуждения откатывается к корню развёртывания, из-за чего записи внутри рабочей области завершаются отказами `workspace-write` даже в сессии `danger-full-access`. Отказ `workspace-write` проявляется как `PatchError`, сообщение которой несёт текст бэкенда (структурированный маркер `[sandbox: …]` и поля эскалации в том же ходе не реализованы; если нужна эскалация, пользуйтесь нативными `write`/`edit`).
- **Add не создаёт родительские каталоги.** Это соответствует нативному инструменту `write` (у `ctx.fs` нет mkdir); в ошибке называется недостающий каталог.
- Нет нечёткого/со смещением сопоставления: локализация хунка точная → `trimEnd` → `trim`, причём намеренно (нечёткое сопоставление небезопасно для разрушающих операций; оно есть в дорожной карте).
- Двоичные патчи отклоняются с понятной ошибкой.

## Разработка

```bash
npm install
npm run typecheck        # against 0.2.1-rc.1 peers (devDependencies)
npm run typecheck:0.2.1  # against real 0.2.1-rc.1 peer packages installed from the registry (dual-baseline proof)
npm test                 # vitest, 92 tests
npm run lint
npm run build            # lib/
npm run verify:source    # static safety assertions (intent dance, no node:fs, …)
npm run smoke            # load-level smoke against a stub host
```

Ручная проверка на живом DSH (`dsh plugin --profile web add` → перезапуск → вызов инструмента в сессии) намеренно оставлена оператору.

## Лицензия

MIT
