selfware 0.6.7

Your personal AI workshop — software you own, software that lasts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
{
  "component": "lsp",
  "tier": "tooling",
  "loop_stage": "perceive",
  "summary": "The LSP subsystem gives the loop semantic code intelligence by speaking Language Server Protocol (JSON-RPC 2.0 / Content-Length over stdio) to external language servers. As a client (LspClient) it lazily launches rust-analyzer, pyright, typescript-language-server, or gopls based on file extension, opens documents, and answers goto_definition, find_references, document_symbols, hover, workspace_symbol, goto_implementation, and diagnostics. As a server (SelfwareLspServer / run_lsp_server) selfware itself answers documentSymbol, workspace/symbol, definition, and hover from its ProjectIntelligence symbol index so editors like Zed can query it. On the loop it is primarily a 'perceive' node — it turns positions and queries into grounded Symbols, Locations, Hovers, and Diagnostics — while diagnostics feed the 'verify' stage.",
  "loop_objects": ["LspClient", "SelfwareLspServer", "Location", "SymbolInfo", "Symbol", "Diagnostic", "Hover", "Language", "JsonRpcRequest", "ProjectIntelligence", "Evidence"],
  "context_basis": "Recommendations were formed by reading src/lsp/ (client.rs, server.rs, mod.rs) in the context of the full engine's ~600k-token budget framing, so each move accounts for how a precise LSP fact replaces reading whole files and thereby conserves the loop's token budget.",
  "examples": [
    {
      "id": "lsp-01",
      "title": "Lazily launch the right server by file extension",
      "loop_stage": "perceive",
      "pattern": "lazy-perceive",
      "intent": "Bring semantic intelligence online for a file only when the loop first touches it.",
      "how_it_shapes_the_loop": "LspClient defers spawning until a query arrives, then Language::from_path picks the language and server_candidates the binary; the first working candidate becomes the connection, so the loop pays server-start cost only for languages it actually inspects.",
      "loop_objects_touched": ["LspClient", "Language"],
      "wiring": {
        "inputs_from": ["file path from planner", "PATH via binary_exists"],
        "outputs_to": ["LspServerConnection", "downstream queries"]
      },
      "touch_interaction": {
        "gesture": "drag",
        "canvas_action": "Drag a source file onto the LSP node to bind it; the node auto-selects and launches the matching language server.",
        "visual": "A language glyph (Rust/Py/TS/Go) fades in and the node ring spins during spawn, then settles solid."
      },
      "mini_scenario": "The loop queries src/main.rs; from_path returns Rust, rust-analyzer is found on PATH, and the connection comes up on demand.",
      "pitfall": "If no candidate binary exists, the client errors — don't wire an LSP query as a hard dependency without a fallback to plain file reads."
    },
    {
      "id": "lsp-02",
      "title": "Open a document before querying it",
      "loop_stage": "perceive",
      "pattern": "sync-then-query",
      "intent": "Give the server the file content so its answers reflect the loop's working state.",
      "how_it_shapes_the_loop": "did_open sends textDocument/didOpen with the current content; the server indexes it so subsequent goto/hover/diagnostics operate on what the loop sees, not stale disk state.",
      "loop_objects_touched": ["LspClient"],
      "wiring": {
        "inputs_from": ["file content buffer"],
        "outputs_to": ["language server document store"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap the file chip on the LSP node to push its buffer to the server (didOpen).",
        "visual": "The chip flashes then wears a small open-book badge indicating the document is synced."
      },
      "mini_scenario": "Before asking for symbols, did_open streams the edited buffer so document_symbols reflects the uncommitted change.",
      "pitfall": "Query without did_open and the server may answer from stale or absent content — always sync the buffer first."
    },
    {
      "id": "lsp-03",
      "title": "Jump to a definition to ground a plan",
      "loop_stage": "perceive",
      "pattern": "perceive-then-plan",
      "intent": "Resolve where a symbol is defined so the loop edits the right place.",
      "how_it_shapes_the_loop": "goto_definition returns Vec<Location> {file,line,column}; the planner uses the precise site instead of grepping, so the next act step targets exact coordinates.",
      "loop_objects_touched": ["LspClient", "Location"],
      "wiring": {
        "inputs_from": ["cursor position (file,line,col)"],
        "outputs_to": ["planner target Location"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap a symbol token on the code preview to jump the canvas to its definition node.",
        "visual": "A comet edge arcs from the call site to the definition node, which pulses on arrival."
      },
      "mini_scenario": "The plan needs to change fn parse(); goto_definition returns parser.rs:88:4 and the edit step targets it directly.",
      "pitfall": "Multiple Locations can come back for overloaded or ambiguous symbols — don't assume the first is authoritative without checking the file."
    },
    {
      "id": "lsp-04",
      "title": "Find references before a risky rename",
      "loop_stage": "perceive",
      "pattern": "blast-radius-scan",
      "intent": "Enumerate every use of a symbol so a rename or signature change is complete.",
      "how_it_shapes_the_loop": "find_references returns all Locations touching the symbol; the loop expands its edit plan to cover each site, turning a single edit into a bounded multi-site act.",
      "loop_objects_touched": ["LspClient", "Location"],
      "wiring": {
        "inputs_from": ["symbol position"],
        "outputs_to": ["multi-site edit plan"]
      },
      "touch_interaction": {
        "gesture": "spread",
        "canvas_action": "Spread over a symbol node to fan out every reference as a constellation of call-site nodes.",
        "visual": "Reference nodes bloom outward, each labeled file:line, connected by faint edges to the origin."
      },
      "mini_scenario": "Renaming Config::load, find_references surfaces 12 call sites; the plan edits all 12 in one pass.",
      "pitfall": "References are a snapshot — if the loop edits files between the scan and the act, re-query, or a stale Location misses a moved use."
    },
    {
      "id": "lsp-05",
      "title": "Outline a file with document symbols",
      "loop_stage": "perceive",
      "pattern": "structure-first",
      "intent": "Get a file's shape without reading its full text into the budget.",
      "how_it_shapes_the_loop": "document_symbols returns Vec<SymbolInfo> {name,kind,line,column}; the loop perceives the file's functions/structs/methods cheaply and plans against the outline rather than the raw source.",
      "loop_objects_touched": ["LspClient", "SymbolInfo"],
      "wiring": {
        "inputs_from": ["opened document"],
        "outputs_to": ["planner outline / navigation map"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinch a file node to collapse its full text into a compact symbol outline strip.",
        "visual": "Lines of code fold into labeled kind-colored rows (fn/struct/method), preserving order and indentation."
      },
      "mini_scenario": "Instead of reading 900 lines, the loop pulls document_symbols and sees the 14 functions it needs to reason about.",
      "pitfall": "SymbolInfo.kind is a human-readable string, not an enum — match on the exact strings the server emits, not assumed variants."
    },
    {
      "id": "lsp-06",
      "title": "Hover for a signature instead of reading source",
      "loop_stage": "perceive",
      "pattern": "budget-saving-probe",
      "intent": "Learn a symbol's type/signature with a single cheap probe.",
      "how_it_shapes_the_loop": "hover returns Option<String> markdown; the loop resolves a signature or type in one small evidence packet rather than opening the defining file, conserving token budget.",
      "loop_objects_touched": ["LspClient", "Hover"],
      "wiring": {
        "inputs_from": ["symbol position"],
        "outputs_to": ["Evidence (signature)"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press a symbol token to float its hover card with type and doc inline.",
        "visual": "A markdown tooltip lifts above the token with a soft glow; empty hovers show a muted 'no info' state."
      },
      "mini_scenario": "The loop hovers `db.query` and gets the full generic signature, so it calls it correctly without reading the crate.",
      "pitfall": "hover returns None legitimately when the server has nothing — treat None as 'unknown', not as an error to recover from."
    },
    {
      "id": "lsp-07",
      "title": "Collect diagnostics as a verify gate",
      "loop_stage": "verify",
      "pattern": "gate-before-commit",
      "intent": "Check whether an edit introduced errors before the loop advances.",
      "how_it_shapes_the_loop": "diagnostics returns the cached Vec<Diagnostic> from the last textDocument/publishDiagnostics; an error-severity entry fails the verify gate and can route the loop to ErrorRecovery.",
      "loop_objects_touched": ["LspClient", "Diagnostic"],
      "wiring": {
        "inputs_from": ["publishDiagnostics notifications"],
        "outputs_to": ["verify gate / ErrorRecovery"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap the diagnostics badge on a file node to list current errors and warnings.",
        "visual": "The node ring turns red on any error-severity diagnostic, amber on warnings, green when clean."
      },
      "mini_scenario": "After an edit, diagnostics shows an error at line 40; the verify gate blocks the commit and the loop fixes it.",
      "pitfall": "Diagnostics are cached from the last publish — after an edit, give the server a moment to re-publish or you'll gate on stale results."
    },
    {
      "id": "lsp-08",
      "title": "Search the workspace for a symbol",
      "loop_stage": "perceive",
      "pattern": "global-locate",
      "intent": "Find a symbol by name across the whole project without knowing its file.",
      "how_it_shapes_the_loop": "workspace_symbol(query) returns matching Vec<SymbolInfo> project-wide; the loop locates an entity from a name alone, seeding a plan without a prior file target.",
      "loop_objects_touched": ["LspClient", "SymbolInfo"],
      "wiring": {
        "inputs_from": ["symbol name query"],
        "outputs_to": ["candidate Locations for the planner"]
      },
      "touch_interaction": {
        "gesture": "draw-connection",
        "canvas_action": "Draw from a search bubble to the LSP node to run a workspace symbol query.",
        "visual": "Matching symbol nodes light up across the canvas map, ranked by relevance with count badge."
      },
      "mini_scenario": "The loop searches 'Budget' and workspace_symbol returns the struct plus its methods across three files.",
      "pitfall": "Broad queries can return many matches — narrow the query or the planner wastes budget triaging noise."
    },
    {
      "id": "lsp-09",
      "title": "Go to implementation from a trait",
      "loop_stage": "perceive",
      "pattern": "abstraction-to-concrete",
      "intent": "Resolve which concrete type implements an interface at a call site.",
      "how_it_shapes_the_loop": "goto_implementation returns implementation Locations; the loop bridges from a trait/interface to real code, so it can reason about actual behavior rather than the abstract signature.",
      "loop_objects_touched": ["LspClient", "Location"],
      "wiring": {
        "inputs_from": ["trait/interface position"],
        "outputs_to": ["concrete impl Locations"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap a trait node to fan out to its implementor nodes.",
        "visual": "Dashed abstraction edges resolve into solid edges pointing at each concrete impl node."
      },
      "mini_scenario": "The plan hits Tool::execute; goto_implementation lists McpTool and the native tools that implement it.",
      "pitfall": "Not all servers implement goto_implementation richly — handle an empty result as 'server can't answer', not 'no implementors'."
    },
    {
      "id": "lsp-10",
      "title": "Map diagnostic severity to loop response",
      "loop_stage": "verify",
      "pattern": "severity-routing",
      "intent": "Let error vs warning vs hint drive different loop branches.",
      "how_it_shapes_the_loop": "Diagnostic.severity is 'error'|'warning'|'info'|'hint' (mapped from numeric 1–4); the verify step blocks on error, notes warnings, and lets hints inform learn without halting.",
      "loop_objects_touched": ["Diagnostic"],
      "wiring": {
        "inputs_from": ["publishDiagnostics severity codes"],
        "outputs_to": ["verify branch / learn notes"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press the diagnostics badge to break the count down by severity tier.",
        "visual": "A stacked bar shows red/amber/blue/grey segments for error/warning/info/hint counts."
      },
      "mini_scenario": "An edit yields one warning and two hints but no error; the loop commits and records the hints for later cleanup.",
      "pitfall": "Don't halt on warnings by default — over-strict gating on non-error severities stalls otherwise-valid progress."
    },
    {
      "id": "lsp-11",
      "title": "Close documents to bound server state",
      "loop_stage": "control",
      "pattern": "resource-hygiene",
      "intent": "Release documents the loop is done with so server memory stays bounded.",
      "how_it_shapes_the_loop": "did_close sends textDocument/didClose; over a long loop this keeps the language server's open-document set from growing without limit as files are visited and abandoned.",
      "loop_objects_touched": ["LspClient"],
      "wiring": {
        "inputs_from": ["completed file work"],
        "outputs_to": ["language server document store (release)"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick a synced file chip off the LSP node to close its document.",
        "visual": "The open-book badge closes and the chip dims to indicate the server released it."
      },
      "mini_scenario": "After editing and verifying config.rs, the loop closes it so the server isn't holding dozens of stale buffers.",
      "pitfall": "Close only what you're finished with — closing a file mid-plan drops its diagnostics and forces a re-open."
    },
    {
      "id": "lsp-12",
      "title": "Shut down all servers on loop teardown",
      "loop_stage": "control",
      "pattern": "graceful-teardown",
      "intent": "Reclaim every language-server process when the loop ends.",
      "how_it_shapes_the_loop": "shutdown sends the shutdown request then exit notification and kills each process; the loop returns the process budget it borrowed across all launched languages.",
      "loop_objects_touched": ["LspClient", "JsonRpcRequest"],
      "wiring": {
        "inputs_from": ["loop completion signal"],
        "outputs_to": ["language server processes (reaped)"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick the LSP node to the canvas edge to tear down all its connections at once.",
        "visual": "Each language sub-node shrinks and fades in sequence; a final exit glyph confirms full teardown."
      },
      "mini_scenario": "The task finishes having used rust-analyzer and gopls; shutdown reaps both processes cleanly.",
      "pitfall": "Skipping shutdown leaks a long-lived language server per language — always tear down even on the error path."
    },
    {
      "id": "lsp-13",
      "title": "Serve selfware's own symbol index over LSP",
      "loop_stage": "act",
      "pattern": "invert-the-boundary",
      "intent": "Let an editor query selfware's ProjectIntelligence as if it were a language server.",
      "how_it_shapes_the_loop": "run_lsp_server starts SelfwareLspServer answering documentSymbol, workspace/symbol, definition, and hover from ProjectIntelligence; the loop's perception layer becomes an external editor's, inverting client and host.",
      "loop_objects_touched": ["SelfwareLspServer", "ProjectIntelligence", "Symbol"],
      "wiring": {
        "inputs_from": ["editor JSON-RPC requests"],
        "outputs_to": ["ProjectIntelligence queries", "LSP responses"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick the symbol-index node outward past the canvas edge to publish it as an LSP surface.",
        "visual": "The node gains a broadcast halo and an inbound-arrow glyph as editor requests begin flowing in."
      },
      "mini_scenario": "Zed connects to selfware's LSP server and workspace/symbol returns the CodeGraph symbols from ProjectIntelligence.",
      "pitfall": "ProjectIntelligence is refreshed per request (no cache) — that keeps results fresh but adds latency, so don't fan out one request per keystroke."
    },
    {
      "id": "lsp-14",
      "title": "Resolve a token at a cursor position",
      "loop_stage": "perceive",
      "pattern": "position-to-symbol",
      "intent": "Turn a raw (line,column) into the identifier the user means.",
      "how_it_shapes_the_loop": "SelfwareLspServer walks identifier characters (alphanumeric + underscore) around the position, with a fallback to the previous char, so definition/hover resolve even when the cursor sits at a token boundary.",
      "loop_objects_touched": ["SelfwareLspServer", "Symbol"],
      "wiring": {
        "inputs_from": ["textDocument/definition or hover position"],
        "outputs_to": ["ProjectIntelligence symbol lookup"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap precisely on a token in the code preview to resolve the identifier under the point.",
        "visual": "The resolved identifier highlights with a selection box; a boundary-fallback shows a small left-arrow hint."
      },
      "mini_scenario": "The cursor lands just past `parse`; the fallback checks the previous char, resolves `parse`, and hover returns its signature.",
      "pitfall": "Positions are 1-indexed line/column in this server — off-by-one indexing silently resolves the wrong token."
    },
    {
      "id": "lsp-15",
      "title": "Format hover as signature plus location",
      "loop_stage": "perceive",
      "pattern": "compact-evidence",
      "intent": "Return just enough context — the signature and where it lives — as one markdown packet.",
      "how_it_shapes_the_loop": "SelfwareLspServer builds hover markdown as `signature\\nfile:line` from the matched Symbol; the loop gets a self-contained Evidence card it can quote without a follow-up read.",
      "loop_objects_touched": ["SelfwareLspServer", "Symbol", "Hover"],
      "wiring": {
        "inputs_from": ["ProjectIntelligence Symbol"],
        "outputs_to": ["MarkupContent hover response"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press a symbol node to surface its formatted hover card.",
        "visual": "A markdown card shows the signature on top and a dimmed file:line footer beneath."
      },
      "mini_scenario": "Hovering a struct returns `pub struct Budget { ... }` plus `resource/budget.rs:22`, enough to reason without opening the file.",
      "pitfall": "Keep the hover compact — dumping the whole symbol body defeats the budget savings the hover exists to provide."
    },
    {
      "id": "lsp-16",
      "title": "Auto-detect Content-Length framing on the wire",
      "loop_stage": "foundation",
      "pattern": "wire-adapt",
      "intent": "Read LSP-framed messages regardless of how the peer chunks its writes.",
      "how_it_shapes_the_loop": "Both LSP client and server frame with Content-Length headers via the shared mcp::transport read/write helpers; the loop's perceive channel stays intact even when a header and body arrive in separate reads.",
      "loop_objects_touched": ["LspClient", "SelfwareLspServer", "JsonRpcRequest"],
      "wiring": {
        "inputs_from": ["stdio byte stream"],
        "outputs_to": ["parsed JSON-RPC messages"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinch the transport node to inspect the header/body framing of the last message.",
        "visual": "A framed envelope glyph shows the Content-Length header split from its JSON body."
      },
      "mini_scenario": "rust-analyzer sends a header line then the body in a second write; read_message assembles both into one JSON-RPC message.",
      "pitfall": "LSP uses Content-Length framing, not newline-delimited — mixing MCP's default newline framing here truncates messages."
    },
    {
      "id": "lsp-17",
      "title": "Sanitize the server environment for toolchain vars only",
      "loop_stage": "foundation",
      "pattern": "credential-firewall",
      "intent": "Launch a language server with just the toolchain discovery vars it needs and no secrets.",
      "how_it_shapes_the_loop": "The client clears secrets (SELFWARE_API_KEY, AWS_*, GITHUB_TOKEN) and whitelists only CARGO_HOME, RUSTUP_HOME, NODE_PATH, PYTHONPATH, VIRTUAL_ENV, GOPATH, GOROOT, JAVA_HOME, so the loop can perceive safely via third-party servers.",
      "loop_objects_touched": ["LspClient", "Language"],
      "wiring": {
        "inputs_from": ["process environment", "toolchain whitelist"],
        "outputs_to": ["language server subprocess env"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press the language sub-node to inspect the sanitized env passed to its server.",
        "visual": "Secret vars render struck-through in red; whitelisted toolchain vars glow green as passed through."
      },
      "mini_scenario": "gopls launches seeing GOPATH and GOROOT but not the operator's GITHUB_TOKEN, so it can resolve modules without touching credentials.",
      "pitfall": "A server that needs an unlisted var to find its toolchain will fail quietly — extend the whitelist deliberately, never disable the firewall."
    },
    {
      "id": "lsp-18",
      "title": "Time out a stalled server query",
      "loop_stage": "control",
      "pattern": "bounded-wait",
      "intent": "Keep a hung language server from freezing the loop's perceive step.",
      "how_it_shapes_the_loop": "Each request awaits its oneshot with a 30s timeout; on expiry the client returns an explicit error so the loop can fall back to a file read rather than blocking indefinitely.",
      "loop_objects_touched": ["LspClient", "JsonRpcRequest"],
      "wiring": {
        "inputs_from": ["pending request oneshot"],
        "outputs_to": ["timeout error / fallback path"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap an in-flight query edge to see its remaining timeout countdown.",
        "visual": "A thinning ring counts down around the edge; at zero it snaps red with a timeout badge."
      },
      "mini_scenario": "rust-analyzer is still indexing and a goto_definition hangs; at 30s the client errors and the loop reads the file directly.",
      "pitfall": "A cold server can exceed 30s during initial indexing — don't treat the first timeout as a fatal server failure, retry once."
    },
    {
      "id": "lsp-19",
      "title": "Suppress responses for notifications",
      "loop_stage": "foundation",
      "pattern": "id-gated-reply",
      "intent": "Reply only to requests, never to fire-and-forget notifications.",
      "how_it_shapes_the_loop": "SelfwareLspServer sends a JsonRpcResponse only when the incoming request carries an id; didOpen/didChange/didClose and initialized are accepted silently, keeping the loop's message flow correct.",
      "loop_objects_touched": ["SelfwareLspServer", "JsonRpcRequest"],
      "wiring": {
        "inputs_from": ["client notifications and requests"],
        "outputs_to": ["responses only for id-bearing requests"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick a notification pulse at the server node; it absorbs without emitting a reply edge.",
        "visual": "Notification pulses fade into the node with no return animation; id-bearing requests spawn a reply edge."
      },
      "mini_scenario": "The editor streams didChange notifications as the user types; the server updates state and answers none of them.",
      "pitfall": "Emitting a response to a notification confuses the client's correlation table — check for id before ever writing a reply."
    },
    {
      "id": "lsp-20",
      "title": "Convert file URIs safely across the boundary",
      "loop_stage": "foundation",
      "pattern": "path-uri-bridge",
      "intent": "Translate between file:// URIs and filesystem paths without corrupting either.",
      "how_it_shapes_the_loop": "uri_to_path and path_to_uri use url::Url so definitions and symbols cross the LSP boundary as valid file:// URIs; the loop's Locations resolve to real paths the act step can open.",
      "loop_objects_touched": ["SelfwareLspServer", "Location", "Symbol"],
      "wiring": {
        "inputs_from": ["file:// URIs from editor", "Symbol.file paths"],
        "outputs_to": ["filesystem paths / Location URIs"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap a Location node to expand its file:// URI and resolved path side by side.",
        "visual": "The URI and path render as paired chips; a mismatch highlights amber if conversion fails."
      },
      "mini_scenario": "A definition response carries file:///Users/ivo/selfware/src/main.rs; uri_to_path resolves it and the edit step opens the file.",
      "pitfall": "Hand-splicing file:// strings breaks on spaces and special characters — always round-trip through url::Url, never string concatenation."
    }
  ]
}