| 1 | # ADR 0111: `LineNumber` и `LineRange` как доменные типы редактора |
| 2 | |
| 3 | ## Связанные ADR |
| 4 | |
| 5 | | ADR | Роль | |
| 6 | |-----|------| |
| 7 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | Семантика параметрики по строкам; 0111 — VO LR/LN в домене IDE | |
| 8 | | [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) | Каталог, `tail_signature`; `:ln` — метаданные каталога | |
| 9 | | [0110](0110-roslyn-refactor-intent-melody-bridge.md) | Roslyn по диапазону; будущий мост к аргументам MCP | |
| 10 | |
| 11 | ## Резюме |
| 12 | |
| 13 | - Value objects **`LineNumber`** / **`LineRange`** (1-based, Start ≤ End). |
| 14 | - Граница к JSON/командам — `int` в args; `ParsedLineRange` для парсинга. |
| 15 | |
| 16 | |
| 17 | ## Отклонённые альтернативы |
| 18 | |
| 19 | - **Только алиасы `:ln` в TOML** — не заменяют проверки и самодокументацию API в C#. |
| 20 | - **`record` с int без инвариантов** — дублирование проверок в каждом потребителе. |
| 21 | - **Полный переход команд на typed DTO вместо JSON** — вне scope v1; граница остаётся на сериализации. |
| 22 | - **Спец-токены «до конца файла»** (`7:end`, `7:*` и т.п.) — не входят в v1: отдельная мини-грамматика, взаимодействие с длиной файла на parse vs build, риск коллизий; оставлено на отдельное решение при явной потребности. |
| 23 | |
| 24 | ## Последствия (текущие) |
| 25 | |
| 26 | - Новые сценарии **только по строкам** в ветке parametric melody → предпочтительно через LR/LN. |
| 27 | - Проверка «строки есть в файле» остаётся в **`ParametricLineRangeArgsBuilder`**, не в порядке двух чисел во вводе и не в LN/LR как таковых. |
| 28 | |
| 29 | --- |
| 30 | ## Решение |
| 31 | |
| 32 | - Ввести **value objects** в пространстве имён `CascadeIDE.Models.Editor`: |
| 33 | - **LN (`LineNumber`)** — 1-based номер строки; инвариант `Value >= LineNumber.MinimumOneBasedInclusive` (1); создание через `TryCreate(int, out LineNumber)`; операторы сравнения (в т.ч. для CA1036 / `IComparable`). |
| 34 | - **LR (`LineRange`)** — пара `Start` / `End` типа LN с инвариантом `Start <= End` (инклюзивный диапазон в терминах редактора и IDE-команд); создание через `TryCreate(LineNumber, LineNumber, out LineRange)`. |
| 35 | - **`ParametricIntentMelody.ParsedLineRange`** хранит диапазон как **`LineRange Lines`**, а не два «голых» `int`. |
| 36 | - Парсер хвоста melody (`TryExtractLineRangeFromRemainder` в `ParametricIntentMelody`): |
| 37 | - **Одно целое** без второго слота — **одна строка** (`<line>` по смыслу совпадает с `<line>:<line>` как LR). |
| 38 | - **Два целых** — границы одного инклюзивного диапазона; порядок ввода не важен: после разбора применяется **`min..max`** (например `7:3` и `3:7` → один LR). |
| 39 | - У **`LineRange.TryCreate`** для **явного кода** контракт по-прежнему «второй аргумент не раньше первого»; «перевёрнутый» ввод обрабатывается **до** вызова `TryCreate`, за счёт нормализации в парсере. |
| 40 | - На границе к JSON (**`ParametricLineRangeArgsBuilder`**) в анонимные DTO по-прежнему уходят **`int`** через `.Value` у LN — **wire-формат** `IdeCommands.Select` / `IdeCommands.ApplyEdit` не меняется. |
| 41 | - **`IntentMelodyTailSemantics.MinEditorLineNumber`** согласован с **`LineNumber.MinimumOneBasedInclusive`** (одна константа-источник для «минимальной строки 1-based»). |
| 42 | |
| 43 | ## Реализация v1 (источники в коде) |
| 44 | |
| 45 | | Артефакт | Назначение | |
| 46 | |----------|------------| |
| 47 | | `Models/Editor/LineNumber.cs` | LN, константа минимума, `TryCreate`, равенство и сравнение | |
| 48 | | `Models/Editor/LineRange.cs` | LR, `TryCreate` при `End >= Start` | |
| 49 | | `Services/ParametricIntentMelody.cs` | `ParsedLineRange`, `TryParseLineRangeTail`, `TryExtractLineRangeFromRemainder`, делегирование в `ParametricLineRangeArgsBuilder` | |
| 50 | | `Services/ParametricLineRangeArgsBuilder.cs` | Сборка JSON-args из `ParsedLineRange.Lines` + проверка вылета за длину файла | |
| 51 | | `Services/IntentMelodyTailSemantics.cs` | `MinEditorLineNumber` → ссылка на минимум LN | |
| 52 | | `CascadeIDE.Tests/EditorLineNumberRangeTests.cs` | Юнит-тесты LN/LR | |
| 53 | | `CascadeIDE.Tests/ParametricIntentMelodyTests.cs` | Парсинг, одна строка, `min..max`, сборка args | |
| 54 | | `IntentMelody/intent-melody-aliases.toml` и копия в `publish-gh-release/IntentMelody/` | Подсказки палитры для `els` / `eld` (диапазон и одна строка) | |
| 55 | |
| 56 | Каталог по-прежнему описывает **два** числовых слота в `tail_signature` (`<start:ln>:<end:ln>`); сокращение «одно число» — **соглашение парсера приложения**, не отдельная строка каталога. |
| 57 | |
| 58 | ## Реализация v2 (дорожная карта после v1 — закрыта в коде) |
| 59 | |
| 60 | | Артефакт | Назначение | |
| 61 | |----------|------------| |
| 62 | | `Models/Editor/ColumnNumber.cs` | CN — 1-based колонка (`TryCreate`, сравнение) | |
| 63 | | `Models/Editor/EditorDocumentPath.cs` | Обёртка пути документа: `CanonicalFilePath.TryNormalize`, сравнение без регистра | |
| 64 | | `Models/Editor/EditorMcpSpans.cs` | `EditorTextSpan.TryParse`, `EditorContentLineRangeMcpArgs.TryParse`, `EditorGoToPositionMcpArgs.TryParse` | |
| 65 | | `Services/RoslynLinePositionMapper.cs` | `Microsoft.CodeAnalysis.Text.LinePosition` (0-based) → `(LineNumber, ColumnNumber)` для UI/MCP | |
| 66 | | `Services/ContextMinimizer.cs`, `Services/WorkspaceDiagnosticsCoordinator.cs` | Используют маппер вместо дублирования `+1` | |
| 67 | | `ViewModels/IdeMcpCommandExecutor.Handlers.Editor/*.cs` | `Select` / `ApplyEdit` / `GoToPosition` / `GetEditorContentRange` через общий разбор VO | |
| 68 | | `Services/ParametricLineRangeArgsBuilder.cs` | Границы колонок через `ColumnNumber` + канонический `EditorDocumentPath` | |
| 69 | | `Features/WebAiPortal/Application/WebAiPortalChatMixInFormatter.cs` | Снимок диапазона редактора: `LineRange` вместо «голых» int строк | |
| 70 | | `CascadeIDE.Tests/EditorMcpSpansTests.cs`, расширение `EditorLineNumberRangeTests` | Отказы и границы парсера MCP | |
| 71 | |
| 72 | ## Связанные ADR |
| 73 | |
| 74 | | ADR | Роль | |
| 75 | |-----|------| |
| 76 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | Семантика параметрики по строкам; 0111 — VO LR/LN в домене IDE | |
| 77 | | [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) | Каталог, `tail_signature`; `:ln` — метаданные каталога | |
| 78 | | [0110](0110-roslyn-refactor-intent-melody-bridge.md) | Roslyn по диапазону; будущий мост к аргументам MCP | |
| 79 | |
| 80 | ## Отклонённые альтернативы |
| 81 | |
| 82 | - **Только алиасы `:ln` в TOML** — не заменяют проверки и самодокументацию API в C#. |
| 83 | - **`record` с int без инвариантов** — дублирование проверок в каждом потребителе. |
| 84 | - **Полный переход команд на typed DTO вместо JSON** — вне scope v1; граница остаётся на сериализации. |
| 85 | - **Спец-токены «до конца файла»** (`7:end`, `7:*` и т.п.) — не входят в v1: отдельная мини-грамматика, взаимодействие с длиной файла на parse vs build, риск коллизий; оставлено на отдельное решение при явной потребности. |
| 86 | |
| 87 | ## Последствия (текущие) |
| 88 | |
| 89 | - Новые сценарии **только по строкам** в ветке parametric melody → предпочтительно через LR/LN. |
| 90 | - Проверка «строки есть в файле» остаётся в **`ParametricLineRangeArgsBuilder`**, не в порядке двух чисел во вводе и не в LN/LR как таковых. |
| 91 | |
| 92 | --- |
| 93 | |
| 94 | ## Дорожная карта (после v1) — выполнено (v2) |
| 95 | |
| 96 | Цель итераций v2 — **тот же паттерн**: инварианты в типах до границы JSON/MCP, без смены wire-контрактов `IdeCommands`. |
| 97 | |
| 98 | ### Приоритет 1 — граница MCP → редактор — **сделано** |
| 99 | |
| 100 | - **`EditorTextSpan`** + **`ColumnNumber`**, **`TryParse`** из `IReadOnlyDictionary<string, JsonElement>` для `Select` / `ApplyEdit`; **`EditorGoToPositionMcpArgs`** для `go_to_position`; **`EditorContentLineRangeMcpArgs`** для `get_editor_content_range` (при отсутствии ключей — 1..1 как раньше; инвертированный явный диапазон — отказ с сообщением). |
| 101 | |
| 102 | ### Приоритет 2 — веб-портал — **сделано** |
| 103 | |
| 104 | - **`WebAiPortalChatMixInFormatter`**: `EditorRangeSnap` хранит **`LineRange`**; разбор JSON нормализует границы через `min/max` при «перевёрнутых» int в wire-ответе. |
| 105 | |
| 106 | ### Приоритет 3 — parametric args — **сделано** |
| 107 | |
| 108 | - **`ParametricLineRangeArgsBuilder`**: колонки через **`ColumnNumber.TryCreate`**, путь через **`EditorDocumentPath`**. |
| 109 | |
| 110 | ### Приоритет 4 — Roslyn — **сделано** |
| 111 | |
| 112 | - **`RoslynLinePositionMapper`**: `LinePosition` (0-based) → `(LineNumber, ColumnNumber)`; **`ContextMinimizer`**, **`WorkspaceDiagnosticsCoordinator`**. |
| 113 | |
| 114 | ### Путь к файлу — **сделано** |
| 115 | |
| 116 | - **`EditorDocumentPath`** поверх **`CanonicalFilePath.TryNormalize`**. |
| 117 | |
| 118 | ### Критерий готовности |
| 119 | |
| 120 | - Два call site с одним набором полей JSON (`Select` + `ApplyEdit`) → **`EditorTextSpan`**; тесты в **`EditorMcpSpansTests`**. |
| 121 | |