From bdeefccbb356e7a8ab386e994fdff922ccabc898 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 13 Oct 2026 19:54:32 +0900 Subject: [PATCH 1/9] 0.0.15: what clangd answered before stands in while it is slow; agents are told which answers are which (mcppls-clangd 0.0.16) Implements .agents/docs/2026-10-13-answer-cache-layer-design.md: the answer layer (keys K1-K3, certainty from the visibility fingerprint, the enclosing text, other files' changes and the provider's generation; the answer budget, dedup, cancelled requests going on in the background, prefetch while idle, persistence), item-level marks only, the outline that corrects a changed class's members and relocates jumps, cxxModules/completionReady (S3 5.9), answers given before the engine's (S3 6.3), provenance for agents and the command line with fresh / --fresh (S5 2.6), mcppls.answer.* and mcppls.completion.provenance, the engine's module-input-recheck and interactive-actions options, retained ASTs by memory, and a guide to laying out module projects (docs/25-module-projects.md). --- .../2026-10-13-answer-cache-layer-design.md | 545 ++++++++++++ .agents/docs/design.md | 13 + CHANGELOG.md | 52 ++ conformance/traceability.json | 85 ++ docs/20-projects.md | 3 + docs/25-module-projects.md | 199 +++++ docs/30-settings.md | 7 +- docs/40-agents.md | 16 + docs/50-troubleshooting.md | 14 + docs/90-architecture.md | 7 + docs/README.md | 1 + docs/specs/CHANGELOG.md | 11 + docs/specs/s3-lsp-extensions.md | 23 +- docs/specs/s5-semantic-query.md | 37 +- docs/zh-CN/20-projects.md | 2 + docs/zh-CN/25-module-projects.md | 154 ++++ docs/zh-CN/30-settings.md | 7 +- docs/zh-CN/40-agents.md | 10 + docs/zh-CN/50-troubleshooting.md | 6 + docs/zh-CN/90-architecture.md | 1 + docs/zh-CN/README.md | 1 + editors/agents/README.md | 8 +- .../.claude-plugin/marketplace.json | 2 +- .../mcppls-lsp/.claude-plugin/plugin.json | 2 +- editors/clion/gradle.properties | 2 +- editors/vscode/package.json | 57 +- editors/vscode/package.nls.json | 16 +- editors/vscode/package.nls.zh-cn.json | 16 +- editors/vscode/src/completionGate.ts | 14 + editors/vscode/src/extension.ts | 16 +- editors/vscode/src/settingsRead.ts | 20 + .../vscode/test/unit/completionGate.test.ts | 10 +- editors/vscode/test/unit/settingsRead.test.ts | 16 +- editors/zed/extension.toml | 2 +- mcpp.toml | 2 +- modules/base/src/version.cppm | 2 +- src/ai/mcp/tools.cpp | 18 +- src/ai/query/view.cpp | 19 +- src/ai/query/view.cppm | 8 + src/cli/options.cpp | 5 + src/cli/query.cpp | 15 + src/config/settings.cpp | 46 +- src/engine/clangd.cpp | 4 + src/engine/clangd/guard.cpp | 7 +- src/engine/clangd/guard.cppm | 3 + src/engine/clangd/process.cpp | 2 + src/engine/clangd/process.cppm | 4 + src/orchestrator/answer.cpp | 806 ++++++++++++++++++ src/orchestrator/answer.cppm | 170 ++++ src/orchestrator/answering.cpp | 463 ++++++++++ src/orchestrator/answering.cppm | 148 ++++ src/orchestrator/kernel.cpp | 4 +- src/orchestrator/kernel.cppm | 4 +- src/orchestrator/outline.cpp | 353 ++++++++ src/orchestrator/outline.cppm | 25 + src/orchestrator/workspace.cpp | 388 ++++++++- src/orchestrator/workspace.cppm | 7 + src/server/session.cpp | 5 + tests/test_answer.cpp | 332 ++++++++ tests/test_answer_routing.cpp | 473 ++++++++++ tests/test_workarounds.cpp | 8 +- 61 files changed, 4659 insertions(+), 37 deletions(-) create mode 100644 .agents/docs/2026-10-13-answer-cache-layer-design.md create mode 100644 docs/25-module-projects.md create mode 100644 docs/zh-CN/25-module-projects.md create mode 100644 src/orchestrator/answer.cpp create mode 100644 src/orchestrator/answer.cppm create mode 100644 src/orchestrator/answering.cpp create mode 100644 src/orchestrator/answering.cppm create mode 100644 src/orchestrator/outline.cpp create mode 100644 src/orchestrator/outline.cppm create mode 100644 tests/test_answer.cpp create mode 100644 tests/test_answer_routing.cpp diff --git a/.agents/docs/2026-10-13-answer-cache-layer-design.md b/.agents/docs/2026-10-13-answer-cache-layer-design.md new file mode 100644 index 00000000..fee0b88d --- /dev/null +++ b/.agents/docs/2026-10-13-answer-cache-layer-design.md @@ -0,0 +1,545 @@ +# 多级缓存应答层:补全、签名、悬停、跳转(2026-10-13) + +目标:项目准备好之后,**任何项目、任何文件,交互请求都在 0.5 s 内响应**;之后到达的权威结果**无感地**替换先给出的结果。冷启动和第一次加载可以慢,但**同类请求见过一次,之后就一定在 0.5 s 内响应**。 + +不同使用面的侧重不同: +- **编辑器用户**:以无感为核心,不暴露底层机制; +- **agent(MCP 工具)和 CLI**:每个结果都明确标注来源与确定性; +- **日志**:每一次应答都带来源标记,方便调试。 + +本文先说明 0.0.14 的应答路径和它在大型模块项目上的缺口,再给出分层与职责。然后依次是:应答层的通用框架;补全、签名帮助、悬停、跳转、引用各自的策略;各使用面的可见性规则;引擎侧的配合;指标与测试;设置、风险、计划,以及多视角自审。 + +## 0. 结论 + +1. **分两层,各管一件事。** + - **mcppls-clangd 只管正确**:给定一个文档快照,给出权威答案。可以慢,但不猜。 + - **mcppls 管快、交互和可见性**:多级缓存、请求去重、后台续跑、结果合成、无感刷新、持久化,以及各使用面的标注。这一层只依赖标准 LSP 请求,与核心引擎是谁无关。 +2. **大部分答案与文件无关,可以跨文件、跨会话缓存。** + - `std::` 的成员、`ordered_json` 的成员、`std::vector` 的悬停、`spdlog::info` 的定义位置,在可见集合相同的文件里都一样; + - 只有局部变量和 `this` 的成员是文件私有的。 +3. **一套通用框架,按请求类型配不同的策略。** + - 通用部分:三级键(K1 位置、K2 语义、K3 名字)、可见集合指纹、代际、多级存储、引擎代理、合成器。 + - 补全和签名帮助:可以给不确定的结果,再原地更新。 + - 悬停:可以给不确定的结果,下次悬停时更新。 + - 跳转:只给确定的结果,或者经过文本校验后能精确定位的结果,否则等引擎。跳错位置的代价比列表里多一项大得多。 +4. **截止时间 0.45 s。** 到时权威结果没来,就用缓存合成一个结果。引擎请求**不取消**,跑完后写回缓存。 +5. **可见性按使用面区分。** + - 编辑器:不主动重弹列表,不确定的项默认只带一个很浅的 `·`,确定的项不加任何标记; + - agent:默认等权威结果(和 `cxx_diagnostics` 的 `fresh` 一致),用了缓存时,在结果里给出来源、确定性、代际和缓存年龄; + - CLI 和 report:输出命中统计和来源分布; + - 日志:每次应答都有一行统一格式的来源记录。 +6. **引擎侧三个补丁(E1、E2、E4),加一项 mcppls 侧的用法调整(E5)**,补丁都写成能提交上游的形式;E3 经核实不做(第 7 节)。预计把大型模块项目上一次补全的权威结果从 3.1–3.5 s 降到约 2 s,悬停从秒级降到毫秒级,具体以测量为准。 +8. **模块项目怎样组织代码更快**(包装模块、头文件收敛、不混用),单独成文:`docs/25-module-projects.md`。 +7. **不改 mcpp。** 所需的信息(模块图、文件变化、编译命令)mcppls 都已经有了。 + +## 1. 现状 + +### 1.1 0.0.14 的应答路径(`src/orchestrator/workspace.cpp`、`completion.cpp`、`routing.cpp`) + +| 机制 | 做什么 | 范围 | +|---|---|---| +| R-7 预算(`answer_budget`) | 补全和签名帮助 1000 ms,悬停 2000 ms,跳转类 10000 ms;到时引擎没答,就用 mcppls 自己的结果作答 | 每个请求 | +| UP-25 `EnginePace` | 某个文件最近的引擎答案都稍晚于 1 s 时,补全预算放宽,最多到 2.5 s;远超上限的答案不计入 | 每个文件 | +| C-2 `LateCompletion` / `LateAnswer` | 补全预算到了,引擎请求继续跑(最多 10 s);同一个词后续的请求等它,或者直接用它(5 s 内) | 每个文档一条 | +| A1 `WordAnswer` | 引擎的完整答案按词保留 30 s;同一个词里继续打字、词外文本没变时直接复用 | 每个文档一条 | +| 补全兜底 `document_words` | 文件里前缀相同的单词,加关键字;在 `.`、`->`、`::` 之后**什么都不给** | — | +| 悬停兜底(`explain_if_preparing`) | 说明模块还在准备 | — | +| agent 工具(`cxx_symbol`、`cxx_references` 等) | 经由同一个会话向引擎查询,有工具级的截止时间;`cxx_diagnostics` 有 `fresh` 参数(默认等待新结果) | 每次调用 | + +这些机制都**只认"同一个文档里的同一个词",而且只保留几秒到几十秒**。悬停和跳转完全没有缓存。 + +### 1.2 缺口:大型模块项目 + +测量对象是 Windows 上一个大型模块项目,有 25 个前置模块的实现单元,用 CI windows runner(2 核 4 线程)测量: + +- **补全**:引擎答一次要 **3.1–3.5 s**,超过 `EnginePace` 的上限,每次都在 1 s 处回退。回退在 `.` 之后是空列表,所以三轮测量里这个文件的成员补全都是 **0/8**。这 3.1–3.5 s 由三部分组成: + - 模块输入校验:0.67–1.0 s + - 编译 + Sema:2.2–2.3 s + - 结果整理:0.25–0.27 s +- **悬停**:在 2 s 处回退,答案不对。直接用引擎时,在同一个文件上悬停要 7–11 s,这个数字与重建一次 AST 的时间相当:打开的文件比常驻 AST 的数量多。 +- **跳转**:一个跨模块的定义 4.5–9.5 s,结果不对。 +- 同一项目里只依赖 4 个模块的小文件,补全 0.4–0.7 s。问题集中在依赖多、BMI 大的文件上,而这类文件正是项目里最常编辑的。 + +## 2. 目标与非目标 + +**目标** +- G1:稳定之后(项目模型就绪、引擎完成首轮准备),**补全、签名帮助、悬停的响应 p99 ≤ 500 ms**,不论文件多重。跳转:缓存确定时 ≤ 500 ms,否则在引擎的时间内给出正确结果。 +- G2:同类请求见过一次后,必然命中缓存。"同类"包括同一位置、同一作用域或类型、同一符号,跨文件、跨会话都算。 +- G3:编辑器里,权威结果到达后**无感**替换:不重弹列表,不打乱顺序,不跳光标。 +- G4:缓存结果的错误项有界,并且可以度量(第 9 节)。 +- G5:agent 和 CLI 拿到的每一个结果,都能知道它的来源和确定性;日志可以按来源检索。 +- G6:只用标准 LSP 加 mcppls 已有的自定义通知,所有编辑器都可用;VS Code 体验最好。 + +**非目标** +- 不在 mcppls 里做语义分析:类型和符号身份只来自标准请求(`typeDefinition`、`definition`)和文本大纲。 +- 不改 mcpp,也不改构建数据库格式。 +- 冷启动和第一次加载的时间不在本文范围(见 `2026-10-12-windows-performance-plan.md`)。 +- 语义高亮、文档符号、诊断不纳入缓存:语义高亮和诊断本来就跟着 AST 走,文档符号和工作区符号已经由 native 层回答。 + +## 3. 分层与职责 + +``` +┌──────────────────── mcppls:快、交互、可见性 ────────────────────┐ +│ 应答层(新,orchestrator/answer) │ +│ 上下文提取 → 键与指纹 → 多级缓存 → 合成器 → 按请求类型的策略 │ +│ 引擎代理:去重、不随客户端取消、后台续跑、交互优先、并发上限 │ +│ 可见性:编辑器无感 / agent 与 CLI 标注 / 日志标记 │ +│ 刷新:isIncomplete 原地更新;仅一种情况主动拉一次 │ +│ 预取:空闲时在文件已有的触发点上提问 │ +│ 持久化:工作区缓存 + 全局缓存;按代际失效;防污染 │ +│ 代际服务(新,汇总已有的信息):文件监听 + 模块图 + BMI 重建 │ +└─────────┬──────────────────────────────────────▲─────────────────┘ + │ 标准 LSP:completion、completionItem/resolve、signatureHelp、 + │ hover、definition、declaration、typeDefinition、implementation + │ + 输入核对的有效期(E1)+ 交互槽位(E4)+ 常驻 AST 数量(E5) +┌─────────▼──────────────────────────────────────┴─────────────────┐ +│ mcppls-clangd:只管正确 │ +│ 给定快照,给出权威答案;不返回不确定的结果 │ +│ 正确性前提下的效率补丁 E1、E2、E4,都能提交上游 │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**层间约定** + +- C1 **引擎不猜**:返回的一定是它在那个快照上算出的结果。 +- C2 **mcppls 不改写权威结果**:原样返回,只额外记录它对应的文档版本、代际和键。 +- C3 **代际只有一个来源,就是 mcppls**:文件监听、模块图、BMI 重建都在 mcppls 汇总;引擎的输入校验信任 mcppls 的提示(E1)。 +- C4 **引擎可以替换**:应答层只用标准请求。引擎不支持某个请求时(比如 `typeDefinition`),对应的学习能力关掉,其余照常工作。 +- C5 **每个应答都知道自己是从哪里来的**:应答层内部所有结果都带一个来源记录(见 4.11)。各使用面只决定把它显示到什么程度,不决定有没有。 + +## 4. 通用框架 + +### 4.1 上下文提取(同步,目标 < 5 ms) + +只读文档文本,借助已有的 native 词法与模块扫描: + +| 字段 | 例子 | 用于 | +|---|---|---| +| 位置类型 | 成员访问(`.`、`->`)、限定名(`::`)、无限定、调用实参中、import 行、预处理行 | 选择键与策略 | +| 光标处的标识符,以及它前面的接收者或限定符 | `dump`;`fileOverview`;`std::views`;`fs` | K1、K3 | +| 所在声明 | 外层函数、类、命名空间的名字与起始行 | K1 | +| 已输入的前缀 | `push_b` | 补全过滤 | +| 命名空间别名、`using` | `namespace fs = std::filesystem;` | 规范化 K3 | +| 局部上下文哈希 | 所在声明体内、光标以前的文本 | 判断 K1 是否确定(4.3) | + +import 行和空格触发沿用现有逻辑(F9、F15),不经过这一层。 + +### 4.2 三级键 + +- **K1 位置键**:(文档,所在声明,位置类型,标识符或接收者、限定符的文字)。最精确,文件私有。"打了又删""同一处反复补全""同一处反复悬停"命中的就是这一级。A1 和 C-2 按词的单条记录保留在它前面,作为同一个词里继续打字的最快路径;K1 补上它们够不到的情况:每个文档可以保留很多条,有效期按代际判断,不再固定为 5–30 s。 +- **K2 语义键**:(符号或类型的规范名,定义位置,可见集合指纹)。**跨文件共享。** + - 限定名:规范化之后直接得到,例如 `fs::path` → `std::filesystem::path`。 + - 成员访问、标识符:符号身份靠**学习**。权威结果回来后,在后台对接收者发 `typeDefinition`,对悬停或跳转的标识符发 `definition`。有常驻 AST 时这些都很便宜。学到的结果记成"(文档,文字,所在声明) → (规范名,定义位置)"。之后同一个符号在任何文件出现,都能映射到 K2。推不出来时(模板依赖、宏)只用 K1。 +- **K3 名字键**:(规范化的限定名,工具链与标准库指纹)。用于 K2 还推不出来的时候,比如 `std::`、`std::views::`、`std::vector`。**跨项目共享**,存在全局缓存里。 + +### 4.3 确定性:指纹与代际 + +每个缓存条目在被使用时,都要判断它是**确定**还是**不确定**。 + +**可见集合指纹**:影响可见性的编译参数(标准、`-D`、目标、标准库),加上文件 import 的模块闭包,再加上 preamble 部分(`#include` 与 GMF)的内容哈希。 +- 完全一致:指纹相符。 +- 不一致但提供该作用域或符号的模块、头文件相同:**兼容**,只能作为不确定的结果使用。 +- 其他情况:不使用。 + +**代际**:每个模块单元、每个文件都有一个整数代际,由 mcppls 在下面三种情况下加一: +- 文件内容变化(编辑器的修改、保存,或者磁盘上的变化); +- 依赖的模块重建; +- 项目模型更新。 + +每个条目记下写入时相关模块和文件的代际向量。 + +**判定**: +- K2、K3:指纹相符,而且提供者的代际一致 → **确定**;否则 → **不确定**。 +- K1:除上面两条外,还要求局部上下文哈希一致(光标所在声明里、光标之前的文本没变)→ 确定。否则不确定,并且补全时并入 `document_words` 里的局部单词,补上新出现的局部变量。 + +### 4.4 存储 + +| 层 | 内容 | 容量与淘汰 | +|---|---|---| +| 内存 | 全部键的条目 | 按条目数和字节数做 LRU;每个文档的 K1 条目有单独上限 | +| 工作区磁盘缓存 | K1(最近的文档)、K2、学到的符号映射 | 放在工作区的 mcppls 缓存目录,计入 C-8 的缓存预算,`mcppls cache` 统计并清理 | +| 全局磁盘缓存 | K3 | 按工具链加标准库指纹分区,有总量上限 | + +- 条目只存显示与插入需要的字段(第 5 节逐类说明)。补全项的文档注释不存,交给 `resolve`。 +- 文件格式带版本号,版本不认识就整块丢弃。写入是原子的(先写临时文件再改名)。 +- 工作区不受信任时,不写磁盘,只用内存。 + +### 4.5 引擎代理 + +- **去重**:同一个 K1 加同一个请求类型,在飞的引擎请求只保留一个。同一个词里继续打字产生的补全请求、在同一处反复悬停,都由缓存回答。这同时降低了引擎的排队。 +- **不随客户端取消**:客户端发 `$/cancelRequest` 时,引擎请求继续跑完,结果写进缓存。引擎在收到请求时已经拿到了当时的文本快照,所以之后的编辑不影响这个结果对它那个键的有效性。 +- **上限**: + - 后台续跑同时最多 2 个,超出时最旧的那个真正取消; + - 单个最长 30 s(C-2 的迟到答案仍是 10 s); + - 有交互请求在等时,后台续跑和预取都让路。 +- **与现有机制的关系**: + - A1(按词的完整答案)和 C-2(同一个词等同一个迟到答案)保留,排在应答层前面:同一个词里继续打字,它们比查表更快,也已有完整的测试;应答层处理它们够不到的情况(换了一行、换了文件、打了又删、取消后再问); + - `EnginePace` 保留它对 1 s 预算的自适应;有保存的答案时,预算换成应答层的预算。 + +### 4.6 合成器 + +1. 收到请求后立即查缓存,并把请求交给引擎代理(去重之后可能并不新发)。 +2. 到了截止时间(默认 0.45 s;跳转见 5.4): + - 权威结果到了:原样返回,回写缓存; + - 没到:按 K1 > K2 > K3 > native 取第一个可用的结果,按第 5 节对应请求类型的规则处理后返回; + - 都没有:返回 native 的结果(补全给单词和关键字,悬停给"准备中"的说明),或者按策略继续等。 + +### 4.7 用最新文本修正 + +成员或声明"刚变"时,定义它的文件刚被编辑或保存过,mcppls 手里有最新文本。新增一个**轻量声明大纲**:只做词法级解析,得出 `class`/`struct` 体内的成员名,以及命名空间里声明的名字和行号,拿不准就不出结果。 + +当某个条目的提供者代际变了: +- **补全**:大纲里新出现的成员补进列表;大纲里已经没有的成员排到最后。两者都算不确定。 +- **悬停**:大纲里这个名字已经不在了,就不用这条缓存。 +- **跳转**:用大纲在目标文件里重新找到这个名字的声明,只找到一处时才用(5.4)。 + +同时把相关条目标为过期,安排空闲时刷新。遇到宏、条件编译、模板特化,或者大纲解析失败时,不做修正,只保留不确定标记。 + +### 4.8 预取 + +只在空闲时做:2 s 内没有输入,而且没有交互请求在处理。一次一个,用户一打字就暂停。 +- **重放文件里已有的触发点**:每个 `x.`、`x->`、`ns::` 后面都是合法的补全位置;光标附近的标识符可以预先悬停和取定义。按离光标的远近排序。 +- **打开文件时**:按历史命中次数,预取这个工作区最常用的作用域和符号。 +- **代际变化后**:对标为过期、而且最近用过的条目,重新提问一次。 +- **CPU 上限**:预取占用的引擎时间不超过空闲时间的一定比例;后台索引在跑时,预取让路。 + +### 4.9 失效与清理 + +- 代际不一致的条目不删除,降级成不确定(4.3)。 +- 每个条目有一个"最长不确定期",超过之后不再返回,只等刷新。 +- 文档关闭时,它的 K1 条目留在磁盘上,内存里的会被淘汰。 + +### 4.10 防污染 + +引擎在状态不好时给出的答案不写入缓存: +- 这个文件的模块加载失败,或者引擎状态是 degraded、doomed(沿用现有的 status 与 issue); +- 答案为空(空答案只说明"现在答不出"); +- 请求的快照已经落后当前代际不止一代,而且结果和已有的确定条目冲突。 + +后台续跑答案的快照如果与当前不同,也只写入它自己那个快照对应的键,并按自己的代际向量记账,不覆盖更新的条目。 + +### 4.11 来源记录 + +应答层内部的每一个结果,都带一条来源记录: + +``` +source engine | cache:K1 | cache:K2 | cache:K3 | outline | native +certainty authoritative | certain | uncertain +generation 条目写入时的代际向量摘要,与当前的差异 +age 条目年龄(ms) +latency 这次应答的耗时;引擎是否仍在后台计算 +``` + +各使用面怎么用这条记录,见第 6 节。 + +## 5. 各请求的策略 + +### 5.1 补全(`textDocument/completion`、`completionItem/resolve`) + +- **存储**:`label`、`kind`、`detail`、`labelDetails`、`sortText`、`filterText`、`insertText` 或 `textEdit` 的新文本、`data`。 +- **返回缓存结果时**: + - 用前缀做模糊过滤(规则与编辑器一致),保留原来的 `sortText`,并入关键字(F15); + - 用 `retarget` 把每一项的 `textEdit` 范围对准当前光标; + - 标 `isIncomplete: true`。 +- **用户看不见的安全措施**:不确定的项不预选(`preselect: false`),也不设 `commitCharacters`;按大纲判断已经删掉的项排到最后。 +- **`resolve`**:缓存项的文档注释,在 `resolve` 时向引擎取,用存下来的 `data`。引擎没有及时回答,就用上一次的注释,没有就不显示。 +- **刷新**:见 6.1。 + +### 5.2 签名帮助(`textDocument/signatureHelp`) + +- **K1**:(文档,所在声明,被调用者的文字);**K2**:(被调用函数的规范名,定义位置,指纹)。 +- **存储**:所有签名,以及 `activeSignature`。`activeParameter` 不存,按当前实参位置在文本上现算。 +- 不确定的结果照样返回。编辑器在用户输入逗号或括号时会重新请求,到时就是权威结果,原地更新。 + +### 5.3 悬停(`textDocument/hover`) + +- **K1**:(文档,所在声明,标识符,接收者或限定符);**K2**:(符号规范名,定义位置,指纹),由 `definition` 学到;**K3**:限定名,比如 `std::vector`。 +- **存储**:markdown 内容。`range` 不存,按当前标识符的位置现算。 +- 截止时间到时,有可用的缓存就返回。不确定的悬停在编辑器里不加标记(6.1);下次悬停到这里时,就已经是新结果了。 +- 涉及模板实参的悬停,比如 `std::vector` 在不同位置显示的实参不同:只使用 K1,或者在 K2 内容里只保留与实参无关的部分(声明、文档),实参相关的那一段等权威结果。具体规则在实现时依据引擎输出的格式确定。 + +### 5.4 跳转(`definition`、`declaration`、`typeDefinition`、`implementation`) + +跳错位置的代价高,所以策略更保守: + +- **确定**:K1 或 K2 命中,而且目标文件自记录以来代际没变 → 立即返回(≤ 500 ms)。 +- **目标文件变过**:用声明大纲在目标文件里找同名声明。 + - 只找到一处 → 返回新位置,算不确定; + - 找到多处,或者一处都没有 → 不用缓存,等引擎。 +- 其他情况都等引擎,沿用现有的 10 s 预算。 +- `implementation` 可能有多个目标,只在完全确定时才用缓存。 + +### 5.5 引用与调用关系(`references`、agent 的 `cxx_references`) + +- **本轮不缓存。** 结果大、分布在很多文件里,任何一个相关文件变了就是不确定,而查找引用本来就不是按键级别的交互。 +- agent 的 `cxx_references` 先通过悬停、跳转定位符号,这两步走应答层,结果的 `provenance` 照常标注(6.2);引用列表本身总是引擎的。 +- 以后要缓存时,键是 (符号规范名,定义位置,`includeDeclaration`、方向),编辑器里的"查找引用"仍不使用不确定的缓存。 + +### 5.6 不纳入的请求 + +语义高亮、诊断、文档高亮、文档符号、工作区符号、格式化、重命名:或者跟着 AST 走,或者已经由 native 层回答,或者必须完全正确(重命名、格式化)。 + +## 6. 各使用面的可见性 + +| 使用面 | 截止策略 | 来源的可见性 | +|---|---|---| +| 编辑器(LSP) | 0.45 s;可以用不确定的结果(跳转除外) | 默认几乎看不出来(6.1) | +| agent(MCP 工具) | 默认与编辑器相同,可以用缓存换速度;`mcppls.answer.agent: authoritative` 或单次调用的 `fresh: true` 时等权威结果 | **结果里一定带** `provenance`,其中 `authoritative` 字段直接说明是不是权威结果 | +| CLI 与 report | 不涉及交互截止 | 命中统计、来源分布、缓存体积,都明确输出 | +| 日志 | — | 来源记录:`info` 级只记值得注意的应答,`debug` 级记每一次(6.4) | + +请求从哪个使用面进来,由会话标记:LSP 客户端的请求,和 MCP 工具在内部发起的查询,走的是不同的入口。应答层按这个标记选择策略。 + +### 6.1 编辑器:以无感为核心 + +**刷新:不重弹列表** +- **R1 用户还在打字**:缓存结果标了 `isIncomplete`,编辑器每次按键都会重新请求。这时直接从已经更新的缓存回答,列表在打字过程中原地变成权威结果。 +- **R2 用户停住不动**:不做任何事,下一次按键或下一次打开时自然就是新结果。 +- **R3 唯一的主动刷新**:上一次在 `.`、`->`、`::` 之后只给出了空列表或少于 3 项,而且文档版本没变、光标没动、距上次请求不超过 5 s。这时 mcppls 发 `mcppls/completionReady {uri, version, position}`,VS Code 扩展执行一次 `editor.action.triggerSuggest`。对用户来说,这是列表第一次真正出现,和一个稍慢的补全没有区别。其他编辑器忽略这个通知。 +- **顺序稳定**:R1 原地更新时,已经显示过的项保持相对顺序;权威结果里新出现的项按权威排序插进去;不存在的项去掉。 +- 悬停和跳转没有刷新动作:悬停在下次悬停时就是新的;跳转只给确定的结果,或者经过大纲校验的结果。 + +**标记:默认看不出来**(设置 `mcppls.completion.provenance`,同时作用于补全和悬停) + +| 值 | 补全列表里 | 补全项详情区、悬停卡片 | +|---|---|---| +| `subtle`(默认) | 不确定的项,在 `labelDetails.description` 的末尾加一个细间隔和 `·`;确定的项不加 | 详情区最后一行小字:"来自上次的结果,正在确认";悬停卡片不加 | +| `off` | 不显示 | 不显示 | +| `verbose` | 每一项都标来源:缓存、权威或大纲 | 补全详情和悬停卡片末尾,都写出来源与代际(用于报问题) | + +**标记的是项,不是整个列表**。只有真正有疑问的项才带 `·`: +- 按大纲补进来的项; +- 按大纲判断已经不存在、被排到最后的项; +- 条目的提供者(定义该类型或作用域的文件)代际变了、又无法用大纲核对时,这个条目的项。 + +只因为用户在当前文件里继续编辑而变得不确定的条目,项本身并没有疑问:列表照常返回,标 `isIncomplete`、不预选,但**不加标记**。这样正常打字时几乎看不到 `·`。 + +### 6.2 agent:速度可选,来源一定明确 + +- **默认 `mcppls.answer.agent: cached`**:和编辑器一样,可以先用缓存,换取速度。 +- **每一个结果都带 `provenance` 对象**(4.11 的来源记录),其中: + - `authoritative`:`true` 表示是引擎在当前快照上算出的结果,`false` 表示来自缓存或大纲; + - `source`、`certainty`、`ageMs`、`generationChanged`:细节; + - 不确定时,另有 `notes`,用一句人能读懂的话说明,例如"成员列表来自 3 分钟前的结果,定义该类型的文件此后修改过"。 +- **要权威结果时**:设置 `mcppls.answer.agent: authoritative`,或者在单次调用里传 `fresh: true`。这时等权威结果,直到工具的截止时间;截止时间到了仍没有,就照常返回失败,不用缓存充数。这与 `cxx_diagnostics` 现有的 `fresh` 语义一致,`cxx_symbol`、`cxx_references` 也加上同名参数。 +- **跳转类的位置**:不确定的位置只有在 `cached` 模式下才会返回,并且 `authoritative: false`。 +- `cxx_review` 和 `cxx_verify` 这类结论依赖正确性的工具,始终只用权威结果,不受设置影响。 + +### 6.3 CLI 与 report + +- `cxxModules/report`、`mcppls report` 和诊断包里,新增"应答层"一节: + - 按请求类型列出命中率(K1/K2/K3/outline/native/engine); + - 响应 p50/p99; + - 收敛时间; + - 误项率; + - 后台续跑与预取的次数; + - 缓存体积。 +- `mcppls cache` 统计并清理工作区和全局两级缓存,规则沿用 C-8。 +- 诊断包默认只带统计,不带缓存内容(缓存里有项目的符号名)。只有显式加 `--include-answer-cache` 时才带,而且经过现有的脱敏处理。 + +### 6.4 日志 + +格式统一、方便检索,但保持克制: + +- **`info` 级只记值得注意的应答**: + - 返回了不确定的缓存结果; + - 权威结果到达后,和先给出的缓存结果不一样(写出差了几项); + - 权威结果迟于截止时间 3 倍以上。 +- **`debug` 级记每一次应答**,以及预取、去重、防污染丢弃、失效降级。 +- **进程启动时一行 `info`**:缓存条目数、体积、版本、读入耗时。 +- **按会话汇总**的命中率、响应时间等,在 report 里看(6.3),不刷日志。 + +``` +answer completion parser.cpp:52:13 member source=cache:K2 certainty=uncertain age=184s took=6ms engine=pending +answer completion parser.cpp:52:13 member source=engine certainty=authoritative took=2140ms replaced=cache:K2 diff=+1/-0 +answer hover app.ixx:88:20 source=cache:K1 certainty=certain took=3ms +answer definition parser.cpp:310:9 source=outline certainty=uncertain took=9ms target=i18n.ixx:41 +``` + +## 7. 引擎侧(mcppls-clangd,正确性前提下的效率) + +| 编号 | 内容 | 针对 | 预期 | 把握 | +|---|---|---|---|---| +| **E1** | 模块输入校验按代际记住结果:项目内的输入(包括 `-I` 引入的第三方树)校验一次之后,在下一次输入变化提示之前不再重复 stat。提示用 `workspace/didChangeWatchedFiles`(mcppls 本来就在监听),另加一条引擎专用通知,用来告诉它"某一代所有输入都没变" | 每次补全 0.67–1.0 s | **−0.7–1.0 s** | 高(直接测得) | +| **E2** | ForceLoadPreamble 只用于真的 import 了模块的文件。它的作用是让 Sema 在补全时读入外部声明,因为索引里没有模块的符号;不 import 模块的文件,preamble 的符号索引里都有。"import 了模块"按这次请求看到的为准:preamble 之后才加上的 import(文件里新打的,或者经由某个头文件带进来的)只为这次请求构建,索引里还没有,仍要读入;文件里有 import 行时直接判定,否则用请求本来就要做的模块扫描(整次补全只重放一次)。成员访问和限定名补全仍然需要读入外部声明,所以不能按补全位置区分 | 头文件为主、但开着模块支持的文件 | 每次补全 −0.15–0.3 s(测得:同一个头文件,开关模块支持前后相差这么多) | 高 | +| **E3** | ~~补全项的文档注释改到 `resolve` 时再取~~ **不做**:clangd 本来就不解析简要注释(`IncludeBriefComments=false`),完整注释是补全项的文档内容;推迟到 `resolve` 需要 clangd 先支持 `completionItem/resolve`,代价和收益都不在本轮 | — | — | — | +| **E4** | 交互请求优先:补全和签名帮助使用单独的并发槽,不和其他文件的 preamble 构建、AST 构建排同一个队 | 请求成串时、以及准备期间的排队 | 排队时间 | 高 | +| **E5** | 常驻 AST 的数量按内存分档:15 GiB 以上 8 个,31 GiB 以上 12 个(按可用于判断的总内存取整,16/32 GB 的机器报告的值略小于整数;之前是 6 个或默认 3 个)。clangd 的这个选项只在启动时生效,所以按内存而不是按打开的文件数定 | 打开的文件比常驻 AST 多时,悬停和跳转要重建 AST | 悬停、跳转从秒级降到毫秒级(待验证) | 中 | + +E1 的具体做法:新增引擎选项"项目输入的复查间隔"(默认 0,即每次都查,行为不变)。mcppls 设为 10 s。间隔内,项目输入的校验结论直接复用;收到 `didSave`,或者收到 `workspace/didChangeWatchedFiles` 且其中有校验过的输入文件时,立即作废。这样文件监听漏掉的磁盘变化,最多晚 10 s 被发现,这与 clang 自己"每个构建会话只校验一次"的选项是同一类取舍。 + +- 每个补丁都有开关,默认行为不变,直到测量证明收益;写法上不包含任何产品相关的内容,能直接提交上游。 +- E5 只是 mcppls 侧的用法调整,不改引擎。 + +## 8. 与现有设置、行为的关系 + +- `MCPPLS_COMPLETION_REFILTER=off` 保留,作用同 `cache=off`。 +- R-7 的预算继续存在:它变成"没有任何可用缓存时等引擎多久",补全和悬停默认仍然是 1 s 和 2 s,跳转仍然是 10 s。新的 0.45 s 截止时间只在有可用缓存时生效。 +- F9、F15、import 行补全、keywords 的合并都不变。 + +## 9. 指标与测试 + +### 9.1 指标 + +由 report 和 `cxxModules/report` 输出,bench 和 conformance 直接读取。 + +| 指标 | 定义 | 目标 | +|---|---|---| +| 响应 p50/p99 | 每类请求从收到到返回(稳定后) | 补全、签名、悬停 p99 ≤ 500 ms;确定的跳转 ≤ 500 ms | +| 命中率 | 按请求类型和来源统计 | 稳定后,补全的引擎加缓存命中 ≥ 95% | +| 收敛时间 | 从返回缓存结果,到同一个键的权威结果写入 | 大型模块文件 ≤ 3 s(E1、E2、E4 之后 ≤ 2 s) | +| 补全误项率 | 缓存结果里不在随后权威结果中的项的比例;**选中的项**单独统计 | 选中误项率 ≤ 0.1% | +| 补全漏项率 | 权威结果里有、缓存结果里没有,而用户最终输入的正是这个名字 | 记录并跟踪 | +| 悬停不一致率 | 缓存悬停与随后权威悬停内容不同的比例 | 记录并跟踪 | +| 跳转错误率 | 返回的缓存或大纲位置与权威位置不同的比例 | ≤ 0.1% | +| 主动刷新次数 | R3 触发次数占补全请求的比例 | 接近 0 | +| 不确定项占比 | 带 `·` 的项占全部返回项的比例 | 正常打字时接近 0 | +| 每个词的引擎请求数 | 去重效果 | ≤ 1 | +| 缓存体积与读入耗时 | 内存、工作区磁盘、全局磁盘;启动时读入 | 在预算之内;读入 ≤ 200 ms | + +### 9.2 测试 + +- **单元测试**: + - 键与指纹(别名展开、前缀、所在声明、局部上下文哈希); + - 代际向量的比较与确定性判定; + - 合成器在各请求类型下的分支选择; + - 顺序稳定的合并; + - `retarget` 与悬停 `range` 的重算; + - 声明大纲(成员、命名空间名、宏和条件编译时退出); + - 防污染规则; + - 持久化文件的版本处理与损坏恢复; + - 来源记录与日志格式。 +- **conformance**(mock 引擎可以设定延迟与答案): + - 补全:引擎 3 s 才答时响应 ≤ 500 ms 并收敛;打了又删;跨文件同一作用域;类型加一个成员、删一个成员;热启动第一次按键;R3 只在空结果时触发;后台续跑上限; + - 签名帮助:缓存结果立即返回,输入逗号后变成权威结果; + - 悬停:同一处、同一符号跨文件;定义文件改动后不用过期的悬停; + - 跳转:目标文件未变时立即返回;变了且大纲唯一命中时返回新行;有歧义时等引擎; + - agent:默认等权威结果;`fresh: false` 时结果里带 `provenance` 和 `notes`;`cxx_review` 只用权威结果; + - 防污染:模块加载失败期间的答案不进缓存; + - 不受信任的工作区不写磁盘;`cache=off` 与 0.0.14 的行为一致。 + - **所有现有 fixture 默认用独立的新缓存目录**,避免缓存让结果依赖运行顺序;缓存相关的 fixture 显式准备缓存。 +- **engine-bench**(Windows,大型模块项目):在现有的打字、悬停、跳转场景上,加入跨文件、打了又删、类型变化、热启动四类用例,逐项输出 9.1 的指标。 +- **VS Code e2e**:R1 原地更新;R3 只触发一次;默认档位下补全和悬停都没有可见标记;跳转不出现"跳了又跳"。 + +## 10. 设置与兼容 + +| 设置 | 取值 | 默认 | 说明 | +|---|---|---|---| +| `mcppls.answer.budget` | 100–2000 ms | 450 | 有可用缓存时,补全、签名帮助、悬停的截止时间 | +| `mcppls.answer.cache` | `on`、`memory`、`off` | `on` | `memory` 不写磁盘;`off` 回到 0.0.14 的行为 | +| `mcppls.answer.prefetch` | `on`、`off` | `on` | 4.8 | +| `mcppls.completion.provenance` | `subtle`、`off`、`verbose` | `subtle` | 6.1;同时作用于悬停 | +| `mcppls.answer.agent` | `cached`、`authoritative` | `cached` | 6.2;单次调用的 `fresh: true` 优先 | + +- **升级无感**:第一次运行新版本时,缓存从空开始积累;旧格式自动作废。没有新增必填设置。 +- 不认识 `completionReady` 的编辑器不受影响,只是少了 R3。 +- agent 工具新增的 `fresh` 参数默认 `true`,结果里新增 `provenance` 字段,都向后兼容。 +- 设置的中英文文档(`docs/30-settings.md` 与 `docs/zh-CN` 对应页面)、`docs/40-agents.md`、`docs/50-troubleshooting.md`(新的日志行怎么读),随实现一起更新。 + +## 11. 风险 + +| 风险 | 后果 | 处理 | +|---|---|---| +| 成员刚变,用户正好选了已删的成员 | 插入一个不存在的名字,几秒后诊断报错 | 大纲修正把它排到最后;不预选;单独统计选中误项率 | +| 成员刚加,缓存里没有 | 晚 1.5–3 s 出现 | 大纲修正补进来;R1 原地更新 | +| 局部变量刚加 | K1 缓存里没有 | 局部上下文哈希不一致时并入 `document_words` | +| 跳到旧位置 | 光标落错 | 只用确定的结果,或者大纲唯一命中的结果;其他情况等引擎;统计跳转错误率 | +| 悬停显示旧内容 | 一次看到旧签名或旧注释 | 定义文件代际变了、大纲里名字不在就不用;下次悬停即更新 | +| 同一文件在不同上下文里 | 成员集合不同 | 指纹里包含上下文;不同上下文之间只允许兼容命中,并算不确定 | +| 引擎在出问题时给出的答案被缓存 | 坏结果被反复使用 | 防污染(4.10) | +| 缓存占用内存、磁盘 | 资源压力 | 计入 C-8;LRU 和上限;`mcppls cache` 可见可清理 | +| 磁盘缓存里有项目的符号名 | 本机数据 | 只存在工作区缓存目录和用户缓存目录;不上传;诊断包默认不带;不受信任的工作区不写 | +| 后台续跑和预取抢占引擎 | 交互变慢 | 并发上限;交互优先(E4);打字时预取暂停 | +| 学到的符号映射错了 | 用了错误符号的结果 | 映射也带代际;权威结果到达时校验,不一致就删除映射 | +| 缓存让测试结果依赖运行顺序 | 结果不稳定 | fixture 默认用独立的新缓存目录 | +| `isIncomplete` 让编辑器每次按键都请求 | 请求数增加 | 缓存回答在 5 ms 以内;去重保证不放大到引擎 | + +## 12. 计划:一个版本完成(mcppls 0.0.15,mcppls-clangd 0.0.16) + +### 12.1 拆分原则(多视角) + +- **架构**:应答层是一个独立模块(`mcppls.orchestrator.answer`),对外只有"查缓存、合成、记录、学习、持久化"几个入口,不依赖 workspace 的内部状态,可以单独测试;workspace 只负责把请求、答案和代际事件交给它。声明大纲是另一个独立模块(`mcppls.orchestrator.outline`)。 +- **稳定性**:所有新行为都有开关;`answer.cache=off` 时,路由与 0.0.14 完全一致(A1、C-2 的单测和 conformance 照常通过)。后台续跑、预取、持久化都有上限。 +- **优雅简洁**:A1、C-2 只管同一个词,应答层管其余,职责不重叠;引擎侧每个补丁只做一件事。 +- **用户体验**:编辑器默认无感;agent 结果明确标注;日志克制。 +- **兼容性**:只用标准 LSP 和已有的自定义通知机制;新字段向后兼容;引擎的新选项默认关闭,旧引擎上 mcppls 照常工作(按能力声明决定是否传选项)。 +- **跨平台**:应答层与平台无关;持久化路径走已有的缓存目录 API;引擎补丁在三个平台的 CI 上构建和测试。 +- **一致性**:缓存预算、清理、报告并入 C-8、`mcppls cache`、report;设置走统一的设置表(文档由它生成和校验)。 +- **无感升级**:缓存格式带版本号,旧格式作废;没有新的必填设置;引擎升级沿用 payload 的版本锁定。 + +### 12.2 任务与依赖 + +| 编号 | 任务 | 依赖 | 可并行 | +|---|---|---|---| +| T1 | 应答层核心:上下文提取、三级键、确定性判定、存储(内存 LRU)、合成(补全、签名、悬停、跳转)、来源记录与日志格式 | — | 与 T2、T8 并行 | +| T2 | 声明大纲:类成员、命名空间里的名字与行号;拿不准就不出结果 | — | 与 T1 并行 | +| T3 | 持久化:工作区缓存与全局缓存的读写、版本、原子写入、预算与清理(接 C-8 和 `mcppls cache`) | T1 | 与 T4 并行 | +| T4 | workspace 接入:路由前查缓存、0.45 s 截止、引擎代理(去重、不随客户端取消、后台续跑与上限)、权威结果回写、防污染、代际服务、可见集合指纹、学习(`typeDefinition`/`definition`)、预取 | T1、T2 | — | +| T5 | 可见性:编辑器的标记三档与 R3(服务端通知 + VS Code 扩展);agent 的 `provenance` 与 `fresh`;report 与诊断包的统计 | T4 | 与 T6 并行 | +| T6 | 设置与文档:设置表、`docs/30-settings.md`(中英文)、`docs/40-agents.md`、`docs/50-troubleshooting.md`、CHANGELOG | T4、T5 的接口定下后 | 与 T5 并行 | +| T7 | 模块项目代码组织指南:`docs/25-module-projects.md` 与中文版 | — | 任何时候 | +| T8 | 引擎补丁 E1、E2、E4(mcppls-clangd 0.0.16):单测、lit、ledger、CI | — | 与 T1–T7 并行 | +| T9 | E5,以及 mcppls 按引擎能力声明传 E1 的选项 | T8 的能力名定下后 | — | +| T10 | 测试:T1–T3 的单元测试;conformance(引擎慢时的补全、打了又删、跨文件、类型变化、热启动、agent 的来源字段、`cache=off` 的回归);engine-bench 的新场景 | T1–T5 | — | +| T11 | 集成验证:产品 PR 的 CI 使用 T8 的候选构建;Windows 大型模块项目上的 engine-bench(临时验证 PR,摘要分文件) | T8、T10 | — | +| T12 | 发版:引擎先合入、发版,产品锁定 0.0.16 的 payload 后合入、发版,本地验证产物 | T11 | — | + +``` +T1 ─┬─▶ T3 ─┐ +T2 ─┴─▶ T4 ─┴─▶ T5 ─▶ T10 ─▶ T11 ─▶ T12 +T7(独立) T6(随 T4/T5) +T8 ─▶ T9 ───────────────────────▶ T11 +``` + +### 12.3 验收 + +- 第 9.1 节的指标,在 conformance 和 Windows 大型模块项目的 engine-bench 上都给出数字。 +- `answer.cache=off` 时,所有现有测试结果与 0.0.14 相同。 +- 引擎的新选项关闭时,引擎的所有现有测试结果与 0.0.15 相同。 + +## 13. 多视角自审 + +自审时发现的问题,已经改进到上文中的,标为"已改"。 + +**架构** +- 快和对分开,两侧各有独立的指标;应答层只依赖标准请求,换引擎不受影响。 +- 一套通用框架,加上按请求类型的策略表,没有为每种请求各写一套缓存。 +- 已改:可见性做成"每个结果都带来源记录(C5),各使用面决定显示多少",而不是只有编辑器有标记。这样 agent、CLI、日志共用同一份数据。 +- 已改:A1、C-2 保留在应答层前面、只管同一个词,应答层不重复它们的规则(4.5),避免两套规则互相干扰,也不必重写它们已有的测试。 + +**正确性** +- 权威结果总会覆盖缓存结果;跳转只用确定的结果或者大纲唯一命中的结果。 +- 已改:K1 原先只看所在声明,局部变量刚加时会漏,改成同时看局部上下文哈希,不一致时并入局部单词(4.3)。 +- 已改:增加防污染(4.10),模块加载失败期间的答案不进缓存。 +- 已改:缓存项的 `textEdit` 范围和悬停的 `range` 都按当前位置重算(5.1、5.3)。 +- 剩余风险:模板实参相关的悬停,在不同位置内容不同,只能保守处理(5.3),实现时需要依据引擎输出的格式细化。 + +**稳定性** +- 后台续跑和预取都有上限和让路规则;磁盘缓存原子写入、带版本号、损坏可以恢复。 +- 已改:测试默认使用独立的新缓存目录,避免缓存让 conformance 结果依赖运行顺序(9.2)。 + +**体验** +- 编辑器里默认没有可见变化,只是变快:不重弹列表,悬停和跳转没有刷新动作,标记只在细看时才能发现。 +- agent 默认拿权威结果,不会被缓存"悄悄"影响;需要速度时显式传 `fresh: false`,并且能看到来源。 + +**性能** +- 缓存应答在 5 ms 以内,所以 `isIncomplete` 带来的额外请求不放大到引擎。 +- 去重让引擎在打字期间的请求数降到每个词至多一个,权威结果本身也会因为排队变短而更快。 +- 启动时读入缓存要设上限(≤ 200 ms),超过就异步读入,先用内存里已有的。 + +**兼容性与无感升级** +- 不认识自定义通知的编辑器照常工作;`cache=off` 回到 0.0.14 的行为;agent 的新参数和新字段向后兼容;旧缓存格式自动作废。 + +**跨平台** +- 没有平台相关的逻辑。E1 在 Windows 上收益最大,在其他平台上也有收益。 + +**安全与隐私** +- 缓存只在本机;诊断包默认不带缓存内容;不受信任的工作区不写磁盘。 + +**一致性** +- 缓存预算、清理、报告并入现有的 C-8、`mcppls cache`、report 与诊断包;agent 的 `fresh` 沿用 `cxx_diagnostics` 的语义;日志沿用现有的级别约定。 + +**仍需在实现前确认的** +- 0.45 s、后台续跑上限(2 个、30 s)、R3 的阈值(少于 3 项、5 s 内)这些数值,用阶段 0 和阶段 1 的数据校准。 +- E5 的常驻 AST 上限与内存预算的关系。 +- K3 全局缓存的总量上限,以及按工具链分区的粒度。 diff --git a/.agents/docs/design.md b/.agents/docs/design.md index 8a758a13..281c51c1 100644 --- a/.agents/docs/design.md +++ b/.agents/docs/design.md @@ -52,6 +52,13 @@ editor / coding agent / CI — module-name navigation, `import` completion, the module graph, unresolved / ambiguous / cross-module-partition diagnostics — from a syntax index it owns. clangd answers C++ semantics from a database mcppls writes for it. A clice engine was considered and is not implemented. +- **An answer layer in front of clangd** (`src/orchestrator/answer`, `answering`): what clangd answered for a + completion, a hover, signature help or a jump is kept under the place, the receiver's learned type and the + qualifier's spelled-out name, with a certainty (the file's visibility fingerprint, the enclosing text, the other + files' changes, the provider's generation). When clangd is slower than `mcppls.answer.budget` a kept answer goes + out -- marked incomplete, doubted items with a small mark only -- and clangd's own answer replaces it; for agents + every response says where it came from (`provenance`). clangd stays the one source of correctness; the layer only + decides what an earlier correct answer is worth now. - **One process shape per client**: `mcppls serve` (LSP over stdio), `mcppls mcp` (MCP over stdio, or `--daemon` to share one workspace between several agents), and one-shot commands (`mcppls check`, `report`, `cache`, `symbol`, `review`, ...). @@ -395,5 +402,11 @@ close at once · C-2 late completions reach their word · C-4 slow files and bui followed without touching the project · X-3 the build compiler's own standard · X-4 a standard library unit that does not scan · I-1 include cleaner stays on · CL-1..CL-4 CLion 2026.2.3, one engine per file · Z-1..Z-3 Zed in CI. +**"answer-layer design" — [2026-10-13-answer-cache-layer-design.md](2026-10-13-answer-cache-layer-design.md).** 4 the +keys K1–K3, certainty (4.3 the visibility fingerprint), the budget, dedup and background (4.8 prefetch, 4.10 nothing +kept from a failing engine) · 6.1 marks in the editor · 6.2 provenance for agents and the command line · 6.4 logs · 7 +the engine side (E1 module input rechecks, E2 the preamble load, E4 interactive slots, E5 retained ASTs) · R3 the +`cxxModules/completionReady` notification. + **"tooling architecture".** 3.2 the workspace layout · 5.1 what mcpp, mcppls and devtools each do · 5.5 how devtools finds the server it just built · M0–M6 its migration steps. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a85f275..ca1eac39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,58 @@ release's notes are that section. Versions are three-part semantic versions, `MAJOR.MINOR.PATCH`, and every editor plugin carries the product version unchanged. +## [0.0.15] — 2026-10-13 + +The bundled engine is [mcppls-clangd 0.0.16](https://github.com/Sunrisepeak/mcppls-clangd/releases/tag/v0.0.16) +(`23.1.0-mcppls.4`): 35 patches, three of them new. This release is about answering at once what was answered before: +clangd stays the one source of correctness, and mcppls decides what an earlier correct answer is worth now. + +### Answers in under half a second + +- **What clangd answered before stands in while it is slow.** Completion, hover, signature help and go-to-definition + keep clangd's answers under the place they were for, the type of what is before the `.` (learned from clangd in the + background), and the name before the `::` with its aliases spelled out -- so `std::` asked in one file is answered in + another, and a list typed away and asked for again is there at once. When clangd is slower than + `mcppls.answer.budget` (450 ms), the kept answer goes out, marked incomplete, and clangd's own replaces it as you type + on; a request you cancel goes on in the background and is kept for the next time. Kept answers survive a restart + (`mcppls.answer.cache`: `on`, `memory`, `off`). While clangd is idle, mcppls asks it in advance for the places after + a `.`, `->` or `::` near the cursor that have no answer yet (`mcppls.answer.prefetch`). +- **You do not see it.** A list given so looks like clangd's own. Only an item mcppls doubts -- a member of a class + whose text changed since -- carries a small `·` after its description, and the members the class's text added or + removed are corrected in the list (`mcppls.completion.provenance`: `subtle`, `off`, `verbose`). A jump is given from + an earlier answer only while the file it lands in is as it was, or the declaration is found there again exactly once. +- **A list that came up empty comes back by itself.** When a completion after `.`, `->` or `::` had almost nothing in + time and clangd's answer arrives while you are still at the same place, VS Code shows the list without a keystroke + (`cxxModules/completionReady`, S3 5.9). +- **Agents and the command line are told which answers are which.** A symbol or references result that rests on a + kept answer says so in `provenance` (`authoritative`, the source, how old, whether the file that declares it + changed). `fresh: true` (MCP) and `--fresh` (`mcppls query symbol|refs|calls`) wait for clangd's own answers; + verify, impact and review always do; `mcppls.answer.agent: authoritative` makes every query wait. + +### The engine + +- **A check that found a module's inputs as they were holds for 10 s** while no file change is reported + (`--module-input-recheck`): an importer of 25 modules no longer re-checks every input at every completion, which cost + 0.7-1.0 s on Windows. A save, a change on disk the editor reports or an edit with dirty headers checks again at once. +- **A file that imports no module completes as without module support.** Sema no longer loads the preamble's + declarations the index already has (a header-heavy file: 0.6-0.7 s per completion to about 0.4 s); an import typed + since, or brought in by a header, is still loaded. +- **Completion and signature help have two slots of their own** (`--interactive-actions`), instead of queueing behind + other files' builds while modules are prepared. +- With 15 GiB or more, eight ASTs of files you switch between are kept; with 31 GiB or more, twelve. + +### Seeing what happens + +- The log says, at `info`, only the answers worth reading: one given from an earlier answer while something it depends + on changed, and clangd's answer when it differed from what went out (`diff=-/+`). The report's `answers` counts + how often, how fast and how often clangd's answer differed. See the troubleshooting guide. + +### Documentation + +- A new guide, [Laying out a module project](docs/25-module-projects.md): wrapper modules for heavy headers, each header + in one place, `import std` without standard `#include`s, a minimal global module fragment, bodies in implementation + units, no re-export chains, and why each makes a large mixed project faster to build and to complete in. + ## [0.0.14] — 2026-10-13 The bundled engine is [mcppls-clangd 0.0.15](https://github.com/Sunrisepeak/mcppls-clangd/releases/tag/v0.0.15) diff --git a/conformance/traceability.json b/conformance/traceability.json index 6ab0eeb4..ecdbb66e 100644 --- a/conformance/traceability.json +++ b/conformance/traceability.json @@ -2246,5 +2246,90 @@ "script": "editors/vscode/src/cacheHub.ts", "contains": "about?.cacheDevDrive === false" } + ], + "S3-5.9-1": [ + { + "test": "tests/test_answer_routing.cpp: a client that did not declare completionReady is not sent it" + } + ], + "S3-5.9-2": [ + { + "test": "tests/test_answer_routing.cpp: a slow engine's answer is kept, and given at the answer budget the next time" + }, + { + "test": "tests/test_answer_routing.cpp: once the person typed on, an answer is not called up again" + } + ], + "S3-5.9-3": [ + { + "script": "editors/vscode/src/completionGate.ts", + "contains": "export function completionStillWanted(" + } + ], + "S3-5.9-4": [ + { + "script": "editors/vscode/src/extension.ts", + "contains": "void vscode.commands.executeCommand('editor.action.triggerSuggest');" + } + ], + "S3-6.3-1": [ + { + "test": "tests/test_answer.cpp: a kept completion is filtered by the word, placed on it, and incomplete" + }, + { + "test": "tests/test_answer_routing.cpp: a slow engine's answer is kept, and given at the answer budget the next time" + } + ], + "S3-6.3-2": [ + { + "test": "tests/test_answer.cpp: hover is placed on the word; signature help on the argument; jumps only when certain" + }, + { + "test": "tests/test_answer_routing.cpp: a hover and a jump are kept; a jump only while the file it lands in is as it was" + } + ], + "S3-6.3-3": [ + { + "script": "src/orchestrator/workspace.cpp", + "contains": "return !status.failed && status.state != \"unavailable\";" + } + ], + "S3-6.3-4": [ + { + "test": "tests/test_answer.cpp: a class that changed: members added and removed by its outline, marked; only those" + } + ], + "S5-2.6-1": [ + { + "test": "tests/test_answer_routing.cpp: an agent is told where every answer came from" + }, + { + "script": "src/ai/mcp/tools.cpp", + "contains": "result.value[\"provenance\"] = std::move(provenance);" + } + ], + "S5-2.6-2": [ + { + "test": "tests/test_answer_routing.cpp: an agent is told where every answer came from" + }, + { + "script": "src/cli/query.cpp", + "contains": "view.require_authoritative(args.is_flag_set(\"fresh\"));" + } + ], + "S5-2.6-3": [ + { + "script": "src/ai/mcp/tools.cpp", + "contains": "name == \"cxx_review\" || name == \"cxx_verify\" || name == \"cxx_impact\"" + }, + { + "script": "src/cli/query.cpp", + "contains": "view.require_authoritative(true);" + } + ], + "S5-2.6-4": [ + { + "test": "tests/test_answer_routing.cpp: with mcppls.answer.agent authoritative, an agent always waits for the engine" + } ] } diff --git a/docs/20-projects.md b/docs/20-projects.md index fedcb9d9..96a5832f 100644 --- a/docs/20-projects.md +++ b/docs/20-projects.md @@ -225,3 +225,6 @@ else. The semantic kit provides the semantics and the status says why. | "needs a download" | Planning offline stopped at something not on the machine; the project is served from its sources meanwhile, and there are actions to fix it (or build in your terminal: it upgrades by itself) | | "files the build generates … do not exist yet" | A rule's output (a Qt form's header, say) is not built yet; build once and the files that include it get their semantics | | "implementation unit(s) cannot be read" | An implementation unit does not build (a missing header, usually), so go-to-definition cannot reach the definitions in it | + +How to lay out a module project so it builds and completes fast — wrapper modules, each header in +one place, `import std` without standard `#include`s — is in [25-module-projects.md](25-module-projects.md). diff --git a/docs/25-module-projects.md b/docs/25-module-projects.md new file mode 100644 index 00000000..6205b4e8 --- /dev/null +++ b/docs/25-module-projects.md @@ -0,0 +1,199 @@ +# Laying out a module project + +**English** | [简体中文](zh-CN/25-module-projects.md) + +Everything here builds without it. These are the layouts that keep a large project — modules, +headers and third-party code mixed — fast to build, fast to complete in, and easy to keep +consistent. Each comes with the reason, so you can tell when it does not apply. + +## What a module costs, and when + +A module interface is compiled once into a BMI (a binary module interface). A file that imports it +loads the BMI instead of parsing the source again. Two costs follow from that: + +- **Building.** Compiling against a BMI is much cheaper than parsing the headers it was made from: + in a mid-sized project we measured, a unit compiled about four times faster against a wrapper + module than against the same headers included directly. But an edit to an interface rebuilds its + BMI, and the BMIs of everything that imports it, directly or not. +- **Editing.** A completion, a hover or a jump in a file that imports modules loads the BMIs of the + file's whole import closure for that request. The fewer and smaller those BMIs, the faster the + answer. A closure with each heavy header in one place loads each declaration once; a closure where + ten modules carry their own copy of the same header loads ten copies, and the compiler merges them + on every request. + +The rules below follow from these two costs. + +## 1. Each heavy header in exactly one wrapper module + +Put each large third-party header (a JSON or TOML library, a GUI toolkit, `vulkan.hpp`) in the +global module fragment of one module of its own, and export the names your code uses: + +```cpp +// src/third_party/toml.cppm +module; +#include +export module third_party.toml; + +export namespace toml { + using toml::value; + using toml::parse; + using toml::find; +} +``` + +Every other module imports `third_party.toml` and never includes `` itself. + +**Why.** A header included in the global module fragment of a module becomes part of that module's +BMI. Included in ten modules, it is in ten BMIs, and a file importing several of them loads every +copy and merges the duplicates — on every build of that file and on every completion in it. In one +wrapper it is parsed once, stored once, loaded once. + +## 2. Do not mix `#include` of the standard library with `import std` + +In a unit that imports `std`, include no standard header: not directly, and not through a header +that includes one. A third-party header that needs the standard library goes into its own wrapper +(rule 1), where its `#include ` stays in the wrapper's global module fragment. + +```cpp +// avoid: the same declarations from two places +module; +#include +#include "my_lib.hpp" // includes , +export module app.core; +import std; +``` + +```cpp +// prefer +export module app.core; +import std; +import third_party.my_lib; // the wrapper includes my_lib.hpp, and with it and +``` + +**Why.** `import std` brings the standard library as one module. An `#include ` in the same +unit brings a textual copy of the same declarations. The compiler has to prove each pair the same +and merge it, which is slow and, with today's compilers, the most common source of "redefinition" +and "ambiguous" errors in mixed projects. + +## 3. Keep the global module fragment to what the interface needs + +The global module fragment of an interface should hold only the headers its exported declarations +mention. Headers that only the implementation needs go into an implementation unit: + +```cpp +// app/net.cppm -- the interface +export module app.net; +import std; +export namespace app::net { + struct Response { int status; std::string body; }; + Response get(std::string_view url); +} +``` + +```cpp +// app/net.cpp -- the implementation +module; +#include // only here: importers never load it +module app.net; +namespace app::net { + Response get(std::string_view url) { /* ... */ } +} +``` + +**Why.** What an interface includes is in its BMI, and every importer loads it. What an +implementation unit includes costs only that unit. An edit to an implementation unit does not change +the BMI, so nothing that imports the module is rebuilt. + +## 4. Put function bodies in implementation units + +In a module interface, a member function defined inside its class is not implicitly `inline` as it +is in a header, and changing any body in an interface unit changes the interface. Keep bodies that change often in `module X;` implementation units, +and leave in the interface what importers need to see: declarations, templates, `constexpr` +functions, small inline helpers. + +**Why.** Every edit to an interface unit rebuilds its BMI, and with it the BMIs of everything that +imports it. A body edited in an implementation unit rebuilds that one unit. + +## 5. Import what you use; avoid re-export chains + +`export import` is for a module that is a facade over its partitions, or for a type that is part of +the module's own interface. Do not use it to pass on what a module happens to use: + +```cpp +// avoid +export module app.defines; +export import std; // every importer of app.defines now imports std through it +export import third_party.toml; + +export module app.tool; +export import app.defines; // ...and every importer of app.tool, everything above +``` + +```cpp +// prefer +export module app.tool; +import std; +import third_party.toml; +import app.defines; // exports only its own names +``` + +**Why.** An `export import` puts the whole re-exported closure into every importer's visible scope: +more BMIs loaded for each request, more names in every completion list, and a dependency the +importer never stated, so removing it from the middle of the chain breaks files far away. Never +`export import std`. + +## 6. Split large modules along the lines they change + +A module whose interface is thousands of lines, and that most of the project imports, is rebuilt +— with all its importers — on every edit to any part of it. Split it into separate modules (or +partitions behind a facade) along the lines that change together: a stable core of types, and the +parts that are worked on. + +**Why.** The rebuild after an edit is the size of what imports the edited interface. Small, stable +modules at the bottom of the graph and frequently edited ones near the top keep that small. + +## 7. Platform headers get their own wrapper + +```cpp +// src/platform/win32.cppm +module; +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +export module platform.win32; + +export namespace win32 { + using ::HANDLE; + using ::CreateFileW; + using ::CloseHandle; +} +``` + +**Why.** `windows.h` and the POSIX headers are large and define macros (`min`, `max`, `ERROR`) that +collide with ordinary code. Macros do not cross a module boundary, so wrapped, they stay inside the +wrapper. If other code needs a macro, put it in a small header of its own that defines only that. + +## 8. Names follow directories + +Name a module after its directory (`app/net/` holds `app.net`), give each directory one primary +interface, and use partitions (`app.net:http`) for the parts nobody outside imports. A build tool, +mcppls and a person can then find a module's source from its name. + +## Header-only code in a module project + +Files that import no module (headers, and units that only include) work as before: the engine +completes in them from its index, as without module support, so they do not pay for the project's +modules. A project with no module at all runs the engine without module support. The rules above +matter for the files that import. + +## Seeing what it costs + +- The report's `slowestFiles` lists the ten files with the slowest completion, and + `engines[].details.buildTimes` where each file's build went: preamble, imported modules, AST + builds. +- The log line `Prerequisites of : modules, reused, built …` says how large a + file's import closure is and what it cost ([50-troubleshooting.md](50-troubleshooting.md)). +- `Rebuilding module : ` names the edit that caused a rebuild. + +A file whose closure is dozens of modules, or a module that is rebuilt whenever anything changes, is +where these rules pay off first. diff --git a/docs/30-settings.md b/docs/30-settings.md index 97edf763..5d688561 100644 --- a/docs/30-settings.md +++ b/docs/30-settings.md @@ -67,6 +67,10 @@ either wrapped in a top-level `mcppls` object or not. | `mcppls.semanticTokens.modules` | `true`, `false` | `true` | — | restart | Color `import`, `module`, `export` and module names from the server's semantic tokens. Off: only the grammar's colors. | | `mcppls.semanticTokens.moduleType` | `true`, `false` | `false` | — | restart | A client declares it knows the custom `module` semantic token type and the `partition` modifier; off for every client but this one, since none else advertises it. Not a package.json setting: VS Code's own extension always declares it, fixed, because it contributes that token type itself. | | `mcppls.completion.triggerOnSpace` | `true`, `false` | `true` | — | restart | Show the module list as soon as a space is typed after `import` or `export import`. A space anywhere else never reaches the server. A client that says nothing gets this only when it identifies itself as VS Code or a fork of it; every other client opts in with `initializationOptions.completion.triggerOnSpace: true`. | +| `mcppls.answer.cache` | `on`, `memory`, `off` | `on` | `--answer-cache` | restart | Keep the engine's answers to completion, signature help, hover and the jumps, and give a kept one at once when the engine is slower than `answer.budget`; the engine's own answer replaces it when it comes. `on` also keeps them between sessions, `memory` only while mcppls runs, `off` answers as 0.0.14 did. | +| `mcppls.answer.budget` | ? | ? | `--answer-budget` | restart | Milliseconds (100-2000) a completion, signature help or hover waits for the engine before a kept answer is given instead. Only where one is kept; otherwise the request waits as before. | +| `mcppls.answer.prefetch` | `true`, `false` | `true` | — | restart | While the engine is idle, ask it for the completions after the `.`, `->` and `::` the open file already has, so the next one is kept before it is typed. One at a time, and never while a person types. | +| `mcppls.completion.provenance` | `subtle`, `off`, `verbose` | `subtle` | — | restart | How an answer given from what was kept shows it: `subtle` puts a faint `·` after the items that are in doubt (a member added or removed since) and nothing else; `off` shows nothing; `verbose` says where every item and hover came from, for a report. | | `mcppls.format.fallbackStyle` | a string | `auto` | `--format-fallback-style` | restart | Formatting fallback when no project .clang-format exists. auto selects the pinned mcpp style for an mcpp project when the bundled engine supports it; otherwise clangd's default. A named style overrides auto; mcpp opts other projects in and requires engine support. Project .clang-format always wins. | | `mcppls.index.primeImplementationUnits` | `auto`, `off` | `auto` | `--prime-implementation-units` | restart | Build a module's implementation units in clangd in the background, a few at a time, so go-to-definition reaches a definition that only an implementation unit has, before that file was ever opened. clangd's own background index cannot see a module unit's imports (WA-CLANGD-008). `off`: only the units a definition request searches, and the files you open, are indexed for this. | | `mcppls.enable` | `true`, `false` | `true` | — | immediately | Start mcppls for this workspace. `false` in a workspace's settings keeps the extension installed but inactive there -- for a project it cannot serve yet -- without uninstalling it; the status bar item turns it back on. | @@ -81,10 +85,11 @@ either wrapped in a top-level `mcppls` object or not. | `mcppls.trace.server` | `off`, `messages`, `verbose` | `off` | — | immediately | Log the LSP traffic to the C++ Modules output channel (at Trace level); `verbose` adds the server's debug log (at Debug level, by also passing `--log-level debug`). Set the channel's own log level to see them. | | `MCPPLS_LOG_LEVEL` | `debug`, `info`, `warning`, `error` | *(empty)* | — | restart | Overrides the log level the VS Code extension starts the server with, ahead of `trace.server`. | -### AI review +### AI review and agents | Setting | Values | Default | Command line | Applies | What it does | |---|---|---|---|---|---| +| `mcppls.answer.agent` | `cached`, `authoritative` | `cached` | — | restart | What the MCP tools may answer with: `cached` lets a kept answer stand in when the engine is slower, and says so in the result's `provenance` (`authoritative: false`); `authoritative` waits for the engine. A tool call's own `fresh: true` waits either way. | | `mcppls.ai.enabled` | `true`, `false` | `false` | — | immediately | Show the AI-era features: Review Changes reviews the workspace's changes against `HEAD` with mcppls's rules and shows the findings, with their evidence, as problems. Nothing is sent to a model unless this is on and a model source is separately configured. | ### Paths diff --git a/docs/40-agents.md b/docs/40-agents.md index eece837c..06a2eeee 100644 --- a/docs/40-agents.md +++ b/docs/40-agents.md @@ -26,6 +26,22 @@ Two things make these worth using over grepping: - **They answer from the same model the editor uses**, so an agent and the human are looking at the same build. +**Fast answers, and which ones they are.** A symbol lookup or a references search rests on the +compiler's answers (hovers, jumps). While the compiler is slow to give one, an answer it gave +earlier for the same question may stand in, so a lookup does not wait seconds. A result that rests on +such an answer says so in `provenance`: + +```json +"provenance": { "authoritative": false, "answers": 3, + "notAuthoritative": [ { "source": "cache:K1", "certainty": "uncertain", "ageMs": 41000, + "note": "The location comes from an answer kept 41 s ago; files changed since, so it may be out of date." } ] } +``` + +`authoritative: true` means every answer was the compiler's own, for this question. Pass `fresh: true` +(MCP) or `--fresh` (command line) to wait for the compiler's own answers; `cxx_verify`, +`cxx_impact` and `cxx_review` always do. To make every query wait, set `mcppls.answer.agent` to +`authoritative` ([30-settings.md](30-settings.md)). + **Model-backed review** is optional and off by default. With it off, review runs deterministic rules only and the server makes no network calls at all. Wiring, and the shared workspace daemon, are in [editors/agents/README.md](../editors/agents/README.md). diff --git a/docs/50-troubleshooting.md b/docs/50-troubleshooting.md index e13cbe70..dcc3ea0a 100644 --- a/docs/50-troubleshooting.md +++ b/docs/50-troubleshooting.md @@ -42,6 +42,10 @@ lines have a fixed shape; the report's `events` carry the same facts. | `clangd (): I[…] Module memos: BMI verdicts and scans kept from the last session …` | mcppls-clangd 0.0.15, at start: what the last session left to check instead of redoing it (each is checked before it is used) | | `clangd (): I[…] Module inputs: managed package store(s) checked by their markers` | Which package stores (mcpp's, xlings') the engine checks by version | | `the last files in use (): of them, opened in clangd ahead of the editor once it takes files` | At start: the files last worked in are opened in clangd before the editor asks; ` was ready in clangd ms before the editor opened it` says it paid off, `... is still being built there; the editor takes it as it is` that the editor came while clangd was at it, and one the editor did not open within two minutes is closed again | +| `clangd (): I[…] Module inputs: a check holds for s while no file change is reported` | mcppls-clangd 0.0.16, at start: a check of a module's inputs holds that long while no file change is reported; ` checks still holding` in the completion line counts the checks it saved | +| `answer :: source= certainty=uncertain age= took= engine=pending` | 0.0.15: an answer given from an earlier one while clangd was slower than `mcppls.answer.budget`, and something it depends on changed since; the ones nothing changed under are at `debug` | +| `answer :: source=engine certainty=authoritative took= replaced= diff=-/+` | clangd's own answer came after one was given, and differed by that many items (it replaces it on the next keystroke); none is written when they were the same | +| `answers: kept from the last session ( KB), read in ms` | At start: what the answer layer read back | | `the cache is not on a Dev Drive: …` | Windows only, once: see [Windows](#windows-a-cache-on-a-dev-drive) | | `restarting clangd (): ()` | Every restart, and whether it counts against clangd | | `the timers of took ms, ms of them in ` | What held the event loop | @@ -128,6 +132,16 @@ in the code, so it is told where it is, as diagnostics: the status stays *ready* `modules-doomed`, category `code`) and is never *preparing* for good. The engine's `doomedModules` and `filesRoutedToOwnEngine` in the report list them. +**A completion list shows at once, then changes a little; an item ends in a small `·`.** From 0.0.15, +when clangd is slower than `mcppls.answer.budget` (450 ms), mcppls gives the answer clangd gave +before for the same place or the same name, and clangd's own answer replaces it as you type on. A +`·` after an item's description marks one mcppls doubts: a member of a class whose text changed +since. `answers` in the report says how often it happened (`answers`), how fast (`fromCache`, +`fromEngine`) and how often clangd's answer differed (`replaced`). To see where every answer came +from, set `mcppls.completion.provenance` to `verbose`; to keep nothing, set `mcppls.answer.cache` +to `off` (or `memory` to keep nothing on disk). A jump is given from an earlier answer only while the +file it lands in is as it was, or the declaration is found there again exactly once. + **What the status says, and what it does not.** A mistake in your code (a missing `;`, an import of a module nothing provides, a module that does not compile) is a diagnostic where it is, in the Problems list; the status stays *ready*. *degraded* means the server lost something it would diff --git a/docs/90-architecture.md b/docs/90-architecture.md index ac30568a..4dc68c2d 100644 --- a/docs/90-architecture.md +++ b/docs/90-architecture.md @@ -29,6 +29,13 @@ editor / coding agent / CI words of the file nearest the cursor, as an incomplete list so the editor asks again; for hover while modules are prepared, a line saying so — and cancels clangd's request. The report counts, per method, which engine answered (`requests..answeredBy`). +- **What clangd answered before stands in while it is slow.** Completion, hover, signature help and + jumps keep clangd's answers under the place, the type and the name they were for. When clangd is + slower than `mcppls.answer.budget` (450 ms) the kept answer goes out, and clangd's own replaces it + as you type on; nothing in the list says so except a small `·` on an item mcppls doubts. A jump is + given so only while the file it lands in is as it was. Agents and the command line are told which + answers were not clangd's own, and can wait for them ([40-agents.md](40-agents.md)). clangd stays + the one source of correctness. - **Background work happens in idle time.** Implementation units are built for clangd's index one at a time, and only after 10 s without typing, opening a file or a request; a restart clangd needs for a changed build description waits until typing has paused for 3 s (at most 60 s). A crash or a diff --git a/docs/README.md b/docs/README.md index 168548f1..a9f03143 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ | [00-install.md](00-install.md) | Getting it, and what a payload is | | [10-editors.md](10-editors.md) | VS Code, Claude Code, Copilot CLI, any other LSP or MCP client | | [20-projects.md](20-projects.md) | What happens for mcpp, CMake, `compile_commands.json`, and for none of those | +| [25-module-projects.md](25-module-projects.md) | Laying out a module project: wrapper modules, one place per header, `import std` alone, and why | | [30-settings.md](30-settings.md) | Every setting, command and command-line option | | [40-agents.md](40-agents.md) | The semantic queries, verification and review for agents and CI | | [50-troubleshooting.md](50-troubleshooting.md) | The diagnostic report, and what each symptom usually means | diff --git a/docs/specs/CHANGELOG.md b/docs/specs/CHANGELOG.md index 4d45e5f9..912e70c6 100644 --- a/docs/specs/CHANGELOG.md +++ b/docs/specs/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2026-10-13 — S3 and S5: answers given before the engine's, and where they came from + +S3 6.3 describes answers a server gives from what its engine answered before -- a completion, a hover, signature help, +a jump -- when the engine is slow to answer: a completion list given so is `isIncomplete` (S3-6.3-1), a jump only while +the file it lands in is as it was or its declaration is found there again exactly once (S3-6.3-2), nothing is kept from +an engine that was failing (S3-6.3-3), and only the items a server doubts carry a small mark (S3-6.3-4). A new +notification `cxxModules/completionReady` (5.9), for a client that declared `completionReady`, says once that a +completion that went out nearly empty has its answer now (S3-5.9-1 to S3-5.9-4). S5 2.6 adds `provenance` to results +that rest on such answers (S5-2.6-1), `fresh` / `--fresh` for the engine's own answers only (S5-2.6-2), and holds +verification, impact and review to the engine's own answers (S5-2.6-3). All additive: protocol version stays 1. + ## 2026-10-12 — S3: whether a Windows cache is on a Dev Drive `about` may carry `cacheDevDrive` on Windows (S3-4-34): whether the server's cache directory is on a Dev Drive, where diff --git a/docs/specs/s3-lsp-extensions.md b/docs/specs/s3-lsp-extensions.md index a46596f8..913f3194 100644 --- a/docs/specs/s3-lsp-extensions.md +++ b/docs/specs/s3-lsp-extensions.md @@ -15,7 +15,7 @@ This specification defines the messages a C++ language server and an editor exch ## 1. Conventions - The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY** and **OPTIONAL** are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals. -- Structures are written as TypeScript interfaces, as in the LSP specification. `DocumentUri`, `Range`, `Command`, `TextDocumentIdentifier` and `TextDocumentPositionParams` are the LSP 3.18 types of those names. +- Structures are written as TypeScript interfaces, as in the LSP specification. `DocumentUri`, `Position`, `Range`, `Command`, `TextDocumentIdentifier` and `TextDocumentPositionParams` are the LSP 3.18 types of those names. - "Set", "context" and "role" have the meanings of [S1](s1-build-database.md). A context is the set the server uses for a file; it determines the file's options and the modules visible to it. ## 2. Principles @@ -35,6 +35,7 @@ interface ClientCxxModulesCapabilities { status?: boolean; // the client presents cxxModules/status graph?: boolean; // the client can present a module graph contexts?: boolean; // the client offers a context selection + completionReady?: boolean; // the client asks for completion again on cxxModules/completionReady (5.9) }; } @@ -356,6 +357,20 @@ interface SweepCacheResult { A server that advertises the command **MUST NOT** stop, restart or interrupt any engine for a sweep's sake. S3-5.8-1 A sweep **MUST NOT** let a request any engine owed fail. S3-5.8-2 A server **MUST NOT** remove a published BMI -- a `.pcm` whose name is not the versioned copy shape. S3-5.8-3 A server **MUST NOT** remove a file a live engine generation could hold mapped: a copy is swept only when its mtime is older than the start of the oldest live generation of the engines using that cache, or when no engine uses the cache at all. S3-5.8-4 A server **MUST NOT** sweep a workspace's cache that another instance has open: an instance directory whose own heartbeat is fresh belongs to a live instance, whatever the workspace's lease says. S3-5.8-5 The `staleCommands` category -- the command directories older units' BMIs occupy -- is the engine start path's own work (C-2), so a server **MUST** skip it unless the client asked for it by name and no engine is live in that root. S3-5.8-6 A server **MUST** run one sweep at a time, and answer a sweep that arrives while one runs with `alreadyRunning: true` and nothing removed by it. S3-5.8-7 With `dryRun: true` a server **MUST** compute the answer over exactly the set it would have removed, so what a client reports as "would free" is what a sweep would free. S3-5.8-8 What a sweep counts freed is what left the disk: an entry a removal could not take is counted in `failed`, never in `freedBytes`. S3-5.8-9 A server **MUST NOT** run the removals on the loop that serves requests and renews leases: the sweep of a grown cache is minutes of filesystem work, the reply arrives when the work is done, and nothing a client asked before it -- the lease's own renewal included -- waits behind it. S3-5.8-10 With more than one root swept, the numbers are the sum over those roots and `roots` says how many there were; `alreadyRunning` appears only when every swept root had a sweep in flight. S3-5.8-11 A `dryRun` sweep also changes nothing the server remembers: no `lastSweep`, no recomputed report -- a preview a client showed must not later read as a sweep that happened. S3-5.8-12 +### 5.9 `cxxModules/completionReady` notification + +Direction: server → client. A completion asked after a `.`, `->` or `::` while the semantic engine is still preparing a file's modules can get nothing in time; the engine's answer arrives a moment later, when the person is still looking at the same place. This notification says so, once, so a client that wants to can bring the list up without a keystroke. + +```ts +interface CompletionReadyParams { + uri: DocumentUri; + version: number; // the document version the completion was asked at + position: Position; // where it was asked +} +``` + +A server **MUST NOT** send `cxxModules/completionReady` to a client that did not declare `completionReady: true`. S3-5.9-1 A server **MUST** send it at most once per completion request, and only when what it answered that request with was nearly empty -- no earlier answer, or the file's own words only -- the engine's answer that arrived since is not empty, the document is still at `version`, and the request was asked at most 5 seconds before. S3-5.9-2 A client **MUST** treat it as a hint: it asks for completion again only when the document is still at `version` and the cursor is still at `position`, and otherwise drops it. S3-5.9-3 A client **MUST NOT** insert or change text on its account. S3-5.9-4 + ## 6. Module features through standard LSP | Feature | Standard message | Answered by | @@ -404,6 +419,12 @@ interface CxxModulesInitializationOptions { A semantic engine may offer some module-syntax keywords, not their combined forms, and nothing while it cannot answer for a file. Where a module declaration or an import declaration can begin, a server **SHOULD** offer the keywords that can appear there — `import`; `export import` in a module interface unit; `module;` before anything else in the file; `export module` and `module` in a file with no module declaration; `module :private;` in a primary module interface unit without one — merged with the semantic engine's result without duplicate labels, and **SHOULD** still offer them when the semantic engine gives no result in time. S3-6.2-4, S3-6.2-5 No module name is offered after `export module`: the name is being declared, not referred to. +### 6.3 Answers given before the engine's + +A semantic engine can take seconds to answer while it prepares what a file imports, and most of what is asked has been asked before: the same members after the same `.`, the same names after the same `std::`. A server **MAY** answer `textDocument/completion`, `textDocument/hover`, `textDocument/signatureHelp` and the jumps (`definition`, `declaration`, `typeDefinition`, `implementation`) from an answer its engine gave earlier, when the engine has not answered within a short budget, and keep the engine's own answer, when it arrives, in its place. + +A server that answers a completion from an earlier answer **MUST** mark the list `isIncomplete: true`, so that the client asks again as the person types and the engine's own answer replaces it. S3-6.3-1 A server **MUST NOT** answer a jump from an earlier answer unless the file it lands in is as it was when that answer was given, or the declaration it named is found again, exactly once, in that file's current text. S3-6.3-2 A server **MUST NOT** keep an answer its engine gave while the engine was failing or unavailable. S3-6.3-3 In a list given from an earlier answer, an item the server doubts -- a member of a class whose text changed since -- **SHOULD** be told by a small mark only (`·` at the end of `labelDetails.description`) and a line in its documentation, with nothing on the items it does not doubt, so that the list looks as the engine's own does. S3-6.3-4 To a client that asks on behalf of an agent (S5 2.6) the server says where each answer came from instead. + ## 7. Versioning The protocol version is an integer. Version 1 is defined by this document. A later version adds optional fields and new messages only; a change that is not backward compatible requires a new method prefix. diff --git a/docs/specs/s5-semantic-query.md b/docs/specs/s5-semantic-query.md index fddf3287..1ebe12f5 100644 --- a/docs/specs/s5-semantic-query.md +++ b/docs/specs/s5-semantic-query.md @@ -78,6 +78,33 @@ interface Failure { - A query that names more than one symbol where one is needed **MUST** fail with `ambiguous` and list the candidates with their identifiers. S5-2.5-1 - A query that needs the core engine in a workspace without one **MUST** fail with `unavailable`, not return an empty result. S5-2.5-2 +### 2.6 Provenance + +A server may answer what the core engine is slow to answer -- a hover, a jump -- from an answer the engine gave before (S3 6.3). A result built from such answers says so: + +```ts +interface Provenance { + authoritative: boolean; // every answer it rests on was the core engine's own, given for this request + answers: number; // how many engine answers the result rests on + notAuthoritative?: Answer[]; // those that were not, at most 8 +} +interface Answer { + authoritative: false; + source: "cache:K1" | "cache:K2" | "cache:K3" | "outline" | "native"; + // kept for this place; for this type; for this name; read from the text; the server's own + certainty: "certain" | "uncertain"; // certain: nothing the answer depends on changed since it was given + ageMs: number; // how long ago the engine gave it + providerChanged: boolean; // the file that declares what it is about changed since + enginePending: boolean; // the engine's own answer was still being computed + note?: string; // the same, in a sentence +} +``` + +- A result that rests on an answer that was not the core engine's own **MUST** carry `provenance` with `authoritative: false`; one that rests only on the engine's own answers **MAY** carry it with `authoritative: true`. S5-2.6-1 +- A query given `fresh: true` (MCP) or `--fresh` (command line) **MUST** rest only on the core engine's own answers. S5-2.6-2 +- Verification, impact and review (3.7, 5.2) **MUST** rest only on the core engine's own answers, whatever they are given. S5-2.6-3 +- A server **SHOULD** let a person require the engine's own answers for every query (`mcppls.answer.agent`: `authoritative`). S5-2.6-4 + ## 3. Queries ### 3.1 Symbols @@ -406,8 +433,8 @@ A server runs as an MCP server over standard input and output (`mcppls mcp`): on | Tool | Query | |---|---| -| `cxx_symbol` | 3.1 | -| `cxx_references` | 3.2, and 3.3 with `direction` | +| `cxx_symbol` | 3.1; `fresh: true` for the engine's own answers only (2.6) | +| `cxx_references` | 3.2, and 3.3 with `direction`; `fresh` as `cxx_symbol` | | `cxx_outline` | 3.4 | | `cxx_module` | 3.5 with 4.2 as `interface` (unless `interface: false`); `graph: true` for the graph | | `cxx_build_context` | 4.1 | @@ -436,9 +463,9 @@ One process per workspace can serve every MCP connection to it from one session | Command | Query | |---|---| -| `mcppls query symbol [--at FILE:LINE:COLUMN] [--id ID] [--kind K] [--module M]` | 3.1 | -| `mcppls query refs [--no-declaration]` | 3.2 | -| `mcppls query calls [--callees]` | 3.3 | +| `mcppls query symbol [--at FILE:LINE:COLUMN] [--id ID] [--kind K] [--module M] [--fresh]` | 3.1 | +| `mcppls query refs [--no-declaration] [--fresh]` | 3.2 | +| `mcppls query calls [--callees] [--fresh]` | 3.3 | | `mcppls query outline ` | 3.4 | | `mcppls query module [--file F] [--graph]` | 3.5 | | `mcppls diagnostics ... [--no-fresh]` | 3.6 | diff --git a/docs/zh-CN/20-projects.md b/docs/zh-CN/20-projects.md index a038dced..1425a748 100644 --- a/docs/zh-CN/20-projects.md +++ b/docs/zh-CN/20-projects.md @@ -96,3 +96,5 @@ C++26 能用到什么,取决于 clangd 23.1:包索引(pack indexing)、` | "needs a download" | 离线规划需要本机没有的依赖;这期间工程按源码提供服务,并提供解决的操作(或者在终端里构建:它会自己升级) | | "files the build generates … do not exist yet" | 规则的输出(比如 Qt 表单的头文件)还没构建出来;构建一次,包含它的文件就有语义 | | "implementation unit(s) cannot be read" | 某个实现单元编译不了(通常是缺头文件),所以跳到定义到不了其中的定义 | + +模块工程怎么组织才能构建快、补全快——包装模块、每个头文件只出现在一处、`import std` 不与标准库 `#include` 混用——见[模块工程组织](25-module-projects.md)。 diff --git a/docs/zh-CN/25-module-projects.md b/docs/zh-CN/25-module-projects.md new file mode 100644 index 00000000..7f88f37c --- /dev/null +++ b/docs/zh-CN/25-module-projects.md @@ -0,0 +1,154 @@ +# 模块工程怎么组织 + +[English](../25-module-projects.md) | **简体中文** + +不按这里做也能构建。这里写的是让大型工程(模块、头文件、第三方代码混在一起)构建快、补全快、也容易保持一致的组织方式。每一条都写了原因,方便你判断什么时候不适用。 + +## 模块的代价在哪里、什么时候付 + +模块接口编译一次,得到一个 BMI(二进制模块接口)。导入它的文件加载 BMI,而不是再解析一遍源码。由此有两种代价: + +- **构建。** 基于 BMI 编译,比解析生成它的那些头文件便宜得多:在我们测过的一个中等规模工程里,导入一个包装模块的单元,比直接 include 同样那些头文件的单元编译快大约四倍。但改动一个接口,会重建它的 BMI,以及所有直接或间接导入它的 BMI。 +- **编辑。** 在导入了模块的文件里,每次补全、悬停、跳转都要为这次请求加载该文件整个导入闭包的 BMI。BMI 越少、越小,答得越快。每个重头文件只出现在一处的闭包,每个声明只加载一次;十个模块各带一份同一头文件的闭包,就要加载十份,编译器在每次请求时都要把它们合并。 + +下面的规则都由这两种代价推出来。 + +## 1. 每个重头文件只放进一个包装模块 + +每个大的第三方头文件(JSON、TOML 库,GUI 框架,`vulkan.hpp`)放进一个专门模块的全局模块片段里,导出你的代码用到的名字: + +```cpp +// src/third_party/toml.cppm +module; +#include +export module third_party.toml; + +export namespace toml { + using toml::value; + using toml::parse; + using toml::find; +} +``` + +其他模块都 `import third_party.toml`,自己不再 include ``。 + +**原因。** 放在某个模块全局模块片段里的头文件,会成为这个模块 BMI 的一部分。在十个模块里 include,它就在十个 BMI 里;导入其中几个的文件,每次构建、每次补全都要加载每一份并合并重复的声明。放在一个包装模块里,它只解析一次、存一次、加载一次。 + +## 2. 不要把标准库的 `#include` 和 `import std` 混用 + +导入了 `std` 的单元里,不要 include 任何标准头文件:既不直接 include,也不通过会 include 它们的头文件间接 include。需要标准库的第三方头文件放进它自己的包装模块(规则 1),它的 `#include ` 就留在包装模块的全局模块片段里。 + +```cpp +// 避免:同一批声明来自两处 +module; +#include +#include "my_lib.hpp" // 里面 include 了 、 +export module app.core; +import std; +``` + +```cpp +// 推荐 +export module app.core; +import std; +import third_party.my_lib; // 包装模块 include my_lib.hpp,、 随之留在那里 +``` + +**原因。** `import std` 把标准库作为一个模块带进来;同一单元里的 `#include ` 又带进同一批声明的一份文本副本。编译器要逐对证明它们相同再合并,这很慢;在现在的编译器上,这也是混合工程里 "redefinition"、"ambiguous" 错误最常见的来源。 + +## 3. 全局模块片段只放接口需要的东西 + +接口的全局模块片段里,只放它导出的声明会提到的头文件。只有实现需要的头文件放进实现单元: + +```cpp +// app/net.cppm —— 接口 +export module app.net; +import std; +export namespace app::net { + struct Response { int status; std::string body; }; + Response get(std::string_view url); +} +``` + +```cpp +// app/net.cpp —— 实现 +module; +#include // 只在这里:导入方永远不加载它 +module app.net; +namespace app::net { + Response get(std::string_view url) { /* ... */ } +} +``` + +**原因。** 接口 include 的东西都在它的 BMI 里,每个导入方都要加载。实现单元 include 的东西只影响它自己。改动实现单元不会改变 BMI,导入这个模块的东西都不用重建。 + +## 4. 函数体放进实现单元 + +模块接口里,在类内定义的成员函数不像在头文件里那样隐式 `inline`;而改接口单元里的任何函数体,都是在改接口。经常改的函数体放进 `module X;` 实现单元;接口里留下导入方需要看到的东西:声明、模板、`constexpr` 函数、小的 inline 辅助函数。 + +**原因。** 接口单元的每次改动都会重建它的 BMI,以及导入它的所有 BMI。在实现单元里改函数体,只重建那一个单元。 + +## 5. 用什么就导入什么,避免层层再导出 + +`export import` 用于把若干分区汇总成一个门面模块,或者某个类型本身就是模块接口的一部分。不要用它把模块恰好用到的东西转手出去: + +```cpp +// 避免 +export module app.defines; +export import std; // 每个导入 app.defines 的文件都经由它导入了 std +export import third_party.toml; + +export module app.tool; +export import app.defines; // ……每个导入 app.tool 的文件,也拿到了上面全部 +``` + +```cpp +// 推荐 +export module app.tool; +import std; +import third_party.toml; +import app.defines; // 它只导出自己的名字 +``` + +**原因。** `export import` 把整个被再导出的闭包放进每个导入方的可见范围:每次请求加载的 BMI 更多,每个补全列表里的名字更多,导入方还多了一个自己从没声明过的依赖——从链条中间去掉它,很远的文件就会坏。永远不要 `export import std`。 + +## 6. 大模块按改动的边界拆开 + +接口有几千行、又被工程大部分代码导入的模块,任何一处改动都会连同它的所有导入方一起重建。按照一起改动的边界把它拆成几个模块(或者门面后面的几个分区):稳定的核心类型是一块,正在开发的部分是另一块。 + +**原因。** 一次改动之后重建的量,等于导入被改接口的那些东西的量。依赖图底部是小而稳定的模块、经常改的模块靠近顶部,这个量就小。 + +## 7. 平台头文件单独包装 + +```cpp +// src/platform/win32.cppm +module; +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +export module platform.win32; + +export namespace win32 { + using ::HANDLE; + using ::CreateFileW; + using ::CloseHandle; +} +``` + +**原因。** `windows.h` 和 POSIX 头文件很大,还定义了会和普通代码冲突的宏(`min`、`max`、`ERROR`)。宏不会越过模块边界,包装起来以后它们就留在包装模块里。其他代码确实需要某个宏时,把它放进一个只定义这个宏的小头文件。 + +## 8. 模块名跟目录走 + +模块按目录命名(`app/net/` 里是 `app.net`),每个目录一个主接口,外面没人导入的部分用分区(`app.net:http`)。这样构建工具、mcppls 和人都能从模块名找到它的源码。 + +## 模块工程里的纯头文件代码 + +不导入任何模块的文件(头文件,以及只 include 的单元)照旧工作:引擎在它们里面像不开模块支持时一样从索引补全,不为工程里的模块付出代价。完全没有模块的工程,引擎不开模块支持。上面的规则针对的是导入了模块的文件。 + +## 看清代价在哪里 + +- 报告里的 `slowestFiles` 列出补全最慢的十个文件,`engines[].details.buildTimes` 说明每个文件的构建花在哪里:preamble、导入的模块、AST 构建。 +- 日志行 `Prerequisites of : modules, reused, built …` 说明一个文件的导入闭包有多大、花了多少(见[出问题时](50-troubleshooting.md))。 +- `Rebuilding module : ` 指出引起重建的那次改动。 + +导入闭包有几十个模块的文件,或者无论改什么都会重建的模块,是这些规则最先见效的地方。 diff --git a/docs/zh-CN/30-settings.md b/docs/zh-CN/30-settings.md index 8cd8c64e..39d59fc3 100644 --- a/docs/zh-CN/30-settings.md +++ b/docs/zh-CN/30-settings.md @@ -62,6 +62,10 @@ VS Code 扩展已经会这样做);`重新加载模型` 只重新加载项目 | `mcppls.semanticTokens.modules` | `true`, `false` | `true` | — | 重启 | 用服务端的语义 token 给 `import`、`module`、`export` 和模块名上色。关闭后只用语法文件的颜色。 | | `mcppls.semanticTokens.moduleType` | `true`, `false` | `false` | — | 重启 | 客户端声明自己认得自定义的 `module` 语义 token 类型和 `partition` 修饰符;除本仓库的 VS Code 扩展外都关闭,因为没有别的客户端会声明它。不是 package.json 里的设置:VS Code 扩展自己贡献了这个 token 类型,因此固定声明为开。 | | `mcppls.completion.triggerOnSpace` | `true`, `false` | `true` | — | 重启 | 在 `import` 或 `export import` 后输入空格时立即弹出模块列表;其他位置的空格不会发给服务端。什么都不说的客户端只有在自证是 VS Code 或其分支时才会得到这个行为;其他客户端需要用 `initializationOptions.completion.triggerOnSpace: true` 主动开启。 | +| `mcppls.answer.cache` | `on`, `memory`, `off` | `on` | `--answer-cache` | 重启 | 保留引擎对补全、签名帮助、悬停和跳转的答复;引擎慢于 `answer.budget` 时立即给出保留的答复,引擎自己的答复到达后再替换。`on` 还会跨会话保留,`memory` 只在 mcppls 运行期间保留,`off` 与 0.0.14 的行为相同。 | +| `mcppls.answer.budget` | ? | ? | `--answer-budget` | 重启 | 补全、签名帮助或悬停等待引擎多少毫秒(100-2000),之后改给保留的答复。只在有保留答复时生效,否则照旧等待。 | +| `mcppls.answer.prefetch` | `true`, `false` | `true` | — | 重启 | 引擎空闲时,向它询问当前文件里已有的 `.`、`->`、`::` 之后的补全,让下次输入时已有保留的答复。一次只问一个,有人输入时不问。 | +| `mcppls.completion.provenance` | `subtle`, `off`, `verbose` | `subtle` | — | 重启 | 用保留答复作答时如何显示:`subtle` 只在有疑问的项(此后新增或删除的成员)后加一个浅色 `·`,其余不显示;`off` 什么都不显示;`verbose` 写出每一项和悬停的来源,用于报告问题。 | | `mcppls.format.fallbackStyle` | 字符串 | `auto` | `--format-fallback-style` | 重启 | 没有项目 .clang-format 时使用的格式样式。auto 在引擎支持时为 mcpp 项目使用固定的 mcpp 样式,其他情况沿用 clangd 默认。命名样式覆盖 auto;mcpp 可让其他项目主动启用,但要求引擎支持。项目 .clang-format 始终优先。 | | `mcppls.index.primeImplementationUnits` | `auto`, `off` | `auto` | `--prime-implementation-units` | 重启 | 在后台让 clangd 逐个构建模块的实现单元(每次少量),这样即使实现文件从没打开过,跳到定义也能到达只在实现单元里的定义。clangd 自己的后台索引看不到模块单元的导入(WA-CLANGD-008)。`off`:只有一次跳转请求所搜索的单元和你打开的文件会为此被索引。 | | `mcppls.enable` | `true`, `false` | `true` | — | 立即生效 | 为此工作区启动 mcppls。在某个工作区的设置里设为 `false`,扩展在那里保持安装但不启动——用于它暂时还服务不了的项目,而不必卸载;状态栏上的条目可以重新打开它。 | @@ -76,10 +80,11 @@ VS Code 扩展已经会这样做);`重新加载模型` 只重新加载项目 | `mcppls.trace.server` | `off`, `messages`, `verbose` | `off` | — | 立即生效 | 把 LSP 通信记录到 C++ Modules 输出通道(Trace 级别);`verbose` 还会打开服务端的 debug 日志(通过附加 `--log-level debug`,Debug 级别)。要看到它们,需把该输出通道的日志级别调到对应级别。 | | `MCPPLS_LOG_LEVEL` | `debug`, `info`, `warning`, `error` | (空) | — | 重启 | 覆盖 VS Code 扩展启动服务端时使用的日志级别,优先于 `trace.server`。 | -### AI 评审 +### AI 评审与 agent | 设置 | 取值 | 默认值 | 命令行 | 生效方式 | 作用 | |---|---|---|---|---|---| +| `mcppls.answer.agent` | `cached`, `authoritative` | `cached` | — | 重启 | MCP 工具可以用什么作答:`cached` 允许在引擎较慢时用保留的答复代替,并在结果的 `provenance` 中注明(`authoritative: false`);`authoritative` 等待引擎。单次调用传 `fresh: true` 时,两种设置下都等待。 | | `mcppls.ai.enabled` | `true`, `false` | `false` | — | 立即生效 | 显示 AI 时代的功能:Review Changes 用 mcppls 的规则对照 `HEAD` 审查工作区的变更,并把发现连同证据以问题的形式展示。除非这里开启并且另行配置了模型来源,否则不会向任何模型发送内容。 | ### 路径 diff --git a/docs/zh-CN/40-agents.md b/docs/zh-CN/40-agents.md index da941c3a..95ce4b05 100644 --- a/docs/zh-CN/40-agents.md +++ b/docs/zh-CN/40-agents.md @@ -24,4 +24,14 @@ - **它们按模块来,不是按文件来。**"谁调用了它"的搜索范围是所有导入了其声明模块的模块,而不是恰好出现该名称的文件。 - **它们从编辑器所用的同一个模型给出答案**,这样 Agent 和人看到的是同一份构建。 +**答得快,也说清楚是哪一种答案。** 查符号、查引用依赖编译器的答案(悬停、跳转)。编译器一时答得慢时,它之前对同一问题给过的答案可以先顶上,查询不必等上几秒。凡是用到这类答案的结果,都会在 `provenance` 里写明: + +```json +"provenance": { "authoritative": false, "answers": 3, + "notAuthoritative": [ { "source": "cache:K1", "certainty": "uncertain", "ageMs": 41000, + "note": "The location comes from an answer kept 41 s ago; files changed since, so it may be out of date." } ] } +``` + +`authoritative: true` 表示每个答案都是编译器针对这次问题给出的。传 `fresh: true`(MCP)或 `--fresh`(命令行)可等编译器自己的答案;`cxx_verify`、`cxx_impact`、`cxx_review` 总是这样做。要让所有查询都等,把 `mcppls.answer.agent` 设为 `authoritative`(见[设置](30-settings.md))。 + **依赖模型的审查**是可选的,默认关闭。关闭时,审查只运行确定性规则,服务端完全不发起网络调用。接入方式,以及共享工作区守护进程,见 [editors/agents/README.md](../../editors/agents/README.md)。 diff --git a/docs/zh-CN/50-troubleshooting.md b/docs/zh-CN/50-troubleshooting.md index 9abe920b..0df4db88 100644 --- a/docs/zh-CN/50-troubleshooting.md +++ b/docs/zh-CN/50-troubleshooting.md @@ -41,6 +41,10 @@ | `clangd (): I[…] Module memos: BMI verdicts and scans kept from the last session …` | mcppls-clangd 0.0.15,启动时:上次会话留下了多少条可以核对而不必重做的结果(每条使用前都会再核对) | | `clangd (): I[…] Module inputs: managed package store(s) checked by their markers` | 引擎按版本核对的包仓库(mcpp 的、xlings 的)有几个 | | `the last files in use (): of them, opened in clangd ahead of the editor once it takes files` | 启动时:上次在编辑的文件会在编辑器请求之前就在 clangd 里打开;` was ready in clangd ms before the editor opened it` 说明提前准备起了作用,`... is still being built there; the editor takes it as it is` 说明编辑器打开时 clangd 还在构建它、直接接着用,两分钟内编辑器没打开的会再关掉 | +| `clangd (): I[…] Module inputs: a check holds for s while no file change is reported` | mcppls-clangd 0.0.16,启动时:一次模块输入核对在这段时间内、且没有文件变化报告时一直有效;补全耗时那一行里的 ` checks still holding` 是因此省下的核对 | +| `answer :: source= certainty=uncertain age= took= engine=pending` | 0.0.15:clangd 慢于 `mcppls.answer.budget` 时,用之前的答案先作答,而它依赖的东西之后有变化;没有变化的那些只记在 `debug` | +| `answer :: source=engine certainty=authoritative took= replaced=<来源> diff=-/+` | 先给出答案之后 clangd 自己的答案到了,差了这么多项(下一次按键就替换掉);完全相同时不写 | +| `answers: kept from the last session ( KB), read in ms` | 启动时:答案层读回了多少 | | `the cache is not on a Dev Drive: …` | 只在 Windows 上,只说一次:见 [Windows:缓存放在 Dev Drive 上](#windows缓存放在-dev-drive-上) | | `restarting clangd (): <原因> ()` | 每次重启,以及是否计入 clangd 的重启预算 | | `the timers of took ms, ms of them in ` | 是什么占住了事件循环 | @@ -110,6 +114,8 @@ mcppls report --bundle problem.zip --root path/to/project # 可加 --hide-proj **补全只有文件里的词,或悬停提示说正在准备模块。** 每种请求给 clangd 的都有预算——补全和签名帮助 1 秒,若某文件的补全 clangd 总是刚好超过 1 秒才回答(clangd 23.1 的模块导入方,上游缺陷 UP-25),预算最多放宽到 2.5 秒,让这些答案不会正好在预算线上被取消——悬停 2 秒,跳转到定义 10 秒——超过之后 mcppls 用手上有的东西作答。补全这时给出的是文件里离光标最近的那些词,是一份不完整的列表,所以你继续输入时编辑器会再问一次;悬停在模块准备期间给出的是一行说明。这是 clangd 正忙着处理模块,不是故障。从 0.0.8 起,clangd 迟到的补全不再丢弃:它最多再算 10 秒,你在同一个词里继续输入时发出的请求都等它,一到就交给它们,所以在 clangd 重建得慢的文件里,列表仍会在你打完这个词之前出现。报告里的 `requests..answeredBy` 按方法统计了各由哪个引擎作答;`engineP50Ms`、`engineP95Ms` 是引擎作答的那些请求在引擎里花的时间,`overheadP50Ms`、`overheadP95Ms` 是 mcppls 在其外加的时间,由此看出一次慢的补全慢在谁;`completion.late` 统计 clangd 迟到的答案和用上它们的请求;`slowestFiles` 列出最慢的十个文件,以及每个文件有多少次补全只拿到了词;`engines[].details.buildTimes` 说明 clangd 构建每个文件花在哪里(preamble、导入的模块、AST 构建次数)。 +**补全列表立刻出现,过一会儿又小有变化;某一项末尾有个小小的 `·`。** 从 0.0.15 起,clangd 慢于 `mcppls.answer.budget`(450 ms)时,mcppls 先给出 clangd 之前在同一位置或对同一名字给过的答案,你继续输入时 clangd 自己的答案替换它。某一项描述后面的 `·` 表示 mcppls 对它存疑:它是一个之后文本有改动的类的成员。报告里的 `answers` 说明这种情况发生了多少次(`answers`)、有多快(`fromCache`、`fromEngine`)、clangd 的答案有多少次不一样(`replaced`)。想看每个答案从哪里来,把 `mcppls.completion.provenance` 设为 `verbose`;什么都不保留,把 `mcppls.answer.cache` 设为 `off`(`memory` 则不写磁盘)。跳转只在它落到的文件没变、或者能在那个文件里唯一地重新找到这个声明时,才用之前的答案。 + **模块单元里出现"未使用的头文件"警告。** 这是 clangd 的 include cleaner,默认开启,mcppls 不关它:在模块接口的全局模块片段、实现单元和导入方里,它和在普通文件里一样,只报没有任何东西用到的头文件(有一个 conformance fixture 在 clangd 升级时守着这一点)。要关掉,在项目的 `.clangd` 或你的 clangd `config.yaml` 里写 `Diagnostics: { UnusedIncludes: None }`;mcppls 启动的 clangd 两处都会读。 **准备模块期间编辑器有点卡。** clangd 拿到机器的线程数减一(最少 2 个、最多 8 个,内存小的机器更少);`mcppls.engine.workers`(`auto` 或一个数字)可以覆盖这个值。有你打开的文件在等模块准备时,准备工作用掉除一个之外的全部 worker,否则用一半。为 clangd 的索引构建实现单元——也就是对从没打开过的 `.cpp` 文件做跳转到定义所需要的——一次只构建一个,而且要在你 10 秒内没有输入、没有打开文件、也没有发起请求之后才开始。clangd 因构建描述变化需要的重启,要等到输入停顿 3 秒之后(最多等 60 秒);崩溃或者 clangd 不再应答,仍然立即重启。从 0.0.8 起,mcppls 为准备模块而在 clangd 里打开的单元,在模块一建好就关掉(BMI 留在 clangd 磁盘上的模块缓存里):以前它们要等全部准备结束才关,期间每次保存都要被 clangd 重新检查一遍,开着自动保存时,就占走了你正在编辑的文件要等的 worker。 diff --git a/docs/zh-CN/90-architecture.md b/docs/zh-CN/90-architecture.md index a06b9125..51859074 100644 --- a/docs/zh-CN/90-architecture.md +++ b/docs/zh-CN/90-architecture.md @@ -23,6 +23,7 @@ - **两个引擎服务同一个工作区。** mcppls 自己的引擎从它自有的语法索引出发,回答模块级别的请求;其余的由 clangd 回答,靠 mcppls 为它写的数据库驱动。 - **服务端启动的外部程序不能拖住服务端。** 构建工具、编译器探测、CMake 和 git 全部经由同一个运行器执行,该运行器给每次运行分配独立的进程单元、限制读取量,并记录下发生了什么。 - **每个请求给 clangd 都有预算,并在预算内作答。** 补全和签名帮助 1 秒,悬停 2 秒,跳转到定义 10 秒。超过之后 mcppls 用手上有的东西作答——补全给出文件里离光标最近的词,作为不完整的列表,让编辑器再问;模块准备期间的悬停给出一行说明——并取消发给 clangd 的请求。报告按方法统计由哪个引擎作答(`requests..answeredBy`)。 +- **clangd 答得慢时,用它之前的答案先顶上。** 补全、悬停、签名帮助和跳转会按位置、类型和名字保存 clangd 的答案。clangd 慢于 `mcppls.answer.budget`(450 ms)时先给出保存的答案,你继续输入时由 clangd 自己的答案替换;列表里除了 mcppls 存疑的那一项带个小小的 `·`,看不出任何区别。跳转只在它落到的文件没变时这样做。Agent 和命令行会被告知哪些答案不是 clangd 自己的,也可以选择等它(见 [Agent 与 CI](40-agents.md))。正确性始终以 clangd 为准。 - **后台工作只在空闲时做。** 为 clangd 的索引构建实现单元时一次只做一个,而且要在 10 秒内没有输入、没有打开文件、也没有请求之后;clangd 因构建描述变化需要的重启,要等输入停顿 3 秒之后(最多 60 秒)。崩溃或 clangd 不再应答仍然立即重启。有打开的文件在等模块准备时,准备工作用掉除一个之外的全部 worker,否则用一半。 - **无法恢复的情况立即记录下来。** 服务端无法自行恢复时,会写出一个已脱敏的诊断包(`/bundles/auto--