ADR 0053: Intent map and control flow on PFD (control flow)¶
Status: Accepted · Implemented
Date: 2026-04-17
Related ADRs¶
| ADR / artifact | Role |
|---|---|
| 0021 | PFD as instruments, attention zones |
| 0051 | Attention routing |
| 0055 | Skia pipeline |
| 0056 | Pipeline adoption on the map |
| 0039 | MCP, subgraph |
| 0065 | Code intent map |
CodeNavigationMapSubgraph* |
Subgraph models in code |
Summary¶
- Intent map on PFD: control flow, subgraph, KISS navigation around a code anchor.
- Shared Skia pipeline and cursor semantics (0056).
- Not to be confused with GitMap (0062) or the full solution tree.
Context¶
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.
Example intent:
void A() {
if (cond) B();
else C();
D();
}
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.
Aviation analogy: waypoints and transition conditions on a navigation display, not a printout of every procedural page.
Goals¶
- Visualize control-flow intent inside the selected method where it helps scanning (Flight / “thin” mode), not syntax for its own sake.
- Preserve KISS: do not overload the PFD with condition text and decorations by default.
- Prepare a data contract (subgraph / JSON / MCP) so edges and, when needed, nodes carry link type and optional hints for hover / drill-down.
Non-goals (first phase)¶
- Full rendering of the entire Roslyn CFG on the mini-map.
- Duplicating the body of
if (…)as a permanent label on the PFD. - Visualizing every local assignment and small noise inside branches.
Display principles (keep the UI clean)¶
-
Semantic compression
By default: a branch icon (diamond / “?”) or a thin fork on the edge, without long predicate text — forifand forswitch/ pattern matching alike (see § Switch…).
Condition detail — on gaze dwell / hover / a separate layer (Skia tooltip), optional. -
Only meaningful “external” route points
Priority: calls to other methods and heavy operations the user scans for. Local noise (i++, emptyreturnwith no map meaning) — do not lift onto the graph without an explicit policy. -
Thin lines and metaphors
- Loop (
for/while): see § Loops: loop on the edge. -
try/catch: later — a “protected” segment style (e.g. “umbrella” / unstable node outline) once vocabulary is agreed. -
Vertical “flight plan”
Meaningful step nodes top to bottom, branches to the side, without IDE-style blocks filling the screen.
Loops (for / while): loop on the edge¶
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”.
User meaning: immediately obvious — “this call spins here”, without reading code in Forward and without unrolling iterations on the map.
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.
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.
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.
Switch, case, and pattern matching¶
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.
- 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. - 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. - Pattern matching (
switchexpression,when): do not duplicate long expressions on permanent labels; same principle as forifpredicates.
Contract link: edge type like MultiBranch / ConditionalCall from the common fork ancestor to a step; branch detail in optional metadata.
Data and contract (direction)¶
Existing subgraph models (CodeNavigationMapSubgraphNode, CodeNavigationMapSubgraphEdge) are extended meaningfully, for example:
| Idea | Purpose |
|---|---|
| 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) |
| Short label | Optional; do not duplicate full cond text. |
| Condition detail | Optional, for tooltip / second layer; may be compressed or deferred. |
Exact field names and JSON — to be fixed after a generator prototype (Roslyn / Control Flow Analysis + call filtering).
Analysis source in the stack: Roslyn (ControlFlowAnalysis and binding calls to branches), without mandatory reliance on text-only grep.
Agreements (draft)¶
Predicate on edge vs icon only¶
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.
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).
Map type: controlFlow vs “classic”¶
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.
Draft TOML shape (values and merge with bundle / workspace — at adoption):
[semantic_map]
level = "controlFlow" # control flow inside a method (CF-aware subgraph)
# level = "file" # classic dependency / file slice (baseline intent map)
The same switch should be reachable by the agent (field in MCP / echo in subgraph request), without separate hidden semantics.
Subgraph JSON versioning (MCP / CLI)¶
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).
Next steps (draft)¶
- Fix the minimum set of edge kinds and call-filtering rules.
- Prototype: one method under the cursor → simplified flow → the same JSON the intent map on the PFD already consumes.
- Render: line and node styles on PFD / Skia without mandatory condition text on permanent labels.
Backlog intent (add incrementally)¶
To keep the map intent-focused and avoid turning into a full CFG, introduce extensions in portions:
- Early/abrupt exits:
ThrowExit,Break,Continue. - Async boundaries:
AwaitBoundary(pause/resume point of the flow). - Short circuit:
ShortCircuitfor&&/||in guard conditions. - Pattern guard: explicit semantics for
whenin pattern matching / switch. - Exceptions: coarse
ExceptionFlowforcatch/finallywithout expanding full exception-CFG. - Iterators:
YieldExit(yield return/yield break) as a separate exit type. - Detail policy: separate “operator noise” and “external steps” via declutter policy (e.g. helper calls inside arguments).
(Sections below may be added: JSON examples, screenshots, performance limits, links to external discussions.)