| 1 | # ADR 0053: Карта намерений и поток управления на PFD (control flow) |
| 2 | |
| 3 | **Статус:** Accepted · Implemented |
| 4 | **Дата:** 2026-04-17 |
| 5 | |
| 6 | ## Связанные ADR |
| 7 | |
| 8 | | ADR / артефакт | Роль | |
| 9 | |----------------|------| |
| 10 | | [0021](0021-pfd-mfd-cockpit-attention-model.md) | PFD как приборы, зоны внимания | |
| 11 | | [0051](0051-intent-based-attention-routing-toml.md) | Маршрутизация внимания | |
| 12 | | [0055](0055-skia-instrument-composition-pipeline.md) | Skia pipeline | |
| 13 | | [0056](0056-semantic-map-pipeline-adoption.md) | Внедрение pipeline в карту | |
| 14 | | [0039](0039-workspace-navigation-affordances.md) | MCP, subgraph | |
| 15 | | [0065](0065-instrument-categories-domain-taxonomy.md) | Карта намерений кода | |
| 16 | | `CodeNavigationMapSubgraph*` | Модели subgraph в коде | |
| 17 | |
| 18 | ## Резюме |
| 19 | |
| 20 | - **Карта намерений** на PFD: control flow, subgraph, KISS-навигация вокруг якоря кода. |
| 21 | - Общий Skia pipeline и cursor semantics ([0056](0056-semantic-map-pipeline-adoption.md)). |
| 22 | - Не путать с GitMap ([0062](0062-git-submodules-semantic-map-subgraph.md)) и с полным solution tree. |
| 23 | |
| 24 | --- |
| 25 | ## Контекст |
| 26 | |
| 27 | Семантическая карта в зоне PFD сегодня опирается на **граф зависимостей** (символы, вызовы, связи между артефактами). При редактировании **одного метода** `A()` полезно видеть не только «кто с кем связан», но и **маршрут выполнения** внутри метода: условные вызовы, общий хвост после ветвлений. |
| 28 | |
| 29 | Пример намерения: |
| 30 | |
| 31 | ```csharp |
| 32 | void A() { |
| 33 | if (cond) B(); |
| 34 | else C(); |
| 35 | D(); |
| 36 | } |
| 37 | ``` |
| 38 | |
| 39 | На карте (упрощённо): **B** при условии, **C** на альтернативной ветке, **D** как точка схождения / общий продолжение — без превращения экрана в полный CFG или копию текста из редактора. |
| 40 | |
| 41 | Аналогия с авиацией: **waypoints** и условия перехода между ними на навигационном дисплее, а не распечатка всех процедурных страниц. |
| 42 | |
| 43 | --- |
| 44 | |
| 45 | ## Цели |
| 46 | |
| 47 | 1. Визуализировать **намерение потока управления** внутри выбранного метода там, где это помогает сканированию (Flight / «тонкий» режим), а не синтаксис ради синтаксиса. |
| 48 | 2. Сохранить **KISS**: не перегружать PFD текстом условий и декорациями по умолчанию. |
| 49 | 3. Подготовить **контракт данных** (subgraph / JSON / MCP), чтобы рёбра и при необходимости узлы несли **тип связи** и опциональные подсказки для hover / drill-down. |
| 50 | |
| 51 | ## Не-цели (на первом этапе) |
| 52 | |
| 53 | - Полная отрисовка **всего** CFG Roslyn на мини-карте. |
| 54 | - Дублирование **тела** условия `if (…)` постоянной подписью на PFD. |
| 55 | - Визуализация каждого локального присваивания и мелкого шума внутри веток. |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## Принципы отображения (чтобы не «засрать» интерфейс) |
| 60 | |
| 61 | 1. **Семантическая компрессия** |
| 62 | По умолчанию: **иконка ветвления** (ромб / «?») или **тонкая развилка** на ребре, **без** длинного текста предиката — для **`if` и для `switch` / pattern matching** одинаково (см. [§ «Switch…»](#adr0053-switch)). |
| 63 | **Деталь условия** — по задержке взгляда / hover / отдельный слой (Skia tooltip), опционально. |
| 64 | |
| 65 | 2. **Только значимые «внешние» точки маршрута** |
| 66 | В приоритете: вызовы **других методов** и тяжёлые операции, которые пользователь ищет глазами. Локальный шум (`i++`, пустой `return` без смысла для карты) — не поднимать на граф без явной политики. |
| 67 | |
| 68 | 3. **Тонкие линии и метафоры** |
| 69 | - **Цикл** (`for` / `while`): см. [§ «Циклы: петля на ребре»](#adr0053-loop-edge). |
| 70 | - **`try` / `catch`**: позже — стиль «защищённого» участка (например «зонтик» / нестабильный контур узла), когда будет согласована лексика. |
| 71 | |
| 72 | 4. **Вертикальный «полётный план»** |
| 73 | Узлы значимых шагов сверху вниз, ветвления — **в сторону**, без блоков «как в IDE» на весь экран. |
| 74 | |
| 75 | <a id="adr0053-loop-edge"></a> |
| 76 | |
| 77 | ### Циклы (for / while): петля на ребре |
| 78 | |
| 79 | **Не рисуем** отдельный «граф цикла» как на классической блок-схеме (ромб + стрелка назад, дублирующая узлы). **Рисуем петлю на линии связи** к значимому шагу внутри метода (например вызов `B()` в теле `for`/`while`): линия к узлу **не прямая**, а делает **изящный виток** вокруг направления к этому узлу — по духу как **орбита ожидания / holding** на навигационном дисплее, а не как «ещё один прямоугольник». |
| 80 | |
| 81 | Смысл для пользователя: **сразу видно** — «этот вызов здесь крутится по кругу», без чтения кода в Forward и без разворачивания итераций на карте. |
| 82 | |
| 83 | **Рендер (направление):** тонкие **неоновые** линии на тёмном фоне PFD, **сглаженные кривые** (Bezier / кубические сплайны в Skia), чтобы визуально быть частью **кокпитного** слоя, а не старой блок-схемы. Центральная линия — условный «основной поток» текущего метода `A()`; виток — **на ребре** к зависимому шагу, не отдельный декоративный слой поверх всего графа. |
| 84 | |
| 85 | *Срез реализации (2026-05):* `Views/SkiaKit/Graph/SkiaGraphSceneDrawing.Edges.cs` — для kind с `loop` одна пунктирная кубическая кривая к цели с увеличенным поперечным выносом (в т.ч. когда хорда почти параллельна главному потоку); отдельный эллипс вокруг целевого узла для этого не рисуется. Пунктирный овал группы цикла — `SkiaGraphSceneDrawing.LoopRegions.cs`, только если в `LoopGroupId` **не меньше двух узлов**. При высокой плотности графа пайплайн карты задаёт «компактный» режим (`GraphLayoutEngineOptions.IsDense` + больший минимальный viewport по высоте). |
| 86 | |
| 87 | **Опционально (если анализ позволяет эвристику):** «тяжесть» цикла или ожидаемое число итераций — **плотность** витка (ближе спираль, чуть ярче/толще линия и т.п.), без обещания точного `n` на PFD. |
| 88 | |
| 89 | **Контракт:** ребро с семантикой цикла несёт признак вроде `Loop` / `LoopCall` + опционально метаданные для стиля (см. таблицу ниже); координаты витка — **производные от layout** (StarGraph / force / иной движок), чтобы кривые не перекрывали соседние узлы — отдельная задача подбора контрольных точек Bezier. |
| 90 | |
| 91 | <a id="adr0053-switch"></a> |
| 92 | |
| 93 | ### Switch, `case` и сопоставление с образцом |
| 94 | |
| 95 | **Включаем** в ту же семантику, что и `if` / `else`: это ветвление потока, не «особый случай вне карты». **Не разворачиваем** все ветки как полную таблицу на PFD. |
| 96 | |
| 97 | - **Немного значимых исходов (ориентир 2–4):** веер рёбер от одной точки развилки к значимым шагам; подписи **`case` / guard** — опционально, по hover или второму слою. |
| 98 | - **Много веток или одни «пустые» case:** **сжатие** — один узел/иконка многонаправленного ветвления; наружу только вызовы с смыслом; прочее — агрегат («остальные ветки» / `default`) или скрыто до drill-down. В коде: **glance** — все `MultiBranch` срезаются; **normal** — при веере ≥4 от одной развилки остаются 3 приоритетных исхода + ромб `+N` (`CodeNavigationMapControlFlowDeclutter`); **inspect** — без сжатия. |
| 99 | - **Pattern matching** (`switch` expression, `when`): не дублировать длинные выражения на постоянной подписи; тот же принцип, что для предиката `if`. |
| 100 | |
| 101 | Связь с контрактом: тип вроде **`MultiBranch`** / **`ConditionalCall`** на ребре от общего предка развилки к шагу; детали ветки — в опциональных метаданных. |
| 102 | |
| 103 | --- |
| 104 | |
| 105 | ## Данные и контракт (направление) |
| 106 | |
| 107 | Существующие модели subgraph (`CodeNavigationMapSubgraphNode`, `CodeNavigationMapSubgraphEdge`) расширяются осмысленно, например: |
| 108 | |
| 109 | | Идея | Назначение | |
| 110 | |------|------------| |
| 111 | | Тип ребра | `Call`, `ConditionalCall`, `Merge`, `MultiBranch` (несколько исходов: `switch`, цепочка `if`, pattern matching), `Loop` / `LoopCall` (петля на ребре к шагу внутри цикла; не путать с низкоуровневым `LoopBack` CFG, если понадобится отдельно) | |
| 112 | | Ветви `if` | `edge_provenance`: `cf_branch_true` / `cf_branch_false`; подпись — `condition_branch_label_preset` = `id`; слои merge: бандл `CodeNavigation/condition-branch-label-presets.toml` → `.cascade/workspace.toml` `[[code_navigation_map.condition_branch.presets]]` → `%LocalAppData%` settings (как [0039](0039-workspace-navigation-affordances.md) для `code_navigation`); `custom` — `condition_branch_positive` / `condition_branch_negative` на `[code_navigation_map]`. | |
| 113 | | Краткая метка | Опционально; не дублировать полный текст `cond`. | |
| 114 | | Деталь условия | Опционально, для tooltip / второго слоя; может быть сжата или отложена. | |
| 115 | |
| 116 | Точные имена полей и JSON — **зафиксировать** после прототипа генератора (Roslyn / Control Flow Analysis + фильтрация вызовов). |
| 117 | |
| 118 | Источник анализа в стеке: **Roslyn** (`ControlFlowAnalysis` и привязка вызовов к веткам), без обязательной привязки к одному только текстовому grep. |
| 119 | |
| 120 | --- |
| 121 | |
| 122 | ## Договорённости (черновик) |
| 123 | |
| 124 | ### Предикат на ребре vs только иконка |
| 125 | |
| 126 | Граница «показать **краткий предикат**» и «**только иконка**» задаётся **настройкой пользователя** (приложение / workspace — конкретный ключ и merge с bundle зафиксировать при внедрении), **а не только** жёсткой привязкой к режиму UI вроде Flight. |
| 127 | |
| 128 | Контур **агента** (MCP, запрос контекста / subgraph навигации) опирается на **тот же** уровень детализации: в продуктовом смысле агент — тоже **пользователь** этого представления и не живёт в отдельном скрытом режиме в обход настроек, кроме как при **явном** параметре в контракте вызова (override на один запрос). |
| 129 | |
| 130 | ### Тип карты: `controlFlow` vs «классическая» |
| 131 | |
| 132 | Отдельный пресет **UiMode** (например только Flight) **под одну** CF-aware карту не обязателен: **какой вид семантической карты строить** — выбор **пользователя**, ортогонально режиму кабины. Режим **Flight** остаётся полигоном раскладки PFD; **уровень/тип** карты задаётся отдельно. |
| 133 | |
| 134 | Черновик формы в TOML (значения и merge с bundle / workspace — при внедрении): |
| 135 | |
| 136 | ```toml |
| 137 | [semantic_map] |
| 138 | level = "controlFlow" # поток управления в методе (CF-aware subgraph) |
| 139 | # level = "file" # классическая зависимостная / файловый срез (как baseline карты намерений) |
| 140 | ``` |
| 141 | |
| 142 | Тот же переключатель должен быть **достижим для агента** (поле в MCP / эхо в subgraph-запросе), без отдельной скрытой семантики. |
| 143 | |
| 144 | ### Версионирование subgraph JSON (MCP / CLI) |
| 145 | |
| 146 | **Обратная совместимость при добавлении полей пока не гарантируется:** у продукта **ещё нет пользователей**, JSON subgraph и MCP/CLI могут **менять форму синхронно с кодом** без политики миграции. Когда появятся стабильные потребители вне репозитория (интеграции, долгоживущий контракт агента) — ввести явное **версионирование** схемы и правила совместимости **отдельным решением** (ADR / дополнение к MCP-PROTOCOL). |
| 147 | |
| 148 | --- |
| 149 | |
| 150 | ## Следующие шаги (черновик) |
| 151 | |
| 152 | 1. Зафиксировать минимальный набор **kind** для рёбер и правила фильтрации вызовов. |
| 153 | 2. Прототип: один метод под курсором → упрощённый поток → тот же JSON, который уже потребляет карта намерений на PFD. |
| 154 | 3. Рендер: стили линий и узлов на PFD / Skia без обязательного текста условия на постоянной подписи. |
| 155 | |
| 156 | ## Backlog intent (добавить поэтапно) |
| 157 | |
| 158 | Чтобы карта сохраняла intent и не превращалась в полный CFG, расширения вводим порциями: |
| 159 | |
| 160 | 1. **Early/abrupt exits**: `ThrowExit`, `Break`, `Continue`. |
| 161 | 2. **Асинхронные границы**: `AwaitBoundary` (точка паузы/возобновления потока). |
| 162 | 3. **Короткое замыкание**: `ShortCircuit` для `&&` / `||` в guard-условиях. |
| 163 | 4. **Pattern guard**: явная семантика для `when` в pattern matching/switch. |
| 164 | 5. **Исключения**: укрупнённый `ExceptionFlow` для `catch`/`finally` без разворота полного exception-CFG. |
| 165 | 6. **Итераторы**: `YieldExit` (`yield return` / `yield break`) как отдельный тип выхода. |
| 166 | 7. **Политика детализации**: разделить «операторный шум» и «внешние шаги» через declutter policy (например, helper-вызовы внутри аргументов). |
| 167 | |
| 168 | *(Ниже можно добавлять разделы: примеры JSON, скринмоки, ограничения производительности, ссылки на внешние обсуждения.)* |
| 169 | |