| 1 | # ADR 0053: Intent map and control flow on PFD (control flow) |
| 2 | |
| 3 | **Status:** Accepted · Implemented |
| 4 | **Date:** 2026-04-17 |
| 5 | |
| 6 | ## Related ADRs |
| 7 | |
| 8 | | ADR / artifact | Role | |
| 9 | |----------------|------| |
| 10 | | [0021](0021-pfd-mfd-cockpit-attention-model.md) | PFD as instruments, attention zones | |
| 11 | | [0051](0051-intent-based-attention-routing-toml.md) | Attention routing | |
| 12 | | [0055](0055-skia-instrument-composition-pipeline.md) | Skia pipeline | |
| 13 | | [0056](0056-semantic-map-pipeline-adoption.md) | Pipeline adoption on the map | |
| 14 | | [0039](0039-workspace-navigation-affordances.md) | MCP, subgraph | |
| 15 | | [0065](0065-instrument-categories-domain-taxonomy.md) | Code intent map | |
| 16 | | `CodeNavigationMapSubgraph*` | Subgraph models in code | |
| 17 | |
| 18 | ## Summary |
| 19 | |
| 20 | - **Intent map** on PFD: control flow, subgraph, KISS navigation around a code anchor. |
| 21 | - Shared Skia pipeline and cursor semantics ([0056](0056-semantic-map-pipeline-adoption.md)). |
| 22 | - Not to be confused with GitMap ([0062](0062-git-submodules-semantic-map-subgraph.md)) or the full solution tree. |
| 23 | |
| 24 | --- |
| 25 | ## Context |
| 26 | |
| 27 | The semantic map in the PFD zone today relies on a **dependency graph** (symbols, calls, links between artifacts). When editing **a single method** `A()`, it is useful to see not only “who connects to whom”, but also the **execution route** inside the method: conditional calls, shared tail after branches. |
| 28 | |
| 29 | Example intent: |
| 30 | |
| 31 | ```csharp |
| 32 | void A() { |
| 33 | if (cond) B(); |
| 34 | else C(); |
| 35 | D(); |
| 36 | } |
| 37 | ``` |
| 38 | |
| 39 | On the map (simplified): **B** on the conditional path, **C** on the alternate branch, **D** as a merge point / shared continuation — without turning the screen into a full CFG or a copy of editor text. |
| 40 | |
| 41 | Aviation analogy: **waypoints** and transition conditions on a navigation display, not a printout of every procedural page. |
| 42 | |
| 43 | --- |
| 44 | |
| 45 | ## Goals |
| 46 | |
| 47 | 1. Visualize **control-flow intent** inside the selected method where it helps scanning (Flight / “thin” mode), not syntax for its own sake. |
| 48 | 2. Preserve **KISS**: do not overload the PFD with condition text and decorations by default. |
| 49 | 3. Prepare a **data contract** (subgraph / JSON / MCP) so edges and, when needed, nodes carry **link type** and optional hints for hover / drill-down. |
| 50 | |
| 51 | ## Non-goals (first phase) |
| 52 | |
| 53 | - Full rendering of the **entire** Roslyn CFG on the mini-map. |
| 54 | - Duplicating the **body** of `if (…)` as a permanent label on the PFD. |
| 55 | - Visualizing every local assignment and small noise inside branches. |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## Display principles (keep the UI clean) |
| 60 | |
| 61 | 1. **Semantic compression** |
| 62 | By default: a **branch icon** (diamond / “?”) or a **thin fork** on the edge, **without** long predicate text — for **`if` and for `switch` / pattern matching** alike (see [§ Switch…](#adr0053-switch)). |
| 63 | **Condition detail** — on gaze dwell / hover / a separate layer (Skia tooltip), optional. |
| 64 | |
| 65 | 2. **Only meaningful “external” route points** |
| 66 | Priority: calls to **other methods** and heavy operations the user scans for. Local noise (`i++`, empty `return` with no map meaning) — do not lift onto the graph without an explicit policy. |
| 67 | |
| 68 | 3. **Thin lines and metaphors** |
| 69 | - **Loop** (`for` / `while`): see [§ Loops: loop on the edge](#adr0053-loop-edge). |
| 70 | - **`try` / `catch`**: later — a “protected” segment style (e.g. “umbrella” / unstable node outline) once vocabulary is agreed. |
| 71 | |
| 72 | 4. **Vertical “flight plan”** |
| 73 | Meaningful step nodes top to bottom, branches **to the side**, without IDE-style blocks filling the screen. |
| 74 | |
| 75 | <a id="adr0053-loop-edge"></a> |
| 76 | |
| 77 | ### Loops (for / while): loop on the edge |
| 78 | |
| 79 | **We do not draw** a separate “loop graph” like a classic flowchart (diamond + back arrow duplicating nodes). **We draw a loop on the link line** to a meaningful step inside the method (e.g. call `B()` in a `for`/`while` body): the line to the node is **not straight** but makes an **elegant coil** around the direction toward that node — in spirit like a **holding orbit** on a nav display, not “yet another rectangle”. |
| 80 | |
| 81 | User meaning: **immediately obvious** — “this call spins here”, without reading code in Forward and without unrolling iterations on the map. |
| 82 | |
| 83 | **Render (direction):** thin **neon** lines on the dark PFD background, **smooth curves** (Bezier / cubic splines in Skia), visually part of the **cockpit** layer, not an old flowchart. The central line is the conditional “main flow” of method `A()`; the coil is **on the edge** to a dependent step, not a decorative layer over the whole graph. |
| 84 | |
| 85 | **Optional (when analysis allows a heuristic):** loop “weight” or expected iteration count — **coil density** (tighter spiral, slightly brighter/thicker line, etc.), without promising exact `n` on the PFD. |
| 86 | |
| 87 | **Contract:** an edge with loop semantics carries a flag like `Loop` / `LoopCall` plus optional style metadata (see table below); coil coordinates are **derived from layout** (StarGraph / force / other engine) so curves do not cover neighboring nodes — Bezier control-point tuning is a separate task. |
| 88 | |
| 89 | <a id="adr0053-switch"></a> |
| 90 | |
| 91 | ### Switch, `case`, and pattern matching |
| 92 | |
| 93 | **Include** in the same semantics as `if` / `else`: this is flow branching, not a “special case off the map”. **Do not expand** all branches as a full table on the PFD. |
| 94 | |
| 95 | - **Few meaningful outcomes (guide 2–4):** fan of edges from one fork point to meaningful steps; **`case` / guard** labels — optional, on hover or a second layer. |
| 96 | - **Many branches or empty-only cases:** **compression** — one multi-way branch node/icon; outward only calls that matter; the rest — aggregate (“other branches” / `default`) or hidden until drill-down. |
| 97 | - **Pattern matching** (`switch` expression, `when`): do not duplicate long expressions on permanent labels; same principle as for `if` predicates. |
| 98 | |
| 99 | Contract link: edge type like **`MultiBranch`** / **`ConditionalCall`** from the common fork ancestor to a step; branch detail in optional metadata. |
| 100 | |
| 101 | --- |
| 102 | |
| 103 | ## Data and contract (direction) |
| 104 | |
| 105 | Existing subgraph models (`CodeNavigationMapSubgraphNode`, `CodeNavigationMapSubgraphEdge`) are extended meaningfully, for example: |
| 106 | |
| 107 | | Idea | Purpose | |
| 108 | |------|---------| |
| 109 | | Edge type | `Call`, `ConditionalCall`, `Merge`, `MultiBranch` (several outcomes: `switch`, `if` chain, pattern matching), `Loop` / `LoopCall` (loop on edge to a step inside a loop; not to be confused with low-level CFG `LoopBack` if needed separately) | |
| 110 | | Short label | Optional; do not duplicate full `cond` text. | |
| 111 | | Condition detail | Optional, for tooltip / second layer; may be compressed or deferred. | |
| 112 | |
| 113 | Exact field names and JSON — **to be fixed** after a generator prototype (Roslyn / Control Flow Analysis + call filtering). |
| 114 | |
| 115 | Analysis source in the stack: **Roslyn** (`ControlFlowAnalysis` and binding calls to branches), without mandatory reliance on text-only grep. |
| 116 | |
| 117 | --- |
| 118 | |
| 119 | ## Agreements (draft) |
| 120 | |
| 121 | ### Predicate on edge vs icon only |
| 122 | |
| 123 | The boundary between “show a **short predicate**” and “**icon only**” is set by **user settings** (app / workspace — specific key and merge with bundle to be fixed at adoption), **not only** by a hard tie to a UI mode like Flight. |
| 124 | |
| 125 | The **agent** path (MCP, context / navigation subgraph request) uses the **same** detail level: in product terms the agent is also a **user** of this view and does not live in a separate hidden mode bypassing settings, except with an **explicit** call-parameter override (one-request override). |
| 126 | |
| 127 | ### Map type: `controlFlow` vs “classic” |
| 128 | |
| 129 | A separate **UiMode** preset (e.g. Flight only) **is not required** for a single CF-aware map: **which semantic map view to build** is the **user’s** choice, orthogonal to cockpit mode. **Flight** remains the PFD layout polygon; **map level/type** is set separately. |
| 130 | |
| 131 | Draft TOML shape (values and merge with bundle / workspace — at adoption): |
| 132 | |
| 133 | ```toml |
| 134 | [semantic_map] |
| 135 | level = "controlFlow" # control flow inside a method (CF-aware subgraph) |
| 136 | # level = "file" # classic dependency / file slice (baseline intent map) |
| 137 | ``` |
| 138 | |
| 139 | The same switch should be **reachable by the agent** (field in MCP / echo in subgraph request), without separate hidden semantics. |
| 140 | |
| 141 | ### Subgraph JSON versioning (MCP / CLI) |
| 142 | |
| 143 | **Backward compatibility when adding fields is not yet guaranteed:** the product **has no users yet**; subgraph JSON and MCP/CLI may **change shape in sync with code** without a migration policy. When stable consumers appear outside the repo (integrations, long-lived agent contract), introduce explicit **schema versioning** and compatibility rules **in a separate decision** (ADR / MCP-PROTOCOL add-on). |
| 144 | |
| 145 | --- |
| 146 | |
| 147 | ## Next steps (draft) |
| 148 | |
| 149 | 1. Fix the minimum set of edge **kinds** and call-filtering rules. |
| 150 | 2. Prototype: one method under the cursor → simplified flow → the same JSON the intent map on the PFD already consumes. |
| 151 | 3. Render: line and node styles on PFD / Skia without mandatory condition text on permanent labels. |
| 152 | |
| 153 | ## Backlog intent (add incrementally) |
| 154 | |
| 155 | To keep the map intent-focused and avoid turning into a full CFG, introduce extensions in portions: |
| 156 | |
| 157 | 1. **Early/abrupt exits**: `ThrowExit`, `Break`, `Continue`. |
| 158 | 2. **Async boundaries**: `AwaitBoundary` (pause/resume point of the flow). |
| 159 | 3. **Short circuit**: `ShortCircuit` for `&&` / `||` in guard conditions. |
| 160 | 4. **Pattern guard**: explicit semantics for `when` in pattern matching / switch. |
| 161 | 5. **Exceptions**: coarse `ExceptionFlow` for `catch`/`finally` without expanding full exception-CFG. |
| 162 | 6. **Iterators**: `YieldExit` (`yield return` / `yield break`) as a separate exit type. |
| 163 | 7. **Detail policy**: separate “operator noise” and “external steps” via declutter policy (e.g. helper calls inside arguments). |
| 164 | |
| 165 | *(Sections below may be added: JSON examples, screenshots, performance limits, links to external discussions.)* |
| 166 | |