<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>LSP | Francis Du</title>
    <link>https://francisdu.com/tags/lsp/</link>
      <atom:link href="https://francisdu.com/tags/lsp/index.xml" rel="self" type="application/rss+xml" />
    <description>💻Data Engineer | 🦀 Rustacean | 📷 Photographer | 🤖Vibe Coder</description>
    <generator>Hugo 0.166.0</generator><language>zh-CN</language><copyright>© Francis Du</copyright><lastBuildDate>Sun, 30 Aug 2026 17:30:00 +0800</lastBuildDate>
    <item>
      <title>wcode v0.5：LSP 不再每次现启</title>
      <link>https://francisdu.com/blog/wcode-v0-5/</link>
      <pubDate>Sun, 30 Aug 2026 17:30:00 +0800</pubDate>
      <guid>https://francisdu.com/blog/wcode-v0-5/</guid>
      <description>&lt;p&gt;v0.4 做完以后，我以为 wcode 写代码这条主链已经比较顺了。&lt;/p&gt;
&lt;p&gt;那一版主要解决 Context 成本：&lt;code&gt;agent_context&lt;/code&gt; 变成 Coding Entry，Repo Map 有了 Cache 和 Scope，简单任务不需要每次把整个项目重新理解一遍。&lt;/p&gt;
&lt;p&gt;但真的继续拿它写代码，很快又遇到另一个问题。&lt;/p&gt;
&lt;p&gt;不是“找不到函数”。&lt;/p&gt;
&lt;p&gt;Tree-sitter 和 Search 对定位其实已经很好用了。&lt;/p&gt;
&lt;p&gt;真正麻烦的是这种问题：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;谁在调用这个函数？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这个 Trait 到底有哪些实现？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;改这个 Symbol 会影响哪些跨文件 Reference？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这个调用关系是同名文本，还是类型系统真正解析出来的？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种时候，grep 能找到很多东西，但不一定完整；Tree-sitter 能告诉我语法结构，但它也不应该假装自己知道类型系统。&lt;/p&gt;
&lt;p&gt;所以 v0.5 没继续堆零散的 LSP API，我直接把 Language Server 的生命周期重新做了一遍，让它可以受限地常驻和复用。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-architecture_hu_92a8fd07f12ae64a.webp&#34; alt=&#34;wcode Architecture&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-architecture_hu_92a8fd07f12ae64a.webp 960w, https://francisdu.com/img/wcode/wcode-architecture_hu_3f3f08e508cea449.webp 1800w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;3232&#34; height=&#34;1932&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;我为什么没有把所有查询都切到-lsp&#34;&gt;我为什么没有把所有查询都切到 LSP&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%88%91%e4%b8%ba%e4%bb%80%e4%b9%88%e6%b2%a1%e6%9c%89%e6%8a%8a%e6%89%80%e6%9c%89%e6%9f%a5%e8%af%a2%e9%83%bd%e5%88%87%e5%88%b0-lsp&#34; aria-label=&#34;章节链接：我为什么没有把所有查询都切到 LSP&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最直接的做法其实很诱人：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;既然 Language Server 更懂代码，那以后 Definition、Search、Reference、Call 全走 LSP 不就好了？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我试着沿这个方向想了一轮，最后放弃了。&lt;/p&gt;
&lt;p&gt;原因是 Agent Coding 里有两类完全不同的问题。&lt;/p&gt;
&lt;p&gt;第一类只是定位：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这个函数在哪？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这个 Struct 定义在哪？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;哪个文件包含这个字符串？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这类问题 Tree-sitter / Search 很便宜，而且稳定，不需要启动项目语义环境。&lt;/p&gt;
&lt;p&gt;第二类才是真正需要 Semantic Completeness 的关系问题：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;references
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;implementations
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;incoming callers
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;outgoing callees
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rename impact
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果为了第一类问题也默认走 Language Server，就会把一个本来很便宜的定位动作变成进程启动、Initialize、Document Sync、Semantic Query。&lt;/p&gt;
&lt;p&gt;而 Agent 还有一个额外成本：Tool Result 最后会进入 Context。&lt;/p&gt;
&lt;p&gt;所以我现在更愿意把两层能力分开：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ┌──────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                │     agent_context     │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                └──────────┬───────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                           │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                普通定位   │   跨文件关系
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                           │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          ┌────────────────┴───────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          ↓                                ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;find_symbol / search_code          semantic_navigation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Tree-sitter / text search                 │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          │                               ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          │                       Warm LSP Session
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          └──────────────┬────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                         ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    edit / review
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                         ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    verify_project
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不是“LSP 比 grep 高级，所以替掉 grep”。&lt;/p&gt;
&lt;p&gt;而是让每一层只解决它真正擅长的问题。&lt;/p&gt;
&lt;h2 id=&#34;以前的-lsp-其实还是一次性工具&#34;&gt;以前的 LSP 其实还是一次性工具&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e4%bb%a5%e5%89%8d%e7%9a%84-lsp-%e5%85%b6%e5%ae%9e%e8%bf%98%e6%98%af%e4%b8%80%e6%ac%a1%e6%80%a7%e5%b7%a5%e5%85%b7&#34; aria-label=&#34;章节链接：以前的 LSP 其实还是一次性工具&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;wcode 之前已经能跑 Language Server。&lt;/p&gt;
&lt;p&gt;但生命周期很像一个 Batch Job：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;start provider
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;initialize
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;didOpen
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Document Symbol
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Call Hierarchy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Implementation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;shutdown
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它适合定期补 Software Graph。&lt;/p&gt;
&lt;p&gt;但如果 Agent 紧接着又问一次 Reference，就要重新付一遍启动成本。&lt;/p&gt;
&lt;p&gt;更关键的是，这种模型下“Semantic Provider”在架构上仍然只是一个索引器，不是真正的 Runtime。&lt;/p&gt;
&lt;p&gt;v0.5 把这个生命周期改了。&lt;/p&gt;
&lt;p&gt;现在 Harness 里有一个有界 Session Pool：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Workspace
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Provider + Binary Identity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Warm Session Slot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ├── graph refresh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ├── semantic_navigation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ├── didChange
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   └── didClose
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同一个 Workspace 下，后台 Graph Indexing 和前台 Navigation 会复用同一个 Provider Process。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;rust-analyzer&lt;/code&gt; 已经 Warm，第二次查 Caller 不需要再启动一遍。&lt;/p&gt;
&lt;h2 id=&#34;warm-不等于永远不关&#34;&gt;Warm 不等于永远不关&lt;a class=&#34;heading-anchor&#34; href=&#34;#warm-%e4%b8%8d%e7%ad%89%e4%ba%8e%e6%b0%b8%e8%bf%9c%e4%b8%8d%e5%85%b3&#34; aria-label=&#34;章节链接：Warm 不等于永远不关&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我不想为了省启动成本，最后在后台养一堆永远不会退出的 Language Server。&lt;/p&gt;
&lt;p&gt;所以这个 Pool 从一开始就是 Bounded 的。&lt;/p&gt;
&lt;p&gt;它有几条约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Session 数量有上限；&lt;/li&gt;
&lt;li&gt;Idle Slot 会淘汰；&lt;/li&gt;
&lt;li&gt;同一个 Slot 的 JSON-RPC Stream 串行处理；&lt;/li&gt;
&lt;li&gt;Provider Process 死掉以后重建；&lt;/li&gt;
&lt;li&gt;Provider Binary Identity 变了以后重建；&lt;/li&gt;
&lt;li&gt;Workspace Key 不共享到别的项目；&lt;/li&gt;
&lt;li&gt;离开当前有界索引集合的 Document 会 &lt;code&gt;didClose&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;发版前我专门沿这条生命周期又审了一遍，结果真的抓到一个边界 Bug：最初的 Idle Eviction 其实是 Lazy 的，只有下一次有人访问 Session Pool 时才会 Prune；更糟一点，容量满时如果直接从 Map 里移除“最旧 Slot”，这个 Slot 可能还被一个 Active Request 持有，于是 Map 看起来仍然只有 16 个，实际进程却可能短暂跑到第 17 个。&lt;/p&gt;
&lt;p&gt;最后我把规则改成了更保守的版本：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Idle + unleased
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 可以 prune
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;capacity full + 有 unleased slot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 只驱逐 unleased slot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;capacity full + 全部 leased
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → fail closed / retry
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;provider binary changed + old slot still leased
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 等当前 request 结束
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 再替换
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Background Semantic Coordinator 也会周期性主动 Prune，所以即使是没有 TUI 交互的 &lt;code&gt;mcp-stdio&lt;/code&gt;，Idle Bound 也不是一句文档里的承诺。&lt;/p&gt;
&lt;p&gt;源码变化也不是简单把 Process 杀掉重启。&lt;/p&gt;
&lt;p&gt;现在不是强行给所有 Server 发同一种 Notification，而是先看它在 &lt;code&gt;initialize&lt;/code&gt; 里声明的 &lt;code&gt;textDocumentSync&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Full
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Open 发完整内容
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Change 发整文档
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Incremental
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Open 发完整内容
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Change 用旧文档范围做合法 replacement
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Range 按协商的 UTF-8 / UTF-16 / UTF-32 算
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;None / openClose=false
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 不硬发 Server 没声明支持的 Notification
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Server 继续从磁盘读取
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有 Server 要求 Open/Close Sync 时才发 &lt;code&gt;didOpen&lt;/code&gt; / &lt;code&gt;didClose&lt;/code&gt;。这才比较像我理解的“常驻语义层”：它既知道当前 Document Revision，也尊重每个 Language Server 自己的同步协议，而不是假定 Rust 能工作的 Change Shape 对 22 种语言都成立。&lt;/p&gt;
&lt;h2 id=&#34;semantic_navigation-不让-agent-自己算-utf-16&#34;&gt;&lt;code&gt;semantic_navigation&lt;/code&gt; 不让 Agent 自己算 UTF-16&lt;a class=&#34;heading-anchor&#34; href=&#34;#semantic_navigation-%e4%b8%8d%e8%ae%a9-agent-%e8%87%aa%e5%b7%b1%e7%ae%97-utf-16&#34; aria-label=&#34;章节链接：semantic_navigation 不让 Agent 自己算 UTF-16&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;做 Agent Tool 时，我越来越不喜欢把底层协议细节原样扔给模型。&lt;/p&gt;
&lt;p&gt;原始 LSP 通常希望调用方给：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;file URI
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;line
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;character
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;position encoding
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后不同 Server 还可能用 UTF-8 / UTF-16 Position Encoding。&lt;/p&gt;
&lt;p&gt;让模型自己根据源码去算 UTF-16 Offset，我觉得完全是在浪费模型能力，而且很容易在非 ASCII 代码里漂掉。&lt;/p&gt;
&lt;p&gt;所以 v0.5 的 &lt;code&gt;semantic_navigation&lt;/code&gt; 主入口是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;path + symbol
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;wcode 自己先用 Tree-sitter 找 Symbol 的精确位置，再根据 Language Server Initialize 时协商到的 Encoding 转换 Position。&lt;/p&gt;
&lt;p&gt;Agent 只需要表达意图：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;definition
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hover
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;references
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;incoming_calls
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;outgoing_calls
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;implementations
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;impact
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;真正已经持有精确 Position 的调用方，也可以继续传 &lt;code&gt;line + character&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这件事看起来很小，但我觉得它代表一个方向：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LSP Primitive 不应该直接等于 Agent Primitive。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Agent Tool 应该封装成模型真正想问的问题，而不是要求模型先学会协议的坐标系。&lt;/p&gt;
&lt;p&gt;这里发版审计又抓到另一个我不愿意留到 0.5.1 的问题：最早实现里，如果 Server 声明支持 &lt;code&gt;references&lt;/code&gt;，但这次 Request 实际 Timeout / Error，结果路径可能最后只留下一个空数组。对 Agent 来说，“请求失败”和“确实没有 Reference”完全不是一件事。&lt;/p&gt;
&lt;p&gt;所以现在 Result 会明确分成：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;unsupported
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Server 没这个能力
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;failures
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Server 有能力，但这次 LSP Request 失败
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;relationships = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → Request 成功，真的没有匹配关系
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我尤其不想让失败被当成 Negative Semantic Evidence。语义系统最危险的不是“不知道”，而是失败以后还表现得像自己很确定。&lt;/p&gt;
&lt;h2 id=&#34;22-种语言不能只有-rust-真正跑得通&#34;&gt;22 种语言，不能只有 Rust 真正跑得通&lt;a class=&#34;heading-anchor&#34; href=&#34;#22-%e7%a7%8d%e8%af%ad%e8%a8%80%e4%b8%8d%e8%83%bd%e5%8f%aa%e6%9c%89-rust-%e7%9c%9f%e6%ad%a3%e8%b7%91%e5%be%97%e9%80%9a&#34; aria-label=&#34;章节链接：22 种语言，不能只有 Rust 真正跑得通&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;发版前我又给这版加了一条更苛刻的要求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;既然 wcode 对外说 Syntax Index 支持 22 种语言，那 LSP 层也不能只把 rust-analyzer 做扎实，其他语言只在 Registry 里挂个名字。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这次审计以后，我把“支持”拆成了三个完全不同的概念：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Compatibility
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → wcode 有没有正确的 Provider Adapter / Command / Language ID
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Installation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 用户机器上有没有真的装这个 Language Server
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Live Semantic
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  → 这个已安装 Server 有没有真实 initialize 并回答当前 Revision
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有第一层是 wcode 在 Build/Test 阶段能 100% 保证的。&lt;/p&gt;
&lt;p&gt;第二层取决于用户机器。&lt;/p&gt;
&lt;p&gt;第三层必须等 Runtime 真正和 Server 完成 LSP Handshake 以后才能成立。&lt;/p&gt;
&lt;p&gt;所以 v0.5 现在要求 22 种 Indexed Language &lt;strong&gt;每一种恰好只有一个 Canonical LSP Launch Profile&lt;/strong&gt;，并用测试把映射和 Provider-specific Argument 锁死。除此之外，我还加了一层跨平台 stdio Mock LSP：每一个 Canonical Profile 都会真的 Spawn 一个子进程，完成 &lt;code&gt;initialize&lt;/code&gt;、Capability Negotiation、Open/Change/Close 和 Hover JSON-RPC 往返，而不是只检查数组里的字符串：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Language&lt;/th&gt;
					&lt;th&gt;Canonical LSP&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Bash&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;bash-language-server start&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;C / C++&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;clangd&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;C#&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;csharp-ls&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;CSS&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;vscode-css-language-server --stdio&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Dart&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;dart language-server --protocol=lsp&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Elixir&lt;/td&gt;
					&lt;td&gt;ElixirLS &lt;code&gt;language_server.sh&lt;/code&gt; / Wrapper&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Go&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;gopls serve&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HTML&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;vscode-html-language-server --stdio&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Java&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;jdtls -data &amp;lt;unique state&amp;gt;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JavaScript / TypeScript / TSX&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;typescript-language-server --stdio&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Lua&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;lua-language-server&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;OCaml / Interface&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ocamllsp&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PHP&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;phpactor language-server&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Python&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;pyright-langserver --stdio&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;R&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;R --no-echo -e languageserver::run()&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Ruby&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ruby-lsp&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Rust&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;rust-analyzer&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Swift&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;sourcekit-lsp&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;而且这次不是把旧 Registry 原样拿来写测试。&lt;/p&gt;
&lt;p&gt;我对着各家的当前启动方式重新过了一遍，确实发现了几个容易变成“纸面支持”的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go 明确改成 &lt;code&gt;gopls serve&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;JDT LS 会拿一个 Workspace + Runtime 唯一的用户级 &lt;code&gt;-data&lt;/code&gt; 目录，避免两个项目或两个 wcode Process 共用 JDT State；&lt;/li&gt;
&lt;li&gt;Dart 使用 &lt;code&gt;dart language-server --protocol=lsp&lt;/code&gt;，并带上 wcode 的 Client ID / Version；&lt;/li&gt;
&lt;li&gt;Elixir 同时识别官方 &lt;code&gt;language_server.sh&lt;/code&gt;、Windows Wrapper 和常见发行版 &lt;code&gt;elixir-ls&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;LuaLS 对 Symlink 启动比较特殊，所以不能照抄 rustup Proxy 的处理方式，发现 Symlink 时会执行 Canonical Target；&lt;/li&gt;
&lt;li&gt;OmniSharp 的 &lt;code&gt;-lsp&lt;/code&gt; 不再被拿来凑 C# Fallback 数量，C# Canonical 路径只认 &lt;code&gt;csharp-ls&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正有意义的 Alternate 只留了三个：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PHP     phpactor → intelephense
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Python  pyright  → pylsp
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Ruby    ruby-lsp → solargraph
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;更重要的是，Fallback 不是只存在配置表里。&lt;/p&gt;
&lt;p&gt;现在 &lt;code&gt;semantic_navigation&lt;/code&gt; 和手工 &lt;code&gt;semantic_provider_refresh&lt;/code&gt; 都会在：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;canonical executable exists
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;initialize fails
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;try installed alternate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但 Alternate 不会继承 Canonical Provider 的授权。&lt;/p&gt;
&lt;p&gt;如果它属于非 Automatic Provider，就必须拿自己的 Workspace + Provider + Binary Identity Trust；Refresh 成功切换以后，结果里还会显式记录 &lt;code&gt;fallbacks&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以这里我想表达的不是“wcode 自带 22 个 Language Server”。&lt;/p&gt;
&lt;p&gt;它当然没有。&lt;/p&gt;
&lt;p&gt;而是：&lt;strong&gt;22/22 的 Adapter Contract、stdio Framing 和 Provider-specific Launch Profile 是 wcode 自己要负责的；External Server 是否安装要诚实报告；Semantic Precision 只有那份真实 Binary Live Initialize + 当前 Revision Response 以后才成立。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这比在 README 里写一个长长的“Supported Languages”列表可靠得多。&lt;/p&gt;
&lt;h2 id=&#34;默认开启以后安全边界反而要更严格&#34;&gt;默认开启以后，安全边界反而要更严格&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e9%bb%98%e8%ae%a4%e5%bc%80%e5%90%af%e4%bb%a5%e5%90%8e%e5%ae%89%e5%85%a8%e8%be%b9%e7%95%8c%e5%8f%8d%e8%80%8c%e8%a6%81%e6%9b%b4%e4%b8%a5%e6%a0%bc&#34; aria-label=&#34;章节链接：默认开启以后，安全边界反而要更严格&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这次另一个比较大的决定，是 Hardened Semantic 默认开启。&lt;/p&gt;
&lt;p&gt;也就是说普通启动：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wcode --workspace &lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt;$PWD&lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果项目里有 Rust，而且系统里有可用的 &lt;code&gt;rust-analyzer&lt;/code&gt;，wcode 会自动维护这条 Semantic Lane。&lt;/p&gt;
&lt;p&gt;但我没有把“所有 LSP 默认信任”一起打开。&lt;/p&gt;
&lt;p&gt;v0.5 当前只有 &lt;code&gt;rust-analyzer&lt;/code&gt; 进入 Automatic Profile。&lt;/p&gt;
&lt;p&gt;因为 Language Server 和普通 Parser 不一样：它会读项目配置，有些 Server 甚至可能间接执行 Repository-controlled Code。&lt;/p&gt;
&lt;p&gt;所以默认 Profile 做了几层限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Executable 必须解析到 Workspace 外；&lt;/li&gt;
&lt;li&gt;Workspace 里的假 &lt;code&gt;rust-analyzer&lt;/code&gt; 不会被执行；&lt;/li&gt;
&lt;li&gt;Credential 和 Execution-injection Environment Variable 会清理；&lt;/li&gt;
&lt;li&gt;Build Script 关闭；&lt;/li&gt;
&lt;li&gt;Proc Macro 关闭；&lt;/li&gt;
&lt;li&gt;Cargo Auto Reload 关闭；&lt;/li&gt;
&lt;li&gt;Check-on-save 关闭；&lt;/li&gt;
&lt;li&gt;Result 最后仍重新经过 Workspace Boundary Filter。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是 OS Sandbox。&lt;/p&gt;
&lt;p&gt;我不想用“安全模式”这种词让人误以为 Language Server 完全没有执行面。&lt;/p&gt;
&lt;p&gt;它只是一个我愿意默认打开的、被明显收窄过的 Profile。&lt;/p&gt;
&lt;p&gt;如果是 &lt;code&gt;clangd&lt;/code&gt;、Pyright、gopls 或其他当前还没有 Hardened Profile 的 Provider，仍然需要显式 &lt;code&gt;RiskyExecution&lt;/code&gt; Trust。&lt;/p&gt;
&lt;p&gt;而且 Warm Session 出现以后，授权语义也跟着变了。&lt;/p&gt;
&lt;p&gt;以前一次性 Provider 可以按某次 Refresh Operation 授权。&lt;/p&gt;
&lt;p&gt;现在 Process 会被复用，真正准确的 Trust 应该是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Workspace
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  + Provider
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  + current Provider Binary Identity
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个 Binary Identity 也不能漏。发版审计时我发现，第一版 Provider-session Fingerprint 只绑定了 Provider ID；如果 PATH 上同名 Provider Binary 被替换，Session Key 会重建，但旧授权理论上仍可能继续适用。现在 Authorization 和 Warm Session 使用同一套 Provider Binary Identity：Executable 被替换以后，旧 Grant 不会继承过去。&lt;/p&gt;
&lt;p&gt;因此同一份已批准 Provider 可以被 Refresh 和 Navigation 复用，不会每问一次 Reference 又弹一次权限；但它也不会顺手授权替换后的 Binary。&lt;/p&gt;
&lt;p&gt;如果完全不希望 wcode 启动第一方 Language Server：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wcode --workspace &lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt;$PWD&lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt; --no-semantic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tree-sitter / Search 仍然都在。&lt;/p&gt;
&lt;h2 id=&#34;后台自动维护也不能绕过全局资源边界&#34;&gt;后台自动维护也不能绕过全局资源边界&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%90%8e%e5%8f%b0%e8%87%aa%e5%8a%a8%e7%bb%b4%e6%8a%a4%e4%b9%9f%e4%b8%8d%e8%83%bd%e7%bb%95%e8%bf%87%e5%85%a8%e5%b1%80%e8%b5%84%e6%ba%90%e8%be%b9%e7%95%8c&#34; aria-label=&#34;章节链接：后台自动维护也不能绕过全局资源边界&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;另一个我不想接受的状态是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前台 Tool 都有 Global Semaphore，后台 Semantic Worker 却偷偷无限跑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那 TUI 上看到的并发数就会是假的。&lt;/p&gt;
&lt;p&gt;所以 Background Semantic Maintainer 也必须先拿 Harness Permit，再真正进入 Running。&lt;/p&gt;
&lt;p&gt;生命周期还是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;queued
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acquire global permit
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;running
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;completed / failed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Broad Workspace 下面如果还有具体 Project Subspace，也只让最具体的 Leaf Workspace 启动自动 Semantic Worker。&lt;/p&gt;
&lt;p&gt;不然我把 &lt;code&gt;~/Code&lt;/code&gt; 暴露给 wcode 时，父目录和十几个子项目会同时索引同一批源码。&lt;/p&gt;
&lt;p&gt;这种优化不会出现在“支持哪些 LSP”的 Feature List 里，但我觉得比多支持一个 Server 更重要。&lt;/p&gt;
&lt;h2 id=&#34;tui-现在能看出-warm-到底有没有生效&#34;&gt;TUI 现在能看出 Warm 到底有没有生效&lt;a class=&#34;heading-anchor&#34; href=&#34;#tui-%e7%8e%b0%e5%9c%a8%e8%83%bd%e7%9c%8b%e5%87%ba-warm-%e5%88%b0%e5%ba%95%e6%9c%89%e6%b2%a1%e6%9c%89%e7%94%9f%e6%95%88&#34; aria-label=&#34;章节链接：TUI 现在能看出 Warm 到底有没有生效&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;以前 TUI 的 LSP 状态主要是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;available / runnable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;fresh / stale
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;v0.5 现在还会显示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;warm sessions
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;synced documents
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;provider starts
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;semantic queries
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我特意加这些，不是为了让 Dashboard 再多几个数字。&lt;/p&gt;
&lt;p&gt;而是 Warm Runtime 最容易出现一种假优化：代码里写了 Cache，但实际上每次 Query 还是重启 Process。&lt;/p&gt;
&lt;p&gt;如果 Session Start 一直涨、Query 也一直涨，那我就知道复用没有真的工作。&lt;/p&gt;
&lt;p&gt;可观测性是性能优化的一部分，不是最后补的 UI。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-tui_hu_13e59e78ee0ce797.webp&#34; alt=&#34;wcode TUI&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-tui_hu_13e59e78ee0ce797.webp 960w, https://francisdu.com/img/wcode/wcode-tui_hu_6437a17a51f06ca3.webp 1800w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;3420&#34; height=&#34;2146&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;v04-和-v05-的差别&#34;&gt;v0.4 和 v0.5 的差别&lt;a class=&#34;heading-anchor&#34; href=&#34;#v04-%e5%92%8c-v05-%e7%9a%84%e5%b7%ae%e5%88%ab&#34; aria-label=&#34;章节链接：v0.4 和 v0.5 的差别&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;如果 v0.4 是“让 Intelligence 不要变成 Context Tax”，v0.5 更像是“让语义能力真正进入日常 Coding Hot Path”。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;v0.4&lt;/th&gt;
					&lt;th&gt;v0.5&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Semantic Provider&lt;/td&gt;
					&lt;td&gt;有界 Batch Refresh&lt;/td&gt;
					&lt;td&gt;Bounded Warm Runtime&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;LSP Process&lt;/td&gt;
					&lt;td&gt;Refresh 后退出&lt;/td&gt;
					&lt;td&gt;Workspace Session 复用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Document Sync&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;didOpen&lt;/code&gt; 为主&lt;/td&gt;
					&lt;td&gt;Server-declared Full / Incremental / None&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent Navigation&lt;/td&gt;
					&lt;td&gt;Syntax + Graph Context&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;semantic_navigation&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Default Routing&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;agent_context&lt;/code&gt; + Tree-sitter&lt;/td&gt;
					&lt;td&gt;Localization 走 Syntax，Relationship 才走 LSP&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Automatic Trust&lt;/td&gt;
					&lt;td&gt;LSP 需要显式 Trust&lt;/td&gt;
					&lt;td&gt;Hardened &lt;code&gt;rust-analyzer&lt;/code&gt; 默认开启&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Non-auto LSP&lt;/td&gt;
					&lt;td&gt;Exact Refresh Trust&lt;/td&gt;
					&lt;td&gt;Workspace + Provider Session &lt;code&gt;RiskyExecution&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;TUI&lt;/td&gt;
					&lt;td&gt;available / fresh&lt;/td&gt;
					&lt;td&gt;available → launch-ready → live + warm/fresh&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我觉得这已经不是 0.4.x 的 Patch。&lt;/p&gt;
&lt;p&gt;所以版本直接到了 &lt;strong&gt;v0.5.0&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;这次发版我也把-release-boundary-再收紧了一次&#34;&gt;这次发版我也把 Release Boundary 再收紧了一次&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e8%bf%99%e6%ac%a1%e5%8f%91%e7%89%88%e6%88%91%e4%b9%9f%e6%8a%8a-release-boundary-%e5%86%8d%e6%94%b6%e7%b4%a7%e4%ba%86%e4%b8%80%e6%ac%a1&#34; aria-label=&#34;章节链接：这次发版我也把 Release Boundary 再收紧了一次&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Milestone Release 最怕的不是功能没写完，而是“仓库里的版本看起来有六个答案”。&lt;/p&gt;
&lt;p&gt;wcode 现在除了 Cargo Version，还有 Agent Plugin / Marketplace Manifest。&lt;/p&gt;
&lt;p&gt;v0.5 发版前我把它们统一成同一个版本，并且不只依赖 CI Shell Script 检查。&lt;/p&gt;
&lt;p&gt;Unit Test 也会验证：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Cargo package version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == plugin.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == Claude plugin
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == Codex plugin
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == ZCode plugin
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == root marketplace
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  == plugin marketplace
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Release Workflow 还会再独立检查一次，然后跑：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Design / Traceability / Product Scope gate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Format
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Check
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Clippy --all-targets
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Linux test
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;macOS test
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Windows test
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Linux / macOS / Windows release build
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;binary --version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SHA256SUMS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这次 Tag 前最后一轮本地 Full Gate 是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git diff --check                       ✅
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo check --locked                   ✅
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo fmt --check                      ✅
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo test --locked                    ✅ 270 passed / 0 failed
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo clippy --locked -- -D warnings   ✅
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo build --release --locked         ✅
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 270 个核心测试已经包含 22 个 Canonical Profile 的真实 stdio Mock-LSP Initialize Contract、Full/Incremental/None Document Sync、Warm Session Capacity/Idle、Provider Binary Trust、Fallback 和 Navigation Failure Semantics。&lt;/p&gt;
&lt;p&gt;我仍然不想把“本机 cargo test 绿了”直接等同于“Release 已经成立”。&lt;/p&gt;
&lt;p&gt;真正发布的是 Tagged Revision 和对应 Artifact；Linux/macOS/Windows 的最终跨平台结论继续交给 Tag CI。&lt;/p&gt;
&lt;h2 id=&#34;最后&#34;&gt;最后&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%9c%80%e5%90%8e&#34; aria-label=&#34;章节链接：最后&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;wcode 最开始只是我为了让 Web AI 安全碰本地代码写的一层 MCP Bridge。&lt;/p&gt;
&lt;p&gt;后来它慢慢有了 Design State、Software Graph、Verification、Evidence、Reconciliation。&lt;/p&gt;
&lt;p&gt;v0.4 我开始对 Agent 的 Context 成本负责。&lt;/p&gt;
&lt;p&gt;到 v0.5，我又多了一层判断：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;代码智能不应该只有“便宜但不完整”和“准确但每次很重”两个极端。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Tree-sitter / Search 可以一直做便宜、稳定的定位底座。&lt;/p&gt;
&lt;p&gt;Language Server 则应该在真正需要关系完整性的地方，以受限、可复用、可观测的 Runtime 形式出现。&lt;/p&gt;
&lt;p&gt;不是把所有东西都升级成 LSP。&lt;/p&gt;
&lt;p&gt;而是让 Agent 知道：什么时候 Syntax 已经够了，什么时候值得支付 Semantic Cost。&lt;/p&gt;
&lt;p&gt;这应该会是后面 wcode Semantic Runtime 继续扩展其他语言时最重要的一条原则。&lt;/p&gt;
&lt;p&gt;代码：&lt;a href=&#34;https://github.com/francis-du/wcode&#34;&gt;https://github.com/francis-du/wcode&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;文档：&lt;a href=&#34;https://wcode.francis.run/&#34;&gt;https://wcode.francis.run/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;v0.5.0 Release Notes：&lt;a href=&#34;https://wcode.francis.run/docs/releases/v0.5.0/&#34;&gt;https://wcode.francis.run/docs/releases/v0.5.0/&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>wcode 的 Software Graph：先承认自己不知道</title>
      <link>https://francisdu.com/blog/wcode-software-graph/</link>
      <pubDate>Wed, 26 Aug 2026 03:12:00 +0800</pubDate>
      <guid>https://francisdu.com/blog/wcode-software-graph/</guid>
      <description>&lt;p&gt;最早写 wcode 的代码索引时，我没想过要做什么 Software Graph。&lt;/p&gt;
&lt;p&gt;当时的问题很直接：模型一进大文件就喜欢整份读，几百上千行源码一股脑塞进上下文。于是我先做了 Tree-sitter，给它几个更小的入口：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;file_outline
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;find_symbol
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;symbol_context
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这套东西到现在都还在，而且是我最喜欢的一层。便宜、稳定，不用启动项目。&lt;/p&gt;
&lt;p&gt;后来开始做 Impact，我才发现“临时查一下 Symbol”不够了。&lt;/p&gt;
&lt;p&gt;我需要知道 A 和 B 的关系，还需要知道这条关系是谁告诉我的、什么时候算出来的、源码变了以后还能不能信。&lt;/p&gt;
&lt;p&gt;这才有了 Software Graph。&lt;/p&gt;
&lt;h2 id=&#34;tree-sitter-知道的没有想象中那么多&#34;&gt;Tree-sitter 知道的没有想象中那么多&lt;a class=&#34;heading-anchor&#34; href=&#34;#tree-sitter-%e7%9f%a5%e9%81%93%e7%9a%84%e6%b2%a1%e6%9c%89%e6%83%b3%e8%b1%a1%e4%b8%ad%e9%82%a3%e4%b9%88%e5%a4%9a&#34; aria-label=&#34;章节链接：Tree-sitter 知道的没有想象中那么多&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Tree-sitter 很适合做结构分析。&lt;/p&gt;
&lt;p&gt;定义在哪里、Range 是多少、Qualified Name 是什么，这些都比较稳。一些语法上能明确判断的调用关系，也可以抽出来。&lt;/p&gt;
&lt;p&gt;但它没有编译器的类型系统，也不会替我做宏展开、重载选择和动态分派。&lt;/p&gt;
&lt;p&gt;所以 Tree-sitter 产生的关系一直带着：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;provider = tree-sitter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;precision = syntax
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我后来越来越在意 &lt;code&gt;precision&lt;/code&gt; 这个词。&lt;/p&gt;
&lt;p&gt;做 Agent 工具时，很容易为了让返回结果“看起来更聪明”，把一个启发式结果包装得像确定事实。短期体验会很好，后面做 Impact、Risk 时却很危险，因为上层已经不知道底下到底有多靠谱。&lt;/p&gt;
&lt;p&gt;所以这里干脆先承认自己不知道。&lt;/p&gt;
&lt;h2 id=&#34;lsp-也不是装了就算-semantic&#34;&gt;LSP 也不是装了就算 semantic&lt;a class=&#34;heading-anchor&#34; href=&#34;#lsp-%e4%b9%9f%e4%b8%8d%e6%98%af%e8%a3%85%e4%ba%86%e5%b0%b1%e7%ae%97-semantic&#34; aria-label=&#34;章节链接：LSP 也不是装了就算 semantic&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;现在 wcode 有一套第一方 Semantic Provider，会探测 rust-analyzer、gopls、clangd、typescript-language-server、pyright 这些 Language Server。&lt;/p&gt;
&lt;p&gt;但“机器上有这个二进制”和“当前结果是 semantic”是两回事。&lt;/p&gt;
&lt;p&gt;只有 Language Server 真正启动、返回 Document Symbol / Call Hierarchy / Implementation，这些结果才进入 Graph，并标成：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;precision = semantic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Repository-aware Language Server 也不是默认无条件执行。&lt;/p&gt;
&lt;p&gt;它可能加载项目配置、Build Metadata、插件，甚至间接执行仓库里的东西。现在有两种授权方式：启动 wcode 时直接用 &lt;code&gt;--allow-risky-exec&lt;/code&gt; 做进程级放行；或者让具体 Refresh 先触发本地 Authorization Request，我在 TUI 里批准这个操作后再重试。&lt;/p&gt;
&lt;p&gt;后者是我后来补的，因为很多时候我只想临时跑一次 rust-analyzer，不想顺便把整个 Runtime 后面的高风险执行都放开。&lt;/p&gt;
&lt;h2 id=&#34;stale-semantic-比没有-semantic-更糟&#34;&gt;stale semantic 比没有 semantic 更糟&lt;a class=&#34;heading-anchor&#34; href=&#34;#stale-semantic-%e6%af%94%e6%b2%a1%e6%9c%89-semantic-%e6%9b%b4%e7%b3%9f&#34; aria-label=&#34;章节链接：stale semantic 比没有 semantic 更糟&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这里有个很容易忽略的问题。&lt;/p&gt;
&lt;p&gt;假设上午跑过 rust-analyzer，拿到一组 Call Hierarchy；下午我已经把源码改了一大轮。如果 Impact 还拿上午的结果继续推导，它会表现得很“精准”，实际上精准地错了。&lt;/p&gt;
&lt;p&gt;所以第一方 LSP Fact 会带 &lt;code&gt;source_sha256&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;源码对不上，这个 Provider Revision 就会 stale。stale 的关系不会再进新的 Software Graph，也不会继续参与 &lt;code&gt;software_context&lt;/code&gt; 和 Impact。&lt;/p&gt;
&lt;p&gt;要用就重新 Refresh。&lt;/p&gt;
&lt;p&gt;这点比“自动保持 semantic 数据永远最新”笨一点，但边界清楚。&lt;/p&gt;
&lt;h2 id=&#34;同一条关系可以有几个答案&#34;&gt;同一条关系可以有几个答案&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%90%8c%e4%b8%80%e6%9d%a1%e5%85%b3%e7%b3%bb%e5%8f%af%e4%bb%a5%e6%9c%89%e5%87%a0%e4%b8%aa%e7%ad%94%e6%a1%88&#34; aria-label=&#34;章节链接：同一条关系可以有几个答案&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Software Graph 没有试图把所有来源揉成一个最终真相。&lt;/p&gt;
&lt;p&gt;一条 Edge 会保留自己的：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;provider
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;precision
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;revision
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;attributes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以同一个 &lt;code&gt;A -&amp;gt; B&lt;/code&gt; 完全可能同时有：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Tree-sitter syntax call
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;LSP semantic call
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Runtime observed call
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它们不互相覆盖。&lt;/p&gt;
&lt;p&gt;如果以后接 SCIP、Compiler Index 或 Runtime Trace，也还是走同一个 provider-neutral contract。&lt;/p&gt;
&lt;p&gt;这样做的好处是，上层可以自己决定信谁。&lt;/p&gt;
&lt;p&gt;Impact 遇到真实 Runtime Edge，可以用真实运行关系；只有 Syntax Edge 也能继续工作，只是结果需要更保守。&lt;/p&gt;
&lt;h2 id=&#34;graph-history-解决的是另一个问题&#34;&gt;Graph History 解决的是另一个问题&lt;a class=&#34;heading-anchor&#34; href=&#34;#graph-history-%e8%a7%a3%e5%86%b3%e7%9a%84%e6%98%af%e5%8f%a6%e4%b8%80%e4%b8%aa%e9%97%ae%e9%a2%98&#34; aria-label=&#34;章节链接：Graph History 解决的是另一个问题&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;当 Graph 开始被拿来做分析以后，我还想看结构到底怎么变的。&lt;/p&gt;
&lt;p&gt;于是加了：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;graph_history
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;graph_query
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;graph_diff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里没有每查询一次就存一份快照。图内容没变，就不制造历史噪音。&lt;/p&gt;
&lt;p&gt;Node 用稳定 ID 对齐。Edge 则按：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;from + to + kind + provider + precision
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先找身份。&lt;/p&gt;
&lt;p&gt;如果只是 Revision 或 Attributes 变了，就算 &lt;code&gt;changed&lt;/code&gt;，而不是先删一条再新增一条。&lt;/p&gt;
&lt;p&gt;这类实现细节平时没什么存在感，但图一旦有几十版历史，没有稳定身份很快就看不下去了。&lt;/p&gt;
&lt;h2 id=&#34;我后来把-webui-的球图降级了&#34;&gt;我后来把 WebUI 的球图降级了&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%88%91%e5%90%8e%e6%9d%a5%e6%8a%8a-webui-%e7%9a%84%e7%90%83%e5%9b%be%e9%99%8d%e7%ba%a7%e4%ba%86&#34; aria-label=&#34;章节链接：我后来把 WebUI 的球图降级了&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Software Graph 做完后，我也走过一个很自然的弯路：既然已经有图，那就在 WebUI 里画出来。&lt;/p&gt;
&lt;p&gt;于是有过一个能缩放、拖拽、点 Node 看 provenance 的 Graph Canvas。&lt;/p&gt;
&lt;p&gt;我自己打开几次以后发现，它更像 Debug Tool，不像项目管理页面。&lt;/p&gt;
&lt;p&gt;我真正想查的是某个 Requirement 现在由谁实现、Acceptance 在哪、设计依赖和代码依赖有没有分叉、最近改动碰到什么，而不是盯着几十个圆点猜哪条线比较重要。&lt;/p&gt;
&lt;p&gt;所以现在主 UI 已经改成 Project Observatory，按 Requirement 往下看 Feature、Component、Implementation、Verification 和 Git Change。&lt;/p&gt;
&lt;p&gt;低层 Graph 没消失。&lt;/p&gt;
&lt;p&gt;它仍然是 Context、Impact、历史 Diff 的数据来源，只是不再承担“解释整个项目”的视觉任务。&lt;/p&gt;
&lt;p&gt;做到这里我才确定一件事：底层有 Graph，不代表 UI 也应该是一团 Graph。&lt;/p&gt;
&lt;p&gt;Graph 到这里还只是底层事实。真正拿它去做 Review 时，问题会变成另一句：Diff 之外，这次到底动了什么。那部分我放在 &lt;a href=&#34;https://francisdu.com/blog/wcode-traceability/&#34;&gt;Git Diff 之外，我还想知道什么&lt;/a&gt; 里。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>